/** * Requisition service helpers shared by the crewing server actions and by the * system auto-raise paths (sign-off / end-of-contract / leave-clash backfill, * Phase 3/4). Kept out of the "use server" action module so non-action code can * import the auto-raise helper. See Crewing-Implementation-Spec §5.2/§5.3 (R6). */ import { db } from "@/lib/db"; import { generateRequisitionCode } from "@/lib/requisition-number"; import { notifyCrew } from "@/lib/notifier"; import type { Prisma, RequisitionReason, User } from "@prisma/client"; type Tx = Prisma.TransactionClient; export interface NewRequisitionInput { rankId: string; vesselId?: string | null; siteId?: string | null; reason: RequisitionReason; neededBy?: Date | null; notes?: string | null; raisedById?: string | null; // null = system-raised autoRaised?: boolean; } type RequisitionWithRefs = Prisma.RequisitionGetPayload<{ include: { rank: true; vessel: true; site: true }; }>; /** * Core requisition creator — run inside a transaction. Generates the code and * writes the REQUISITION_RAISED CrewAction. Callers own notification + any * relief-request linking afterwards. */ export async function createRequisitionTx( tx: Tx, input: NewRequisitionInput ): Promise { const code = await generateRequisitionCode(tx); return tx.requisition.create({ data: { code, reason: input.reason, autoRaised: input.autoRaised ?? false, neededBy: input.neededBy ?? null, notes: input.notes ?? null, rankId: input.rankId, vesselId: input.vesselId ?? null, siteId: input.siteId ?? null, raisedById: input.raisedById ?? null, actions: { create: { actionType: "REQUISITION_RAISED", actorId: input.raisedById ?? null, metadata: input.autoRaised ? { auto: true, reason: input.reason } : undefined, }, }, }, include: { rank: true, vessel: true, site: true }, }); } /** Human label for a requisition's cost axis (vessel preferred, else site). */ export function requisitionLocationLabel(r: { vessel: { name: string } | null; site: { name: string } | null; }): string { return r.vessel?.name ?? r.site?.name ?? "—"; } /** Office recipients (MPO sources recruitment; Manager oversees). */ export function getOfficeRecipients(): Promise { return db.user.findMany({ where: { isActive: true, role: { in: ["MANNING", "MANAGER", "SUPERUSER"] } }, }); } /** MPO recipients — for "requisition raised → MPO" (spec §11). */ export function getMpoRecipients(): Promise { return db.user.findMany({ where: { isActive: true, role: { in: ["MANNING", "SUPERUSER"] } }, }); } /** * System auto-raise: an OPEN requisition with no human actor (autoRaised), then * notifies the office. Sign-off, end-of-contract and the leave-clash detector * (later phases) all funnel through here. See spec §5.2/§5.3 (R6). */ export async function autoRaiseRequisition( input: Omit ): Promise { const requisition = await db.$transaction((tx) => createRequisitionTx(tx, { ...input, raisedById: null, autoRaised: true }) ); const recipients = await getOfficeRecipients(); const loc = requisitionLocationLabel(requisition); await notifyCrew({ event: "REQUISITION_RAISED", recipients, subject: `Requisition ${requisition.code} auto-raised`, body: `A ${requisition.rank.name} vacancy on ${loc} was auto-raised (${requisition.code}) — reason: ${requisition.reason}.`, link: `/crewing/requisitions/${requisition.id}`, }); return requisition; }