fix(workorders): preserve partial POC edits

This commit is contained in:
Codex Review Integration 2026-09-18 15:00:01 -03:00
parent 5b0c758a9f
commit 3b1da21abb
8 changed files with 59 additions and 18 deletions

View file

@ -370,7 +370,7 @@ export const workOrdersApi = {
payload, 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 ( updatePoc: async (
id: string | number, id: string | number,
payload: WorkOrderPocPatchPayload, payload: WorkOrderPocPatchPayload,

View file

@ -50,12 +50,12 @@ export function isLocalOnlyTablePatch(patch: WorkOrderTablePatch): boolean {
return keys.length > 0 && keys.every((key) => LOCAL_ONLY_KEYS.has(key)); 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 = { export type BoardPocOp = {
kind: "poc"; kind: "poc";
pocName: string; pocName?: string;
pocPhone: string; pocPhone?: string;
pocNotes: string; pocNotes?: string;
}; };
export type BoardOp = BoardFieldOp | BoardPocOp; export type BoardOp = BoardFieldOp | BoardPocOp;
@ -70,9 +70,9 @@ function expandPocOp(patch: WorkOrderTablePatch): BoardPocOp[] {
return [ return [
{ {
kind: "poc", kind: "poc",
pocName: patch.pocName ?? "", pocName: patch.pocName,
pocPhone: patch.pocPhone ?? "", pocPhone: patch.pocPhone,
pocNotes: patch.pocNotes ?? "", pocNotes: patch.pocNotes,
}, },
]; ];
} }

View file

@ -134,7 +134,7 @@ export interface WorkOrderBoardPatchPayload {
primaryDispatchId?: number | null; 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 { export interface WorkOrderPocPatchPayload {
pocName: string; pocName: string;
pocPhone: string; pocPhone: string;

View file

@ -54,19 +54,23 @@ export type WorkOrderTablePatchHandlers = {
type PatchResult = type PatchResult =
{ kind: "local"; patch: WorkOrderTablePatch } | { kind: "board"; row: WorkOrderTableRow }; { kind: "local"; patch: WorkOrderTablePatch } | { kind: "board"; row: WorkOrderTableRow };
function isPocOp(op: BoardOp): op is Extract<BoardOp, { kind: "poc" }> {
return "kind" in op && op.kind === "poc";
}
async function applyBoardOp( async function applyBoardOp(
id: string | number, id: string | number,
op: BoardOp, op: BoardOp,
row: WorkOrderTableRow, row: WorkOrderTableRow,
): Promise<WorkOrderTableRow> { ): Promise<WorkOrderTableRow> {
if ("pocName" in op) { if (isPocOp(op)) {
if (!row.rowVersion) { if (!row.rowVersion) {
throw new Error("workOrderVersion is required for board patch"); throw new Error("workOrderVersion is required for board patch");
} }
return workOrdersApi.updatePoc(id, { return workOrdersApi.updatePoc(id, {
pocName: op.pocName, pocName: op.pocName ?? row.pocName,
pocPhone: op.pocPhone, pocPhone: op.pocPhone ?? row.pocPhone,
pocNotes: op.pocNotes, pocNotes: op.pocNotes ?? row.pocNotes,
workOrderVersion: row.rowVersion, workOrderVersion: row.rowVersion,
}); });
} }

View file

@ -7,7 +7,7 @@ import type {
/** /**
* Fields compared for dirty state and included in Save patch. * Fields compared for dirty state and included in Save patch.
* Only keys that map to board PATCH (or required companions like dispatcherName). * 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. * tech phone is not board-editable — exclude so Save cannot no-op.
* additionalContacts is board-editable (SH-218 PATCH field). * additionalContacts is board-editable (SH-218 PATCH field).
*/ */

View file

@ -35,7 +35,7 @@ describe("isLocalOnlyTablePatch", () => {
expect(isLocalOnlyTablePatch({ scheduleEnd: "2026-06-12", schedMode: "range" })).toBe(true); 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( expect(isLocalOnlyTablePatch({ pocName: "Pat", pocPhone: "111", pocNotes: "call" })).toBe(
false, 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( expect(
expandTablePatchToBoardFieldOps({ expandTablePatchToBoardFieldOps({
pocName: "Pat", pocName: "Pat",
@ -213,7 +213,7 @@ describe("expandTablePatchToBoardFieldOps", () => {
{ {
kind: "poc", kind: "poc",
pocName: "Pat", pocName: "Pat",
pocPhone: "", pocPhone: undefined,
pocNotes: "call first", pocNotes: "call first",
}, },
{ field: "docStatus", value: "Yes" }, { field: "docStatus", value: "Yes" },

View file

@ -9,12 +9,14 @@ import type {
const getById = vi.fn(); const getById = vi.fn();
const update = vi.fn(); const update = vi.fn();
const updatePoc = vi.fn();
const patchBoardField = vi.fn(); const patchBoardField = vi.fn();
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({ vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
workOrdersApi: { workOrdersApi: {
getById: (...args: unknown[]) => getById(...args), getById: (...args: unknown[]) => getById(...args),
update: (...args: unknown[]) => update(...args), update: (...args: unknown[]) => update(...args),
updatePoc: (...args: unknown[]) => updatePoc(...args),
patchBoardField: (...args: unknown[]) => patchBoardField(...args), patchBoardField: (...args: unknown[]) => patchBoardField(...args),
}, },
})); }));
@ -121,6 +123,7 @@ describe("useWorkOrderTableMutations completion-doc persistence", () => {
beforeEach(() => { beforeEach(() => {
getById.mockReset(); getById.mockReset();
update.mockReset(); update.mockReset();
updatePoc.mockReset();
patchBoardField.mockReset(); patchBoardField.mockReset();
serverState = { ...BASE_SERVER }; serverState = { ...BASE_SERVER };
rowState = { ...BASE_ROW }; 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", () => { describe("useWorkOrderTableMutations audit cache refresh", () => {
it("invalidates work-order queries after a successful status transition", async () => { it("invalidates work-order queries after a successful status transition", async () => {
const client = new QueryClient({ const client = new QueryClient({

View file

@ -87,7 +87,7 @@ describe("slide-over-draft", () => {
expect(buildSlideOverPatch(vendorDraft, row)).toEqual({ vendorId: "v2" }); 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 row = baseRow();
const draft = baseRow({ pocName: "Jordan", pocPhone: "555-0100" }); const draft = baseRow({ pocName: "Jordan", pocPhone: "555-0100" });
expect(isSlideOverDraftDirty(draft, row)).toBe(true); expect(isSlideOverDraftDirty(draft, row)).toBe(true);