diff --git a/backend/src/routes/leaves.js b/backend/src/routes/leaves.js index 483a2c2..f4cb208 100644 --- a/backend/src/routes/leaves.js +++ b/backend/src/routes/leaves.js @@ -5,10 +5,8 @@ import { PrtLeave } from '../models/PrtLeave.js'; import { User } from '../models/User.js'; import { toLeaveDto } from '../utils/serialize.js'; import { toYmd, ymdToDate } from '../services/schedule.js'; -import { Cohort } from '../models/Cohort.js'; -import { findSubstitutes, openLeaveReassignment } from '../services/matching.js'; import { notifyLeaveDecision } from '../services/notify.js'; -import { pushApprovedLeaveSessions } from '../services/programSessions.js'; +import { shiftApprovedLeaveSessions } from '../services/programSessions.js'; const objectId = Joi.string().hex().length(24); @@ -136,24 +134,13 @@ export function leaveRoutes(server) { leave.reviewedBy = creds.user._id; leave.reviewedAt = new Date(); await leave.save(); - let substitutes = []; - let pushResult = null; + let shiftResult = null; if (leave.status === 'approved') { - const startYmd = toYmd(leave.startDate); - const endYmd = toYmd(leave.endDate); - const cohorts = await Cohort.find({ - virtualPrt: leave.user._id, - status: { $in: ['confirmed', 'active'] }, - }); - substitutes = await findSubstitutes({ excludePrtId: leave.user._id }); - pushResult = await pushApprovedLeaveSessions(leave.user._id, startYmd, endYmd, { - substitutePrtId: request.payload.substitutePrtId || undefined, - }); - if (!request.payload.substitutePrtId) { - for (const cohort of cohorts) { - await openLeaveReassignment(cohort, leave.user._id); - } - } + shiftResult = await shiftApprovedLeaveSessions( + leave.user._id, + toYmd(leave.startDate), + toYmd(leave.endDate), + ); } await notifyLeaveDecision({ prt: leave.user, @@ -164,9 +151,7 @@ export function leaveRoutes(server) { await leave.populate('reviewedBy', 'firstName lastName email'); return { leave: toLeaveDto(leave), - substitutes, - pushed: pushResult?.pushed?.length || 0, - covered: request.payload.substitutePrtId ? (pushResult?.affected?.length || 0) : 0, + shifted: shiftResult?.shifted?.length || 0, }; }, }); diff --git a/backend/src/services/programSessions.js b/backend/src/services/programSessions.js index 707c7bc..35ae1ed 100644 --- a/backend/src/services/programSessions.js +++ b/backend/src/services/programSessions.js @@ -354,30 +354,59 @@ export async function pushSessionsOnDate(ymd, reason, { prtId, notifyAdmins = fa return results; } -export async function pushApprovedLeaveSessions(prtId, startYmd, endYmd, { substitutePrtId } = {}) { - const sessions = await CohortSession.find({ - date: { $gte: startYmd, $lte: endYmd }, - status: 'scheduled', - }); - const affected = []; - for (const session of sessions) { - const cohort = await Cohort.findById(session.cohort); - if (!cohort || !isLiveCohortStatus(cohort.status)) continue; - if (String(cohort.virtualPrt) !== String(prtId)) continue; - affected.push({ session, cohort }); - } - if (substitutePrtId) { - for (const { session } of affected) { - session.coverPrt = substitutePrtId; - await session.save(); +// Slides scheduled sessions off approved leave. Earlier meetings stay, and the meeting count stays the same. +export async function shiftApprovedLeaveSessions(prtId, startYmd, endYmd) { + const settings = await getAppSettings(); + if (settings.skipPrtLeave === false) return { shifted: [] }; + const cohorts = await Cohort.find({ + virtualPrt: prtId, + status: { $in: ['confirmed', 'active'] }, + }).populate('virtualPrt', 'firstName lastName email google'); + const shifted = []; + for (const cohort of cohorts) { + const sessions = await CohortSession.find({ cohort: cohort._id }).sort({ date: 1, index: 1 }); + const hit = sessions.find((session) => ( + session.status === 'scheduled' && session.date >= startYmd && session.date <= endYmd + )); + if (!hit) continue; + const tail = sessions.filter((session) => session.status === 'scheduled' && session.date >= hit.date); + if (!tail.length) continue; + const weekdays = await scheduleWeekdays(cohort.scheduleId); + const blocked = new Set( + sessions.filter((session) => session.status === 'completed').map((session) => session.date), + ); + const dates = await collectSessionDates(hit.date, cohort, tail.length, weekdays, blocked); + if (!dates.length) continue; + const fromDate = tail[0].date; + const toDate = dates[0]; + const count = Math.min(tail.length, dates.length); + let changed = false; + for (let i = count - 1; i >= 0; i -= 1) { // later dates move first so two rows never share a date + if (tail[i].date === dates[i]) continue; + tail[i].date = dates[i]; + await tail[i].save(); + changed = true; } - return { affected: affected.map(({ session }) => session), pushed: [] }; + if (!changed) continue; + const updated = await CohortSession.find({ cohort: cohort._id }).sort({ index: 1 }); + await syncExistingMeetSeries(cohort, updated); + const admins = await adminUsers(); + await notifySessionPushed({ + prt: cohort.virtualPrt, + admins, + cohort, + fromDate, + toDate, + reason: 'Approved PRT leave', + }); + const { notifyGroupSessionChange } = await import('./whatsapp.js'); + await notifyGroupSessionChange( + cohort._id, + `The ${fromDate} session for ${cohort.name} was Approved PRT leave. The replacement date is ${toDate}.`, + ); + shifted.push({ cohortId: String(cohort._id), fromDate, toDate }); } - const pushed = []; - for (const { session } of affected) { - pushed.push(await pushSession(session, 'Approved PRT leave', { notifyAdmins: true })); - } - return { affected: affected.map(({ session }) => session), pushed }; + return { shifted }; } async function applyMeetToSessions(sessions, event, slot) { diff --git a/frontend/src/views/virtualpr/leaves/index.jsx b/frontend/src/views/virtualpr/leaves/index.jsx index 04b0e74..9e0f344 100644 --- a/frontend/src/views/virtualpr/leaves/index.jsx +++ b/frontend/src/views/virtualpr/leaves/index.jsx @@ -76,9 +76,7 @@ export default function LeavesPage() { {canPickPrt && (row.original.status || 'pending') === 'pending' ? ( <>