diff --git a/src/ops-console/components/campaignMessages/CampaignMessages.tsx b/src/ops-console/components/campaignMessages/CampaignMessages.tsx index eb8e2baeea..c50949fe5f 100644 --- a/src/ops-console/components/campaignMessages/CampaignMessages.tsx +++ b/src/ops-console/components/campaignMessages/CampaignMessages.tsx @@ -8,12 +8,14 @@ import { useCampaignMessagesController, } from './CampaignMessagesLogic'; import type { CampaignMessagesScheduleType } from './CampaignMessagesLogic'; +import type { CampaignFrequency } from '../../../services/api/ServiceApi'; import './CampaignMessages.css'; interface CampaignMessagesProps { campaignId?: string; campaignStartDate?: string; campaignEndDate?: string; + campaignFrequency?: CampaignFrequency; isCampaignCancelled?: boolean; } @@ -21,6 +23,7 @@ const CampaignMessages: React.FC = ({ campaignId, campaignStartDate, campaignEndDate, + campaignFrequency, isCampaignCancelled, }) => { const { t } = useTranslation(); @@ -28,6 +31,7 @@ const CampaignMessages: React.FC = ({ campaignId, campaignStartDate, campaignEndDate, + campaignFrequency, isCampaignCancelled, translate: (key) => String(t(key)), }); diff --git a/src/ops-console/components/campaignMessages/CampaignMessagesLogic.ts b/src/ops-console/components/campaignMessages/CampaignMessagesLogic.ts index 3027946086..fdbca3e8ae 100644 --- a/src/ops-console/components/campaignMessages/CampaignMessagesLogic.ts +++ b/src/ops-console/components/campaignMessages/CampaignMessagesLogic.ts @@ -9,11 +9,16 @@ import { } from 'react'; import { ServiceConfig } from '../../../services/ServiceConfig'; import { + CampaignFrequency, CampaignMessagingQueryParams, CampaignMessagingRow, } from '../../../services/api/ServiceApi'; import { hasCampaignWriteAccess } from '../../../services/api/campaignListingHelpers'; import { Json } from '../../../services/database'; +import { + buildFrequencyTimelineDates, + DEFAULT_FREQUENCY, +} from '../campaignSetup/campaignAssignmentUtils'; import { buildCampaignDurationTimelineDates } from '../campaignSetup/campaignCommunicationUtils'; import { useAppSelector } from '../../../redux/hooks'; import { AuthState } from '../../../redux/slices/auth/authSlice'; @@ -119,6 +124,7 @@ interface UseCampaignMessagesControllerParams { campaignId?: string; campaignStartDate?: string; campaignEndDate?: string; + campaignFrequency?: CampaignFrequency; isCampaignCancelled?: boolean; translate: (key: string) => string; } @@ -681,6 +687,7 @@ export const useCampaignMessagesController = ({ campaignId, campaignStartDate, campaignEndDate, + campaignFrequency = DEFAULT_FREQUENCY, isCampaignCancelled = false, translate, }: UseCampaignMessagesControllerParams): CampaignMessagesController => { @@ -716,6 +723,17 @@ export const useCampaignMessagesController = ({ Record >({}); const timelineDates = useMemo( + () => + campaignStartDate && campaignEndDate + ? buildFrequencyTimelineDates( + campaignStartDate, + campaignEndDate, + campaignFrequency, + ) + : [], + [campaignEndDate, campaignFrequency, campaignStartDate], + ); + const campaignRangeDates = useMemo( () => campaignStartDate && campaignEndDate ? buildCampaignDurationTimelineDates(campaignStartDate, campaignEndDate) @@ -755,8 +773,8 @@ export const useCampaignMessagesController = ({ // Fetch the full calendar range so persisted Sunday rows cannot displace // later non-Sunday campaign dates from the paginated response. const pageSize = - timelineDates.length > 0 - ? timelineDates.length + campaignRangeDates.length > 0 + ? campaignRangeDates.length : CAMPAIGN_MESSAGES_PAGE_SIZE; const loadedMessagesData = await loadCampaignMessagesData( campaignId, @@ -778,7 +796,7 @@ export const useCampaignMessagesController = ({ return () => { isMounted = false; }; - }, [campaignId, displayTimelineDates, timelineDates.length]); + }, [campaignId, campaignRangeDates.length, displayTimelineDates]); useEffect( () => () => { @@ -1082,8 +1100,8 @@ export const useCampaignMessagesController = ({ { page: 1, pageSize: - timelineDates.length > 0 - ? timelineDates.length + campaignRangeDates.length > 0 + ? campaignRangeDates.length : CAMPAIGN_MESSAGES_PAGE_SIZE, }, ); diff --git a/src/ops-console/components/campaignSetup/CampaignCommunicationTimelineStep.tsx b/src/ops-console/components/campaignSetup/CampaignCommunicationTimelineStep.tsx index cb5215883e..ad086086a1 100644 --- a/src/ops-console/components/campaignSetup/CampaignCommunicationTimelineStep.tsx +++ b/src/ops-console/components/campaignSetup/CampaignCommunicationTimelineStep.tsx @@ -18,6 +18,7 @@ const CampaignCommunicationTimelineStep: React.FC< CampaignCommunicationTimelineStepProps > = ({ form, + frequency, assignmentDrafts, selectedSchoolIds, communicationState, @@ -31,8 +32,8 @@ const CampaignCommunicationTimelineStep: React.FC< const { t } = useTranslation(); const timeOptions = useMemo(() => buildTimeOptions(), []); const timelineDates = useMemo( - () => buildCommunicationTimelineDates(assignmentDrafts, form), - [assignmentDrafts, form], + () => buildCommunicationTimelineDates(assignmentDrafts, form, frequency), + [assignmentDrafts, form, frequency], ); const { campaignReach, loadingReach } = useCampaignReach(selectedSchoolIds); diff --git a/src/ops-console/components/campaignSetup/campaignAssignmentUtils.ts b/src/ops-console/components/campaignSetup/campaignAssignmentUtils.ts index 13a00dbf1e..29a3b2ee22 100644 --- a/src/ops-console/components/campaignSetup/campaignAssignmentUtils.ts +++ b/src/ops-console/components/campaignSetup/campaignAssignmentUtils.ts @@ -1,10 +1,11 @@ import { CampaignAssignmentChapterOption, CampaignAssignmentSubjectOption, + CampaignFrequency, } from '../../../services/api/ServiceApi'; import { CampaignSetupFormState } from './types'; -export type Frequency = 'daily' | 'alternate_days' | 'alternate_week'; +export type Frequency = CampaignFrequency; export type AssignmentRow = { rowId: string; @@ -151,13 +152,18 @@ const getScheduleDates = ( return dates; }; +export const buildFrequencyTimelineDates = ( + startDate: string, + endDate: string, + frequency: Frequency, +): string[] => + getScheduleDates(startDate, endDate, frequency, Number.MAX_SAFE_INTEGER); + export const getRequiredAssignmentCount = ( startDate: string, endDate: string, frequency: Frequency, -) => - getScheduleDates(startDate, endDate, frequency, Number.MAX_SAFE_INTEGER) - .length; +) => buildFrequencyTimelineDates(startDate, endDate, frequency).length; const distributeDatesAcrossAssignments = ( dates: string[], diff --git a/src/ops-console/components/campaignSetup/campaignCommunicationTypes.ts b/src/ops-console/components/campaignSetup/campaignCommunicationTypes.ts index 44d769926c..3da02b94d5 100644 --- a/src/ops-console/components/campaignSetup/campaignCommunicationTypes.ts +++ b/src/ops-console/components/campaignSetup/campaignCommunicationTypes.ts @@ -1,4 +1,4 @@ -import { CampaignAssignmentDraft } from './campaignAssignmentUtils'; +import { CampaignAssignmentDraft, Frequency } from './campaignAssignmentUtils'; import { CampaignCommunicationRowState, CampaignCommunicationState, @@ -8,6 +8,7 @@ import { CampaignSetupFormState } from './types'; export type CampaignCommunicationTimelineStepProps = { form: CampaignSetupFormState; + frequency: Frequency; assignmentDrafts: CampaignAssignmentDraft[]; selectedSchoolIds: string[]; communicationState: CampaignCommunicationState; diff --git a/src/ops-console/components/campaignSetup/campaignCommunicationUtils.ts b/src/ops-console/components/campaignSetup/campaignCommunicationUtils.ts index 62faf438dd..4cbd048100 100644 --- a/src/ops-console/components/campaignSetup/campaignCommunicationUtils.ts +++ b/src/ops-console/components/campaignSetup/campaignCommunicationUtils.ts @@ -1,6 +1,10 @@ import { t } from 'i18next'; import { CAMPAIGN_OBJECTIVE } from '../../../common/constants'; -import { CampaignAssignmentDraft } from './campaignAssignmentUtils'; +import { + buildFrequencyTimelineDates, + CampaignAssignmentDraft, + Frequency, +} from './campaignAssignmentUtils'; import { CampaignSetupFormState } from './types'; export type CampaignCommunicationRowState = { @@ -51,11 +55,16 @@ export const createEmptyCommunicationRow = export const buildCommunicationTimelineDates = ( assignmentDrafts: CampaignAssignmentDraft[], form?: CampaignSetupFormState, + frequency?: Frequency, ): string[] => { if (form?.objective === CAMPAIGN_OBJECTIVE.HOMEPAGE_LEARNING_PATHWAY) { return buildCampaignDurationTimelineDates(form.startDate, form.endDate); } + if (form && frequency) { + return buildFrequencyTimelineDates(form.startDate, form.endDate, frequency); + } + return Array.from(new Set(assignmentDrafts.map((draft) => draft.startsAt))) .filter(Boolean) .sort(); diff --git a/src/ops-console/components/campaignsOverview/CampaignsOverview.tsx b/src/ops-console/components/campaignsOverview/CampaignsOverview.tsx index 58c56ddbc9..03c75eaf64 100644 --- a/src/ops-console/components/campaignsOverview/CampaignsOverview.tsx +++ b/src/ops-console/components/campaignsOverview/CampaignsOverview.tsx @@ -163,6 +163,9 @@ const CampaignsOverview: React.FC = ({ campaignEndDate={ campaignOverviewData?.data?.campaign?.end_date ?? undefined } + campaignFrequency={ + campaignOverviewData?.data?.campaign?.frequency ?? undefined + } /> )} diff --git a/src/ops-console/components/campaignsOverview/CampaignsOverviewLogic.ts b/src/ops-console/components/campaignsOverview/CampaignsOverviewLogic.ts index 5855a35fe2..08b26b6a7a 100644 --- a/src/ops-console/components/campaignsOverview/CampaignsOverviewLogic.ts +++ b/src/ops-console/components/campaignsOverview/CampaignsOverviewLogic.ts @@ -1,3 +1,5 @@ +import type { CampaignFrequency } from '../../../services/api/ServiceApi'; + export type CampaignsOverviewDisplayValue = | string | number @@ -72,6 +74,7 @@ export interface CampaignsOverviewApiCampaign { objective?: string | null; start_date?: string | null; end_date?: string | null; + frequency?: CampaignFrequency | null; updated_at?: string | null; campaign_status?: CampaignStatus | null; manager?: CampaignsOverviewApiPerson | null; diff --git a/src/ops-console/hooks/useCampaignSetupForm.ts b/src/ops-console/hooks/useCampaignSetupForm.ts index 69ccca05f6..4b129c1995 100644 --- a/src/ops-console/hooks/useCampaignSetupForm.ts +++ b/src/ops-console/hooks/useCampaignSetupForm.ts @@ -94,6 +94,13 @@ export const useCampaignSetupForm = () => { const [submitAttempted, setSubmitAttempted] = useState(false); const [rewardSubmitAttempted, setRewardSubmitAttempted] = useState(false); const [message, setMessage] = useState(null); + const assignmentFrequency = useMemo( + () => + assignmentConfigs[activeAssignmentGradeId]?.frequency ?? + Object.values(assignmentConfigs)[0]?.frequency ?? + DEFAULT_FREQUENCY, + [activeAssignmentGradeId, assignmentConfigs], + ); const audience = useCampaignAudienceSelection({ api, @@ -431,6 +438,7 @@ export const useCampaignSetupForm = () => { assignmentOptions, areRewardsValid, assignmentConfigs, + assignmentFrequency, assignmentDrafts, campaignRewards, createdCampaignId, diff --git a/src/ops-console/pages/CampaignSetupPage.tsx b/src/ops-console/pages/CampaignSetupPage.tsx index 6f1aa6a412..00d8715a5e 100644 --- a/src/ops-console/pages/CampaignSetupPage.tsx +++ b/src/ops-console/pages/CampaignSetupPage.tsx @@ -68,8 +68,13 @@ const CampaignSetupPage: React.FC = () => { buildCommunicationTimelineDates( campaignSetup.assignmentDrafts, campaignSetup.form, + campaignSetup.assignmentFrequency, ), - [campaignSetup.assignmentDrafts, campaignSetup.form], + [ + campaignSetup.assignmentDrafts, + campaignSetup.assignmentFrequency, + campaignSetup.form, + ], ); const communicationValidation = useMemo( @@ -292,6 +297,7 @@ const CampaignSetupPage: React.FC = () => { const campaign = { programId: campaignSetup.form.programId, campaignName: campaignSetup.form.campaignName.trim(), + frequency: campaignSetup.assignmentFrequency, objective: campaignSetup.form.objective as CampaignObjective, targetType: campaignSetup.form.objective === CAMPAIGN_OBJECTIVE.HOMEWORK @@ -525,6 +531,7 @@ const CampaignSetupPage: React.FC = () => { ) : campaignSetup.activeStep === 3 ? (