// Work order status machine (client-safe: no server imports). // Server enforcement lives in src/server/services/work-orders/transition.ts, // which MUST call canTransition()/requiredPermission() — never update status directly. export const WORK_ORDER_STATUSES = [ "draft", "review_required", "planned", "assigned", "accepted", "en_route", "in_progress", "paused", "waiting_material", "daily_report_created", "technically_completed", "signature_pending", "in_review", "released_for_billing", "billed", "cancelled", ] as const; export type WorkOrderStatus = (typeof WORK_ORDER_STATUSES)[number]; const T: Record = { draft: ["review_required", "planned", "assigned", "cancelled"], review_required: ["planned", "assigned", "cancelled"], planned: ["assigned", "cancelled"], assigned: ["accepted", "en_route", "in_progress", "planned", "cancelled"], accepted: ["en_route", "in_progress", "assigned", "cancelled"], en_route: ["in_progress", "cancelled"], in_progress: ["paused", "waiting_material", "daily_report_created", "technically_completed", "cancelled"], paused: ["in_progress", "en_route", "cancelled"], waiting_material: ["in_progress", "en_route", "cancelled"], daily_report_created: ["en_route", "in_progress", "cancelled"], technically_completed: ["signature_pending", "in_review", "in_progress", "cancelled"], signature_pending: ["in_review", "cancelled"], in_review: ["released_for_billing", "in_progress", "cancelled"], released_for_billing: ["billed", "in_review"], billed: [], cancelled: [], }; export function allowedTransitions(from: WorkOrderStatus): readonly WorkOrderStatus[] { return T[from]; } export function canTransition(from: WorkOrderStatus, to: WorkOrderStatus): boolean { return T[from].includes(to); } /** Permission required for a transition (server additionally checks work order scope for field roles). */ export function requiredPermission(from: WorkOrderStatus, to: WorkOrderStatus): string { if (to === "cancelled") return "work_order:cancel"; if (to === "released_for_billing" || to === "billed") return "work_order:release_billing"; if (from === "released_for_billing" && to === "in_review") return "work_order:release_billing"; if (from === "in_review" && to === "in_progress") return "report:approve_team"; // or report:approve (checked server-side) if (["draft", "review_required", "planned"].includes(from)) return to === "assigned" ? "work_order:assign" : "work_order:write"; if (from === "assigned" && to === "planned") return "work_order:assign"; return "field:execute"; } /** Statuses in which field users may record time/material/photos/notes. */ export const FIELD_EDITABLE: readonly WorkOrderStatus[] = [ "assigned", "accepted", "en_route", "in_progress", "paused", "waiting_material", "daily_report_created", "technically_completed", "signature_pending", ]; export const OPEN_STATUSES: readonly WorkOrderStatus[] = WORK_ORDER_STATUSES.filter( (s) => !["billed", "cancelled", "released_for_billing"].includes(s), ); /** UI groups per Brandbook §12.3 — label keys live in messages//workOrders.json → statusGroup.. */ export const STATUS_GROUP: Record = { draft: "new", review_required: "new", planned: "planned", assigned: "planned", accepted: "planned", en_route: "en_route", in_progress: "in_progress", paused: "in_progress", waiting_material: "in_progress", daily_report_created: "in_progress", technically_completed: "documentation_incomplete", signature_pending: "documentation_incomplete", in_review: "in_review", released_for_billing: "ready_for_billing", billed: "billed", cancelled: "cancelled", }; export type StatusGroup = | "new" | "planned" | "en_route" | "in_progress" | "documentation_incomplete" | "in_review" | "ready_for_billing" | "billed" | "cancelled"; /** Semantic tone for badges (always combined with text, Brandbook §11.4). */ export const STATUS_GROUP_TONE: Record = { new: "neutral", planned: "info", en_route: "accent", in_progress: "accent", documentation_incomplete: "warning", in_review: "info", ready_for_billing: "success", billed: "success", cancelled: "danger", }; export type CompletionBlocker = | { kind: "checklist_item"; itemId: string; label: string } | { kind: "photo_requirement"; requirementId: string; label: string } | { kind: "running_session"; sessionId: string; userId: string } | { kind: "missing_field"; field: string };