Second slice of Phase 4 (stacked on 4a crew records). Leave (site-applied, Manager-decided) with clash auto-backfill, and the daily attendance calendar, per Crewing-Implementation-Spec §5.3/§8.9–8.10. Behind NEXT_PUBLIC_CREWING_ENABLED. What's in - Schema (crewing_leave_attendance migration): LeaveRequest (LeaveType, LeaveStatus) + Attendance (AttendanceStatus, unique per assignment+date) on CrewAssignment; CrewActionType += LEAVE_APPLIED/LEAVE_DECIDED/ATTENDANCE_RECORDED. - Leave (R1): site staff apply on behalf (apply_leave); Manager decides (decide_leave) → assignment ON_LEAVE; MPO has no leave role. Leave approvals also surface in the central /approvals queue (§8.13 Leave kind). Notification LEAVE_FOR_APPROVAL. - Clash auto-backfill (R6): lib/leave-clash.ts, required strength = 1 — approving a leave that leaves the vessel with zero active same-rank cover auto-raises a LEAVE requisition via the Phase-2 autoRaiseRequisition. - Attendance (R5): daily month calendar; site staff record (record_attendance), Manager views (view_attendance) but cannot edit, MPO neither. saveAttendance bulk-upserts dirty cells. - Screens: /crewing/leave (apply-on-behalf + Manager Approve/Decline) and /crewing/attendance (tap-to-cycle calendar + Save). Leave + Attendance added to the flag-gated nav (Manager + Site staff). Tests & docs - Integration: leave-attendance.test.ts (7) — apply/decide, clash auto-raise (and no-raise when cover remains), MPO/Manager attendance lockout, permission gating. type-check clean; full unit (240) + integration (182) green. - CLAUDE.md updated with the Phase 4b surface. Deferred: the 6-month leave-planner timeline (lightweight list for now); hours/ overtime attendance (A7). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
import type { Prisma } from "@prisma/client";
|
|
|
|
// Leave-clash detection (Crewing-Implementation-Spec §5.3, R6). Required strength
|
|
// is treated as 1: approving a leave is a clash when it would leave the vessel
|
|
// with ZERO active same-rank cover over the leave window — i.e. every other
|
|
// not-signed-off crew member of that rank on the vessel is either absent or on an
|
|
// approved leave that overlaps the window. A clash auto-raises a LEAVE requisition.
|
|
|
|
interface ClashInput {
|
|
assignmentId: string;
|
|
rankId: string;
|
|
vesselId: string | null;
|
|
fromDate: Date;
|
|
toDate: Date;
|
|
}
|
|
|
|
export async function leaveLeavesNoCover(
|
|
tx: Prisma.TransactionClient,
|
|
{ assignmentId, rankId, vesselId, fromDate, toDate }: ClashInput
|
|
): Promise<boolean> {
|
|
// No vessel cost axis → no rank-cover check.
|
|
if (!vesselId) return false;
|
|
|
|
const others = await tx.crewAssignment.findMany({
|
|
where: { rankId, vesselId, status: { not: "SIGNED_OFF" }, id: { not: assignmentId } },
|
|
select: { id: true },
|
|
});
|
|
// This crew member was the only same-rank cover on the vessel.
|
|
if (others.length === 0) return true;
|
|
|
|
const otherIds = others.map((o) => o.id);
|
|
const overlapping = await tx.leaveRequest.findMany({
|
|
where: {
|
|
assignmentId: { in: otherIds },
|
|
status: "APPROVED",
|
|
fromDate: { lte: toDate },
|
|
toDate: { gte: fromDate },
|
|
},
|
|
select: { assignmentId: true },
|
|
});
|
|
const out = new Set(overlapping.map((l) => l.assignmentId));
|
|
const remainingCover = otherIds.filter((id) => !out.has(id)).length;
|
|
return remainingCover === 0;
|
|
}
|