diff --git a/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx index 67d826ee..ddcdbc5e 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/status-cell.tsx @@ -25,6 +25,7 @@ function rowClosabilityGaps(wo: WorkOrderTableRow) { return getClosabilityGaps({ dispatcherId: wo.dispatcherId, dispatcherName: wo.dispatcherName, + company: wo.company, tech: wo.tech, completedDate: wo.completedDate ?? "", woNumber: wo.woNumber, diff --git a/src/domain/work-orders/utils/wo-closability.ts b/src/domain/work-orders/utils/wo-closability.ts index 5f1c67c6..606f9369 100644 --- a/src/domain/work-orders/utils/wo-closability.ts +++ b/src/domain/work-orders/utils/wo-closability.ts @@ -8,7 +8,9 @@ import type { WOType, WorkOrderWizardDraft } from "@/domain/work-orders/types/wo export type WoClosabilityInput = { dispatcherId: string; dispatcherName: string; - /** Vendor / technician assignment on the board row. */ + /** Vendor/company assignment on the board row. */ + company: string; + /** Technician name is informational and does not satisfy the vendor requirement. */ tech: string; /** Work completed date (API CompletedDate). Empty until set. */ completedDate: string; @@ -22,6 +24,7 @@ export type WoClosabilityInput = { }; export const PENDING_UPLIFT_GAP = "Pending uplift approval"; +export const UPLIFT_STATUS_UNAVAILABLE_GAP = "Uplift status unavailable"; /** Design copy when Completed is blocked solely by a pending uplift. */ export const UPLIFT_PENDING_COMPLETED_TITLE = @@ -45,6 +48,12 @@ export function hasPendingUplift( return (row.upliftSummary?.pendingCount ?? 0) > 0; } +function hasKnownUpliftStatus( + row: Pick, +): boolean { + return typeof row.hasPendingUplift === "boolean" || row.upliftSummary != null; +} + function hasDispatcher(row: Pick): boolean { return Boolean(row.dispatcherId.trim() || row.dispatcherName.trim()); } @@ -68,8 +77,8 @@ export function getClosabilityGaps(row: WoClosabilityInput): string[] { if (!hasDispatcher(row)) { gaps.push("Dispatcher"); } - if (!row.tech.trim()) { - gaps.push("Technician"); + if (!row.company.trim()) { + gaps.push("Company / vendor"); } if (!row.completedDate.trim()) { gaps.push("Completed Date"); @@ -81,7 +90,9 @@ export function getClosabilityGaps(row: WoClosabilityInput): string[] { gaps.push("Service"); } - if (hasPendingUplift(row)) { + if (!hasKnownUpliftStatus(row)) { + gaps.push(UPLIFT_STATUS_UNAVAILABLE_GAP); + } else if (hasPendingUplift(row)) { gaps.push(PENDING_UPLIFT_GAP); } @@ -126,6 +137,7 @@ export function wizardDraftToClosabilityInput(draft: WorkOrderWizardDraft): WoCl return { dispatcherId: draft.assigneeId, dispatcherName: draft.assigneeName, + company: draft.vendorName, tech: draft.vendorName, completedDate: "", woNumber: draft.woNumber, 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 c0b24abb..079ac992 100644 --- a/src/test/domain/work-orders/utils/wo-closability.test.ts +++ b/src/test/domain/work-orders/utils/wo-closability.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import type { WoClosabilityInput } from "@/domain/work-orders/utils/wo-closability"; import { PENDING_UPLIFT_GAP, + UPLIFT_STATUS_UNAVAILABLE_GAP, UPLIFT_PENDING_COMPLETED_TITLE, canGenerateCompDoc, formatClosabilityMissingTitle, @@ -14,6 +15,7 @@ function makeInput(overrides: Partial = {}): WoClosabilityIn return { dispatcherId: "u1", dispatcherName: "Alex", + company: "Acme HVAC", tech: "Sam", completedDate: "2026-07-01", woNumber: "1001", @@ -21,8 +23,7 @@ function makeInput(overrides: Partial = {}): WoClosabilityIn docStatus: "Yes", mediaCount: 1, type: "PM", - upliftSummary: undefined, - hasPendingUplift: undefined, + hasPendingUplift: false, ...overrides, }; } @@ -47,7 +48,7 @@ describe("hasPendingUplift", () => { ).toBe(true); }); - it("is false when no uplift signals are present", () => { + it("is false when uplift is explicitly not pending", () => { expect(hasPendingUplift(makeInput())).toBe(false); expect( hasPendingUplift(makeInput({ upliftSummary: { hasUplift: false, pendingCount: 0 } })), @@ -60,12 +61,21 @@ describe("getClosabilityGaps", () => { expect(getClosabilityGaps(makeInput())).toEqual([]); }); + it("accepts a company without a technician", () => { + expect(getClosabilityGaps(makeInput({ tech: "" }))).toEqual([]); + }); + + it("does not let a technician satisfy a missing company requirement", () => { + expect(getClosabilityGaps(makeInput({ company: "" }))).toEqual(["Company / vendor"]); + }); + it("lists field gaps before uplift and document gaps", () => { expect( getClosabilityGaps( makeInput({ dispatcherId: "", dispatcherName: "", + company: " ", tech: " ", completedDate: "", woNumber: "SH-1", @@ -77,7 +87,7 @@ describe("getClosabilityGaps", () => { ), ).toEqual([ "Dispatcher", - "Technician", + "Company / vendor", "Completed Date", "WO number", "Service", @@ -96,6 +106,12 @@ describe("getClosabilityGaps", () => { expect(getClosabilityGaps(makeInput({ hasPendingUplift: true }))).toEqual([PENDING_UPLIFT_GAP]); }); + it("blocks Completed when the backend does not provide uplift status", () => { + expect( + getClosabilityGaps(makeInput({ hasPendingUplift: undefined, upliftSummary: undefined })), + ).toEqual([UPLIFT_STATUS_UNAVAILABLE_GAP]); + }); + it("requires a photo for Emergency / Reactive", () => { expect( getClosabilityGaps(makeInput({ type: "Emergency", mediaCount: 0, docStatus: "No" })),