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' ? (
<>