- Migration 0002_craftvia_domain: 27 Fachtabellen inkl. RLS (enable_tenant_rls) - TENANT_MODELS (db.ts, backup/topology.ts) um alle Fachmodelle ergänzt - moduleGuard liefert DB-autoritative Rechte; ServiceCtx für Domänen-Services - Verträge: Statusmaschine, Events, Nummernkreise, Sichtbarkeits-Scopes, Job-Queues + Worker, KI-Provider-Interfaces, Sync-Envelope - docs/craftvia/ARCHITEKTUR.md mit Lanes, Ownership und DoD Gate: tsc, lint, build, 22/22 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
// 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<WorkOrderStatus, readonly WorkOrderStatus[]> = {
|
|
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/<locale>/workOrders.json → statusGroup.<key>. */
|
|
export const STATUS_GROUP: Record<WorkOrderStatus, StatusGroup> = {
|
|
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<StatusGroup, "neutral" | "info" | "accent" | "warning" | "success" | "danger"> = {
|
|
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 };
|