diff --git a/backend/src/services/programSessions.js b/backend/src/services/programSessions.js index dfa549e..0a898a6 100644 --- a/backend/src/services/programSessions.js +++ b/backend/src/services/programSessions.js @@ -165,25 +165,25 @@ async function collectSessionDates(from, cohort, count, weekdays, blocked) { return dates.slice(0, count); } -// Moves the existing Google event onto the new dates. The Meet link stays. +// Updates the cohort's Google event to cover every held and upcoming session. The Meet link stays. async function syncExistingMeetSeries(cohort, sessions) { const linked = sessions.find((session) => session.googleEventId); if (!linked) return; - const open = sessions - .filter((session) => session.status !== 'completed') + const active = sessions + .filter((session) => session.status !== 'cancelled') .sort((a, b) => String(a.date).localeCompare(String(b.date))); - if (!open.length) return; + if (!active.length) return; const host = await sessionHost(linked, cohort); if (!host?.google?.refreshToken && !host?.google?.accessToken) return; const settings = await getAppSettings(); const weekdays = await scheduleWeekdays(cohort.scheduleId); - const first = open[0]; - const last = open[open.length - 1]; + const first = active[0]; + const last = active[active.length - 1]; const times = slotDateTimes(first.date, first.slot || cohort.slot, cohort.durationMinutes || settings.defaultDurationMinutes || 60); try { await updateCalendarEvent(host, linked.googleEventId, { title: `PR Session ยท ${cohort.name}`, - description: `Pulmonary rehab repeat meetings for ${cohort.name} (${open.length} upcoming sessions).`, + description: `Pulmonary rehab repeat meetings for ${cohort.name} (${active.length} sessions).`, start: times.start, end: times.end, timeZone: env.APP_TIMEZONE, @@ -193,7 +193,7 @@ async function syncExistingMeetSeries(cohort, sessions) { interval: settings.repeatIntervalDays, startTime: times.startTime, timeZone: env.APP_TIMEZONE, - includeDates: open.map((session) => session.date), + includeDates: active.map((session) => session.date), weekdays, }), }); @@ -317,6 +317,17 @@ export async function nextAppendDate(cohort) { return cursor; } +// True when the cohort may meet on this date: its schedule weekdays if set, never Sunday, and not a holiday or PRT leave. +async function canMeetOn(cohort, date) { + if (weekdayOf(date) === 0) return false; + const weekdays = await scheduleWeekdays(cohort.scheduleId); + if (weekdays && !weekdays.map(Number).includes(weekdayOf(date))) return false; + const settings = await getAppSettings(); + if (settings.skipHolidays !== false && (await holidayDatesBetween(date, date, cohort.language)).has(date)) return false; + if (settings.skipPrtLeave !== false && await leaveCoversDate(cohort.virtualPrt?._id || cohort.virtualPrt, date)) return false; + return true; +} + // Adds today's session to open cohorts with no future session left, when today is their next meeting day. export async function appendTodaySessions(date) { const cohorts = await Cohort.find(liveCohortFilter()); @@ -324,7 +335,9 @@ export async function appendTodaySessions(date) { for (const cohort of cohorts) { if (await CohortSession.exists({ cohort: cohort._id, date })) continue; if (await CohortSession.exists({ cohort: cohort._id, status: 'scheduled', date: { $gt: date } })) continue; - if (await nextAppendDate(cohort) !== date) continue; + const next = await nextAppendDate(cohort); + if (next > date) continue; + if (next < date && !(await canMeetOn(cohort, date))) continue; // catching up after missed meeting days const ids = patientIds(cohort).map(String); const length = await cohortProgramLength(cohort, await getAppSettings()); const finished = await finishedSessionCounts(ids); @@ -343,6 +356,7 @@ export async function appendTodaySessions(date) { records: [], }); await ensureSessionRecords(cohort); + await syncExistingMeetSeries(cohort, await CohortSession.find({ cohort: cohort._id })); added += 1; } return added; @@ -386,6 +400,7 @@ export async function pushSession(session, reason, { notifyAdmins = false } = {} records: recordsFor(patientIds(cohort)), }); await ensureSessionRecords(cohort); + await syncExistingMeetSeries(cohort, await CohortSession.find({ cohort: cohort._id })); const admins = notifyAdmins ? await adminUsers() : []; await notifySessionPushed({ diff --git a/backend/src/services/schedule.js b/backend/src/services/schedule.js index 2d5fdd9..088696a 100644 --- a/backend/src/services/schedule.js +++ b/backend/src/services/schedule.js @@ -203,6 +203,7 @@ export function sessionRecurrence({ firstDate, lastDate, interval, startTime, ti let cursor = firstDate; let count = 0; const exdates = []; + const reached = new Set(); let guard = 0; while (cursor <= lastDate && count < 120 && guard < 500) { guard += 1; @@ -213,6 +214,7 @@ export function sessionRecurrence({ firstDate, lastDate, interval, startTime, ti exdates.push(cursor); } else if (!weekdayBlocked && !sunday) { count += 1; + reached.add(cursor); if (include.size && !include.has(cursor)) exdates.push(cursor); } cursor = addDays(cursor, step); @@ -226,5 +228,9 @@ export function sessionRecurrence({ firstDate, lastDate, interval, startTime, ti if (exdates.length) { lines.push(`EXDATE;TZID=${timeZone || 'Asia/Kolkata'}:${exdates.map(stamp).join(',')}`); } + const extra = [...include].filter((ymd) => !reached.has(ymd)).sort(); + if (extra.length) { + lines.push(`RDATE;TZID=${timeZone || 'Asia/Kolkata'}:${extra.map(stamp).join(',')}`); + } return lines; }