Second slice of the Crewing module per wiki Crewing-Implementation-Spec §12 (build order item 2). Everything stays behind NEXT_PUBLIC_CREWING_ENABLED; production is unchanged. Schema is added incrementally — this lands the requisition lifecycle layer. What's in - Schema: Requisition (OPEN→SHORTLISTING→PROPOSING→INTERVIEWING→SELECTED→FILLED, →CANCELLED), ReliefRequest, CrewAction (the POAction mirror) + their enums. Migration crewing_requisitions. - State machine: lib/requisition-state-machine.ts mirrors po-state-machine (selection Manager-only; orthogonal cancel from OPEN/SHORTLISTING by cancel_requisition holders, §6). Codes REQ-9000… via lib/requisition-number.ts. - Actions: raise/cancel/transition + requestReliefCover/convertReliefToRequisition, each guarding flag+permission+state, writing a CrewAction and notifying. Shared autoRaiseRequisition() (lib/requisition-service.ts) is the backfill entry point for sign-off / leave-clash (later phases). - Notifier: notifyCrew() PO-independent path + CrewNotificationEvent. - Screens: /crewing/requisitions (list + Raise modal + relief convert) and /crewing/requisitions/[id] (detail). Requisitions added to the flag-gated Crewing sidebar (Manager + MPO, §7). Tests & docs - Unit: requisition-state-machine.test.ts (11). - Integration: requisitions.test.ts (15) — raise/cancel/transition, relief request + convert, auto-raise, permission gating. - CLAUDE.md "Crewing" section updated with the Phase 2 surface. Deferred: sign-off/experience (Epic K, §12 item 2) depends on the crew/assignment models from Phase 3/4; autoRaiseRequisition() is ready for it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import { hasPermission } from "@/lib/permissions";
|
|
import { CREWING_ENABLED } from "@/lib/feature-flags";
|
|
import { redirect, notFound } from "next/navigation";
|
|
import { RequisitionsManager } from "./requisitions-manager";
|
|
import type { Metadata } from "next";
|
|
|
|
export const metadata: Metadata = { title: "Requisitions" };
|
|
|
|
export default async function RequisitionsPage() {
|
|
// Dark unless the crewing module is switched on.
|
|
if (!CREWING_ENABLED) notFound();
|
|
|
|
const session = await auth();
|
|
if (!session?.user) redirect("/login");
|
|
if (!hasPermission(session.user.role, "view_requisitions")) redirect("/dashboard");
|
|
const role = session.user.role;
|
|
|
|
const [requisitions, reliefRequests, ranks, vessels, sites] = await Promise.all([
|
|
db.requisition.findMany({
|
|
orderBy: { createdAt: "desc" },
|
|
include: {
|
|
rank: { select: { name: true } },
|
|
vessel: { select: { name: true } },
|
|
site: { select: { name: true } },
|
|
raisedBy: { select: { name: true } },
|
|
},
|
|
}),
|
|
db.reliefRequest.findMany({
|
|
where: { status: "OPEN" },
|
|
orderBy: { createdAt: "desc" },
|
|
include: {
|
|
rank: { select: { name: true } },
|
|
vessel: { select: { name: true } },
|
|
site: { select: { name: true } },
|
|
requestedBy: { select: { name: true } },
|
|
},
|
|
}),
|
|
db.rank.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, code: true, name: true } }),
|
|
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
|
db.site.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
|
]);
|
|
|
|
// Flatten to plain props — no Date/Decimal crosses the server→client boundary.
|
|
const rows = requisitions.map((r) => ({
|
|
id: r.id,
|
|
code: r.code,
|
|
status: r.status,
|
|
reason: r.reason,
|
|
autoRaised: r.autoRaised,
|
|
rankName: r.rank.name,
|
|
location: r.vessel?.name ?? r.site?.name ?? "—",
|
|
raisedBy: r.raisedBy?.name ?? "System",
|
|
createdAt: r.createdAt.toISOString(),
|
|
}));
|
|
|
|
const relief = reliefRequests.map((r) => ({
|
|
id: r.id,
|
|
rankName: r.rank.name,
|
|
location: r.vessel?.name ?? r.site?.name ?? "—",
|
|
note: r.note,
|
|
requestedBy: r.requestedBy.name,
|
|
createdAt: r.createdAt.toISOString(),
|
|
}));
|
|
|
|
return (
|
|
<RequisitionsManager
|
|
requisitions={rows}
|
|
reliefRequests={relief}
|
|
ranks={ranks}
|
|
vessels={vessels}
|
|
sites={sites}
|
|
canRaise={hasPermission(role, "raise_requisition")}
|
|
canConvert={hasPermission(role, "convert_relief_to_requisition")}
|
|
/>
|
|
);
|
|
}
|