diff --git a/src/domain/work-orders/mappers/work-order-table-row-mapper.ts b/src/domain/work-orders/mappers/work-order-table-row-mapper.ts index 3b9fc66b..ab226518 100644 --- a/src/domain/work-orders/mappers/work-order-table-row-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-table-row-mapper.ts @@ -177,23 +177,25 @@ function readFlagColor(record: Record): string | null { function readUpliftSummary(item: Record): WorkOrderUpliftSummary | undefined { const nested = asRecord(item.upliftSummary ?? item.UpliftSummary); - const source = Object.keys(nested).length > 0 ? nested : item; + const hasNested = Object.keys(nested).length > 0; + const source = hasNested ? nested : item; - const pendingCount = - readNumber(source, "pendingCount", "PendingCount") ?? - readNumber(item, "pendingUpliftCount", "PendingUpliftCount") ?? - 0; + // Board/search/detail contract (SH-188): pendingUpliftCount is authoritative when present. + const boardCount = readNumber(item, "pendingUpliftCount", "PendingUpliftCount"); + const nestedCount = readNumber(source, "pendingCount", "PendingCount"); + const pendingCount = nestedCount ?? boardCount ?? 0; const hasUplift = readBool(source, "hasUplift", "HasUplift") || readBool(item, "hasUplift", "HasUplift") || pendingCount > 0; - if (!hasUplift && pendingCount === 0) { + // No nested summary and no board count → omit (legacy payloads without the contract field). + if (!hasNested && boardCount == null && !hasUplift) { return undefined; } const summary: WorkOrderUpliftSummary = { - hasUplift, + hasUplift: hasUplift || pendingCount > 0, pendingCount, }; const primaryStatus = readString(source, "primaryStatus", "PrimaryStatus", "status", "Status"); @@ -207,11 +209,15 @@ function readHasPendingUplift( item: Record, upliftSummary: WorkOrderUpliftSummary | undefined, ): boolean | undefined { - if (readBool(item, "hasPendingUplift", "HasPendingUplift")) { - return true; + if ("hasPendingUplift" in item || "HasPendingUplift" in item) { + return Boolean(item.hasPendingUplift ?? item.HasPendingUplift); } - if ((upliftSummary?.pendingCount ?? 0) > 0) { - return true; + const boardCount = readNumber(item, "pendingUpliftCount", "PendingUpliftCount"); + if (boardCount != null) { + return boardCount > 0; + } + if (upliftSummary != null) { + return upliftSummary.pendingCount > 0; } return undefined; } diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts index 021fe1c0..fec033b8 100644 --- a/src/domain/work-orders/types/work-order-board.ts +++ b/src/domain/work-orders/types/work-order-board.ts @@ -159,9 +159,16 @@ export interface WorkOrderBoardRowApi { serviceNotes?: string | null; extraServices?: string[] | null; docStatus?: number | null; + /** Work completed date from board/detail contract. */ + completedDate?: string | null; primaryDispatchId?: number | null; rowVersion?: string | null; dispatchRowVersion?: string | null; + /** + * Authoritative pending-uplift count across all dispatches for this WO (SH-188 / BE board contract). + * Drives the Completed closability gate. + */ + pendingUpliftCount?: number; } export interface WorkOrderBoardCountsApi { diff --git a/src/domain/work-orders/utils/wo-closability.ts b/src/domain/work-orders/utils/wo-closability.ts index 7259cbf5..388d0e02 100644 --- a/src/domain/work-orders/utils/wo-closability.ts +++ b/src/domain/work-orders/utils/wo-closability.ts @@ -25,9 +25,9 @@ export type WoClosabilityInput = { export const PENDING_UPLIFT_GAP = "Pending uplift approval"; -/** Design copy when Completed is blocked solely by a pending uplift. */ +/** SH-188 tooltip when Completed is blocked solely by a pending uplift. */ export const UPLIFT_PENDING_COMPLETED_TITLE = - "This work order cannot be completed while an uplift is pending approval."; + "This work order has an uplift pending approval and cannot be completed."; /** Real (non-placeholder) WO number — non-empty and not an SH-prefixed draft id. */ export function isRealWoNumber(woNumber: string): boolean { @@ -83,7 +83,8 @@ export function getClosabilityGaps(row: WoClosabilityInput): string[] { gaps.push("Service"); } - // Only an actually pending uplift blocks completion; missing uplift fields mean no pending evidence. + // Authoritative pending uplift (board pendingUpliftCount / hasPendingUplift) blocks completion. + // Missing uplift fields mean no pending evidence until the board contract supplies the count. if (hasPendingUplift(row)) { gaps.push(PENDING_UPLIFT_GAP); } diff --git a/src/test/app/(protected)/workorders/status-cell-pending-uplift.test.tsx b/src/test/app/(protected)/workorders/status-cell-pending-uplift.test.tsx index 998b2680..5d610390 100644 --- a/src/test/app/(protected)/workorders/status-cell-pending-uplift.test.tsx +++ b/src/test/app/(protected)/workorders/status-cell-pending-uplift.test.tsx @@ -71,13 +71,38 @@ describe("StatusCell pending uplift closability", () => { expect(completed).toBeDisabled(); expect(completed).toHaveAttribute( "title", - "This work order cannot be completed while an uplift is pending approval.", + "This work order has an uplift pending approval and cannot be completed.", ); fireEvent.click(completed); expect(onChangeStatus).not.toHaveBeenCalled(); }); + it("disables Completed with tooltip when board pendingUpliftCount maps to pending", () => { + const onChangeStatus = vi.fn(); + + renderWithProviders( + , + { withAuth: false }, + ); + + fireEvent.click(screen.getByRole("button", { name: /in progress/i })); + + const completed = screen.getByRole("button", { name: /completed/i }); + expect(completed).toBeDisabled(); + expect(completed).toHaveAttribute( + "title", + "This work order has an uplift pending approval and cannot be completed.", + ); + }); + it("allows Completed when no pending uplift", () => { const onChangeStatus = vi.fn(); diff --git a/src/test/domain/work-orders/mappers/work-order-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-mapper.test.ts index 1b3baa4e..2a33a51c 100644 --- a/src/test/domain/work-orders/mappers/work-order-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-mapper.test.ts @@ -154,6 +154,28 @@ describe("mapWorkOrderTableRow (board DTO)", () => { expect(row.hasPendingUplift).toBe(true); }); + it("maps board pendingUpliftCount into hasPendingUplift (SH-188 contract)", () => { + const pending = mapWorkOrderTableRow({ + id: 305, + pendingUpliftCount: 1, + }); + expect(pending.hasPendingUplift).toBe(true); + expect(pending.upliftSummary).toEqual({ hasUplift: true, pendingCount: 1 }); + + const clear = mapWorkOrderTableRow({ + id: 306, + PendingUpliftCount: 0, + }); + expect(clear.hasPendingUplift).toBe(false); + expect(clear.upliftSummary).toEqual({ hasUplift: false, pendingCount: 0 }); + }); + + it("leaves hasPendingUplift undefined when board count is absent", () => { + const row = mapWorkOrderTableRow({ id: 307 }); + expect(row.hasPendingUplift).toBeUndefined(); + expect(row.upliftSummary).toBeUndefined(); + }); + it("maps hasPendingUplift from top-level API flag", () => { const row = mapWorkOrderTableRow({ id: 304,