Shift leave seesion date and remove substitute
This commit is contained in:
parent
0914b59e58
commit
2241886ca7
|
|
@ -5,10 +5,8 @@ import { PrtLeave } from '../models/PrtLeave.js';
|
||||||
import { User } from '../models/User.js';
|
import { User } from '../models/User.js';
|
||||||
import { toLeaveDto } from '../utils/serialize.js';
|
import { toLeaveDto } from '../utils/serialize.js';
|
||||||
import { toYmd, ymdToDate } from '../services/schedule.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 { notifyLeaveDecision } from '../services/notify.js';
|
||||||
import { pushApprovedLeaveSessions } from '../services/programSessions.js';
|
import { shiftApprovedLeaveSessions } from '../services/programSessions.js';
|
||||||
|
|
||||||
const objectId = Joi.string().hex().length(24);
|
const objectId = Joi.string().hex().length(24);
|
||||||
|
|
||||||
|
|
@ -136,24 +134,13 @@ export function leaveRoutes(server) {
|
||||||
leave.reviewedBy = creds.user._id;
|
leave.reviewedBy = creds.user._id;
|
||||||
leave.reviewedAt = new Date();
|
leave.reviewedAt = new Date();
|
||||||
await leave.save();
|
await leave.save();
|
||||||
let substitutes = [];
|
let shiftResult = null;
|
||||||
let pushResult = null;
|
|
||||||
if (leave.status === 'approved') {
|
if (leave.status === 'approved') {
|
||||||
const startYmd = toYmd(leave.startDate);
|
shiftResult = await shiftApprovedLeaveSessions(
|
||||||
const endYmd = toYmd(leave.endDate);
|
leave.user._id,
|
||||||
const cohorts = await Cohort.find({
|
toYmd(leave.startDate),
|
||||||
virtualPrt: leave.user._id,
|
toYmd(leave.endDate),
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await notifyLeaveDecision({
|
await notifyLeaveDecision({
|
||||||
prt: leave.user,
|
prt: leave.user,
|
||||||
|
|
@ -164,9 +151,7 @@ export function leaveRoutes(server) {
|
||||||
await leave.populate('reviewedBy', 'firstName lastName email');
|
await leave.populate('reviewedBy', 'firstName lastName email');
|
||||||
return {
|
return {
|
||||||
leave: toLeaveDto(leave),
|
leave: toLeaveDto(leave),
|
||||||
substitutes,
|
shifted: shiftResult?.shifted?.length || 0,
|
||||||
pushed: pushResult?.pushed?.length || 0,
|
|
||||||
covered: request.payload.substitutePrtId ? (pushResult?.affected?.length || 0) : 0,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -354,30 +354,59 @@ export async function pushSessionsOnDate(ymd, reason, { prtId, notifyAdmins = fa
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushApprovedLeaveSessions(prtId, startYmd, endYmd, { substitutePrtId } = {}) {
|
// Slides scheduled sessions off approved leave. Earlier meetings stay, and the meeting count stays the same.
|
||||||
const sessions = await CohortSession.find({
|
export async function shiftApprovedLeaveSessions(prtId, startYmd, endYmd) {
|
||||||
date: { $gte: startYmd, $lte: endYmd },
|
const settings = await getAppSettings();
|
||||||
status: 'scheduled',
|
if (settings.skipPrtLeave === false) return { shifted: [] };
|
||||||
});
|
const cohorts = await Cohort.find({
|
||||||
const affected = [];
|
virtualPrt: prtId,
|
||||||
for (const session of sessions) {
|
status: { $in: ['confirmed', 'active'] },
|
||||||
const cohort = await Cohort.findById(session.cohort);
|
}).populate('virtualPrt', 'firstName lastName email google');
|
||||||
if (!cohort || !isLiveCohortStatus(cohort.status)) continue;
|
const shifted = [];
|
||||||
if (String(cohort.virtualPrt) !== String(prtId)) continue;
|
for (const cohort of cohorts) {
|
||||||
affected.push({ session, cohort });
|
const sessions = await CohortSession.find({ cohort: cohort._id }).sort({ date: 1, index: 1 });
|
||||||
}
|
const hit = sessions.find((session) => (
|
||||||
if (substitutePrtId) {
|
session.status === 'scheduled' && session.date >= startYmd && session.date <= endYmd
|
||||||
for (const { session } of affected) {
|
));
|
||||||
session.coverPrt = substitutePrtId;
|
if (!hit) continue;
|
||||||
await session.save();
|
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 = [];
|
return { shifted };
|
||||||
for (const { session } of affected) {
|
|
||||||
pushed.push(await pushSession(session, 'Approved PRT leave', { notifyAdmins: true }));
|
|
||||||
}
|
|
||||||
return { affected: affected.map(({ session }) => session), pushed };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyMeetToSessions(sessions, event, slot) {
|
async function applyMeetToSessions(sessions, event, slot) {
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,7 @@ export default function LeavesPage() {
|
||||||
{canPickPrt && (row.original.status || 'pending') === 'pending' ? (
|
{canPickPrt && (row.original.status || 'pending') === 'pending' ? (
|
||||||
<>
|
<>
|
||||||
<Button size="sm" variant="success" onClick={async () => {
|
<Button size="sm" variant="success" onClick={async () => {
|
||||||
const substitutes = await api('/api/matching/substitutes');
|
await api(`/api/leaves/${row.original.id}/review`, { method: 'POST', body: { status: 'approved' } });
|
||||||
const pick = window.prompt(`Approve leave. Optional substitute PRT id (leave blank to queue/push):\n${(substitutes.items || []).map((item) => `${item.firstName} ${item.lastName} = ${item.id}`).join('\n')}`);
|
|
||||||
await api(`/api/leaves/${row.original.id}/review`, { method: 'POST', body: { status: 'approved', substitutePrtId: pick || null } });
|
|
||||||
await load();
|
await load();
|
||||||
}}>Approve</Button>
|
}}>Approve</Button>
|
||||||
<Button size="sm" variant="outline-danger" onClick={async () => {
|
<Button size="sm" variant="outline-danger" onClick={async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue