import type { Prisma } from "@prisma/client"; // Leave-clash detection (Crewing-Implementation-Spec §5.3, R6 — Option A). // Approving a leave is a clash when the remaining ACTIVE same-rank cover on the // vessel over the leave window would fall BELOW the rank's required strength for // that vessel (VesselRankRequirement.minStrength, default 1 when unconfigured). // A clash auto-raises a LEAVE requisition. interface ClashInput { assignmentId: string; rankId: string; vesselId: string | null; fromDate: Date; toDate: Date; } export async function leaveCausesClash( tx: Prisma.TransactionClient, { assignmentId, rankId, vesselId, fromDate, toDate }: ClashInput ): Promise { // No vessel cost axis → no rank-cover check. if (!vesselId) return false; const requirement = await tx.vesselRankRequirement.findUnique({ where: { vesselId_rankId: { vesselId, rankId } }, select: { minStrength: true }, }); const requiredStrength = requirement?.minStrength ?? 1; if (requiredStrength <= 0) return false; // Other not-signed-off same-rank crew on the vessel (excludes the one going on leave). const others = await tx.crewAssignment.findMany({ where: { rankId, vesselId, status: { not: "SIGNED_OFF" }, id: { not: assignmentId } }, select: { id: true }, }); let remainingCover = 0; if (others.length > 0) { 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)); remainingCover = otherIds.filter((id) => !out.has(id)).length; } return remainingCover < requiredStrength; }