diff --git a/src/domain/work-orders/api/work-orders-api.ts b/src/domain/work-orders/api/work-orders-api.ts index 03778ad3..fb02637a 100644 --- a/src/domain/work-orders/api/work-orders-api.ts +++ b/src/domain/work-orders/api/work-orders-api.ts @@ -370,7 +370,7 @@ export const workOrdersApi = { payload, ), - /** SH-379: manual POC override — same row/error contract as the board patch. */ + /** Manual POC override — same row/error contract as the board patch. */ updatePoc: async ( id: string | number, payload: WorkOrderPocPatchPayload, diff --git a/src/domain/work-orders/mappers/work-order-board-patch-mapper.ts b/src/domain/work-orders/mappers/work-order-board-patch-mapper.ts index a24d011e..65a5dc02 100644 --- a/src/domain/work-orders/mappers/work-order-board-patch-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-board-patch-mapper.ts @@ -50,12 +50,12 @@ export function isLocalOnlyTablePatch(patch: WorkOrderTablePatch): boolean { return keys.length > 0 && keys.every((key) => LOCAL_ONLY_KEYS.has(key)); } -/** SH-379: composite manual POC override op routed to PATCH …/poc. */ +/** Composite manual POC override op routed to PATCH …/poc. */ export type BoardPocOp = { kind: "poc"; - pocName: string; - pocPhone: string; - pocNotes: string; + pocName?: string; + pocPhone?: string; + pocNotes?: string; }; export type BoardOp = BoardFieldOp | BoardPocOp; @@ -70,9 +70,9 @@ function expandPocOp(patch: WorkOrderTablePatch): BoardPocOp[] { return [ { kind: "poc", - pocName: patch.pocName ?? "", - pocPhone: patch.pocPhone ?? "", - pocNotes: patch.pocNotes ?? "", + pocName: patch.pocName, + pocPhone: patch.pocPhone, + pocNotes: patch.pocNotes, }, ]; } diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts index 3bd6f067..3b06e4fb 100644 --- a/src/domain/work-orders/types/work-order-board.ts +++ b/src/domain/work-orders/types/work-order-board.ts @@ -134,7 +134,7 @@ export interface WorkOrderBoardPatchPayload { primaryDispatchId?: number | null; } -/** Payload for PATCH /workorders/{id}/poc (SH-379 manual POC override). */ +/** Payload for PATCH /workorders/{id}/poc (manual POC override). */ export interface WorkOrderPocPatchPayload { pocName: string; pocPhone: string; diff --git a/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts b/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts index 60b200c4..f7684958 100644 --- a/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts +++ b/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts @@ -54,19 +54,23 @@ export type WorkOrderTablePatchHandlers = { type PatchResult = { kind: "local"; patch: WorkOrderTablePatch } | { kind: "board"; row: WorkOrderTableRow }; +function isPocOp(op: BoardOp): op is Extract { + return "kind" in op && op.kind === "poc"; +} + async function applyBoardOp( id: string | number, op: BoardOp, row: WorkOrderTableRow, ): Promise { - if ("pocName" in op) { + if (isPocOp(op)) { if (!row.rowVersion) { throw new Error("workOrderVersion is required for board patch"); } return workOrdersApi.updatePoc(id, { - pocName: op.pocName, - pocPhone: op.pocPhone, - pocNotes: op.pocNotes, + pocName: op.pocName ?? row.pocName, + pocPhone: op.pocPhone ?? row.pocPhone, + pocNotes: op.pocNotes ?? row.pocNotes, workOrderVersion: row.rowVersion, }); } diff --git a/src/domain/work-orders/utils/slide-over-draft.ts b/src/domain/work-orders/utils/slide-over-draft.ts index 5a8bf237..7135bd01 100644 --- a/src/domain/work-orders/utils/slide-over-draft.ts +++ b/src/domain/work-orders/utils/slide-over-draft.ts @@ -7,7 +7,7 @@ import type { /** * Fields compared for dirty state and included in Save patch. * Only keys that map to board PATCH (or required companions like dispatcherName). - * Primary POC scalars persist via the dedicated POC endpoint (SH-379); + * Primary POC scalars persist via the dedicated POC endpoint; * tech phone is not board-editable — exclude so Save cannot no-op. * additionalContacts is board-editable (SH-218 PATCH field). */ diff --git a/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts index e7ebd9b0..ab852413 100644 --- a/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts @@ -35,7 +35,7 @@ describe("isLocalOnlyTablePatch", () => { expect(isLocalOnlyTablePatch({ scheduleEnd: "2026-06-12", schedMode: "range" })).toBe(true); }); - it("does not treat POC edits as local-only (SH-379)", () => { + it("does not treat POC edits as local-only", () => { expect(isLocalOnlyTablePatch({ pocName: "Pat", pocPhone: "111", pocNotes: "call" })).toBe( false, ); @@ -202,7 +202,7 @@ describe("expandTablePatchToBoardFieldOps", () => { ]); }); - it("emits one composite POC op before scalar ops (SH-379)", () => { + it("emits one composite POC op before scalar ops", () => { expect( expandTablePatchToBoardFieldOps({ pocName: "Pat", @@ -213,7 +213,7 @@ describe("expandTablePatchToBoardFieldOps", () => { { kind: "poc", pocName: "Pat", - pocPhone: "", + pocPhone: undefined, pocNotes: "call first", }, { field: "docStatus", value: "Yes" }, diff --git a/src/test/domain/work-orders/use-cases/use-work-order-table-mutations.test.tsx b/src/test/domain/work-orders/use-cases/use-work-order-table-mutations.test.tsx index 97f364b5..a2a6798c 100644 --- a/src/test/domain/work-orders/use-cases/use-work-order-table-mutations.test.tsx +++ b/src/test/domain/work-orders/use-cases/use-work-order-table-mutations.test.tsx @@ -9,12 +9,14 @@ import type { const getById = vi.fn(); const update = vi.fn(); +const updatePoc = vi.fn(); const patchBoardField = vi.fn(); vi.mock("@/domain/work-orders/api/work-orders-api", () => ({ workOrdersApi: { getById: (...args: unknown[]) => getById(...args), update: (...args: unknown[]) => update(...args), + updatePoc: (...args: unknown[]) => updatePoc(...args), patchBoardField: (...args: unknown[]) => patchBoardField(...args), }, })); @@ -121,6 +123,7 @@ describe("useWorkOrderTableMutations completion-doc persistence", () => { beforeEach(() => { getById.mockReset(); update.mockReset(); + updatePoc.mockReset(); patchBoardField.mockReset(); serverState = { ...BASE_SERVER }; rowState = { ...BASE_ROW }; @@ -829,6 +832,40 @@ describe("useWorkOrderTableMutations type and severity", () => { }); }); +describe("useWorkOrderTableMutations POC persistence", () => { + it("fills omitted POC fields from the current row before the full replacement", async () => { + const currentRow = { + ...BASE_ROW, + pocName: "Alex", + pocPhone: "555-0000", + pocNotes: "Gate", + }; + updatePoc.mockResolvedValue({ ...currentRow, pocName: "Jordan", rowVersion: "wo-v2" }); + + const { result } = renderHook( + () => + useWorkOrderTableMutations({ + onPatch: vi.fn(), + clearPatch: () => undefined, + getRow: () => currentRow, + }), + { wrapper: makeWrapper() }, + ); + + act(() => { + result.current.patchField(42, { pocName: "Jordan" }); + }); + + await waitFor(() => expect(updatePoc).toHaveBeenCalled()); + expect(updatePoc).toHaveBeenCalledWith(42, { + pocName: "Jordan", + pocPhone: "555-0000", + pocNotes: "Gate", + workOrderVersion: "AAAAAAA=", + }); + }); +}); + describe("useWorkOrderTableMutations audit cache refresh", () => { it("invalidates work-order queries after a successful status transition", async () => { const client = new QueryClient({ diff --git a/src/test/domain/work-orders/utils/slide-over-draft.test.ts b/src/test/domain/work-orders/utils/slide-over-draft.test.ts index 322391af..2bc85efc 100644 --- a/src/test/domain/work-orders/utils/slide-over-draft.test.ts +++ b/src/test/domain/work-orders/utils/slide-over-draft.test.ts @@ -87,7 +87,7 @@ describe("slide-over-draft", () => { expect(buildSlideOverPatch(vendorDraft, row)).toEqual({ vendorId: "v2" }); }); - it("tracks POC edits for dirty state and patch (SH-379)", () => { + it("tracks POC edits for dirty state and patch", () => { const row = baseRow(); const draft = baseRow({ pocName: "Jordan", pocPhone: "555-0100" }); expect(isSlideOverDraftDirty(draft, row)).toBe(true);