diff --git a/src/domain/work-orders/utils/wo-closability.ts b/src/domain/work-orders/utils/wo-closability.ts index d9db0baf..4c563b8a 100644 --- a/src/domain/work-orders/utils/wo-closability.ts +++ b/src/domain/work-orders/utils/wo-closability.ts @@ -1,6 +1,10 @@ import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail"; import type { CompletionDocStatus } from "@/domain/work-orders/types/work-order-table-row"; -import type { WOType, WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; +import { + WO_TYPES, + type WOType, + type WorkOrderWizardDraft, +} from "@/domain/work-orders/types/work-order-wizard"; /** Fields required to evaluate whether a WO may be marked Completed. */ export type WoClosabilityInput = { @@ -106,9 +110,15 @@ export function wizardDraftToClosabilityInput(draft: WorkOrderWizardDraft): WoCl }; } +function detailSourceToWoType(source: string): WOType | "" { + return (WO_TYPES as readonly string[]).includes(source) ? (source as WOType) : ""; +} + /** - * Map standalone detail DTO onto closability. Fields the detail route does not - * expose (service, docStatus, media, type) stay empty so Completed fails closed. + * Map standalone detail DTO onto closability. `problem` is the same service + * field the board maps into `pm`; `source` carries wizard WO type when set. + * docStatus/media stay empty (detail route does not expose them) so those + * type-specific gates still fail closed. */ export function detailToClosabilityInput(wo: WorkOrderDetail): WoClosabilityInput { const vendorName = wo.dispatches.find((dispatch) => @@ -121,9 +131,9 @@ export function detailToClosabilityInput(wo: WorkOrderDetail): WoClosabilityInpu tech: "", completedDate: wo.completedDate, woNumber: wo.workOrderNumber || wo.internalWONumber, - pm: "", + pm: wo.problem, docStatus: "", mediaCount: 0, - type: "", + type: detailSourceToWoType(wo.source), }; } diff --git a/src/test/domain/work-orders/utils/wo-closability.test.ts b/src/test/domain/work-orders/utils/wo-closability.test.ts index 69f9ea2b..0e9e6353 100644 --- a/src/test/domain/work-orders/utils/wo-closability.test.ts +++ b/src/test/domain/work-orders/utils/wo-closability.test.ts @@ -161,12 +161,14 @@ describe("wizardDraftToClosabilityInput", () => { }); describe("detailToClosabilityInput", () => { - it("maps vendorName to company and fails closed on missing service/media fields", () => { + it("maps problem/source and vendorName so Done is not permanently blocked", () => { const detail = { assignedTo: "Alice", completedDate: "2026-07-15", workOrderNumber: "20260623001", internalWONumber: "SH-1", + problem: "HVAC PM", + source: "Add-On", dispatches: [{ vendorName: "ACME Tech" }], } as WorkOrderDetail; @@ -176,9 +178,28 @@ describe("detailToClosabilityInput", () => { expect(input.tech).toBe(""); expect(input.completedDate).toBe("2026-07-15"); expect(input.woNumber).toBe("20260623001"); - expect(input.pm).toBe(""); - expect(getClosabilityGaps(input)).toContain("Service"); + expect(input.pm).toBe("HVAC PM"); + expect(input.type).toBe("Add-On"); + expect(getClosabilityGaps(input)).not.toContain("Service"); expect(getClosabilityGaps(input)).not.toContain("Company"); expect(getClosabilityGaps(input)).not.toContain("Technician"); + expect(getClosabilityGaps(input)).toEqual([]); + }); + + it("fails closed on missing service when problem is empty", () => { + const detail = { + assignedTo: "Alice", + completedDate: "2026-07-15", + workOrderNumber: "20260623001", + internalWONumber: "SH-1", + problem: "", + source: "Reactive", + dispatches: [{ vendorName: "ACME Tech" }], + } as WorkOrderDetail; + + const input = detailToClosabilityInput(detail); + expect(input.pm).toBe(""); + expect(input.type).toBe("Reactive"); + expect(getClosabilityGaps(input)).toContain("Service"); }); });