pelagia-portal/App/app/(portal)/crewing/requisitions/requisition-ui.ts
Hardik 0b2ed9ac07
All checks were successful
PR checks / checks (pull_request) Successful in 37s
PR checks / integration (pull_request) Successful in 28s
feat(crewing): Phase 2 — requisitions + relief requests (flagged)
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>
2026-06-22 18:22:59 +05:30

52 lines
1.4 KiB
TypeScript

import type { RequisitionStatus, RequisitionReason } from "@prisma/client";
import type { BadgeProps } from "@/components/ui/badge";
type Variant = NonNullable<BadgeProps["variant"]>;
// Status → badge variant (Crewing-Implementation-Spec §8.2).
export const STATUS_VARIANT: Record<RequisitionStatus, Variant> = {
OPEN: "outline",
SHORTLISTING: "default",
PROPOSING: "default",
INTERVIEWING: "warning",
SELECTED: "default",
FILLED: "success",
CANCELLED: "danger",
};
export const STATUS_LABEL: Record<RequisitionStatus, string> = {
OPEN: "Open",
SHORTLISTING: "Shortlisting",
PROPOSING: "Proposing",
INTERVIEWING: "Interviewing",
SELECTED: "Selected",
FILLED: "Filled",
CANCELLED: "Cancelled",
};
export const REASON_LABEL: Record<RequisitionReason, string> = {
NEW_VACANCY: "New vacancy",
REPLACEMENT: "Replacement",
LEAVE: "Leave cover",
SIGN_OFF: "Sign-off",
END_OF_CONTRACT: "End of contract",
OTHER: "Other",
};
export const REASON_OPTIONS: RequisitionReason[] = [
"NEW_VACANCY",
"REPLACEMENT",
"LEAVE",
"SIGN_OFF",
"END_OF_CONTRACT",
"OTHER",
];
// Compact "age" label (e.g. "3d", "5h", "12m") relative to now.
export function ageLabel(iso: string): string {
const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000);
if (mins < 60) return `${Math.max(mins, 0)}m`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h`;
return `${Math.floor(hrs / 24)}d`;
}