Architektur: Craftvia-Domänenmodell, Verträge und Team-Schnitte
- 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>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user