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,
),
/** 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,

View file

@ -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,
},
];
}

View file

@ -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;

View file

@ -54,19 +54,23 @@ export type WorkOrderTablePatchHandlers = {
type PatchResult =
{ 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(
id: string | number,
op: BoardOp,
row: WorkOrderTableRow,
): Promise<WorkOrderTableRow> {
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,
});
}

View file

@ -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).
*/

View file

@ -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" },

View file

@ -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({

View file

@ -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);