diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts index be144b41..9d4f19d5 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts @@ -157,7 +157,7 @@ export function useWorkOrdersListPage() { generatedAt: data.generatedAt, }; setCompletionDocs((prev) => ({ ...prev, [String(row.id)]: nextData })); - const saved = await patchFieldAsync(row.id, { docStatus: status }); + const saved = await patchFieldAsync(row.id, { docStatus: status }, undefined, row); if (saved.kind !== "board" || !shouldCompleteAfterCompletionDoc(status, saved.row)) { return; } 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 9d6dccd2..75d3313a 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 @@ -154,19 +154,34 @@ function syncDetailRowVersion( ); } +type SavedRowSync = { + queryClient: QueryClient; + /** Last server row each patch returned, for panels whose detail query never cached data. */ + savedRows: Map; +}; + +function recordSavedRow( + { queryClient, savedRows }: SavedRowSync, + id: string | number, + row: WorkOrderTableRow, +) { + savedRows.set(String(id), row); + syncDetailRowVersion(queryClient, id, row); +} + async function runPatchAndSyncDetail( - queryClient: QueryClient, + sync: SavedRowSync, id: string | number, patch: WorkOrderTablePatch, readBase: (id: string | number) => WorkOrderTableRow | undefined, ): Promise { try { const result = await runWorkOrderTablePatch(id, patch, readBase); - if (result.kind === "board") syncDetailRowVersion(queryClient, id, result.row); + if (result.kind === "board") recordSavedRow(sync, id, result.row); return result; } catch (error) { if (error instanceof BoardPatchPartialSaveError) { - syncDetailRowVersion(queryClient, id, error.partialRow); + recordSavedRow(sync, id, error.partialRow); } throw error; } @@ -189,6 +204,7 @@ export function useWorkOrderTableMutations( const { onPatch, clearPatch, getRow } = handlers; const queryClient = useQueryClient(); const enqueuePatch = useMemo(() => createWorkOrderPatchQueue(), []); + const savedRows = useMemo(() => new Map(), []); const invalidate = () => { void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all }); @@ -212,12 +228,16 @@ export function useWorkOrderTableMutations( }) => enqueuePatch(id, () => runPatchAndSyncDetail( - queryClient, + { queryClient, savedRows }, id, persistPatch, baseRow ? () => baseRow - : (rowId) => getRow?.(rowId) ?? readDetailRow(queryClient, rowId) ?? fallbackRow, + : (rowId) => + getRow?.(rowId) ?? + readDetailRow(queryClient, rowId) ?? + savedRows.get(String(rowId)) ?? + fallbackRow, ), ), onMutate: async ({ id, persistPatch }) => { @@ -248,9 +268,6 @@ export function useWorkOrderTableMutations( }, }); - const resolvePatch = (id: string | number, patch: WorkOrderTablePatch) => - mergeAutoSchedulePatch(getRow?.(id), patch); - const patchField: WorkOrderTablePatchFn = (id, patch, options) => { const fallbackRow = options?.fallbackRow; const persistPatch = mergeAutoSchedulePatch(getRow?.(id) ?? fallbackRow, patch); @@ -264,9 +281,10 @@ export function useWorkOrderTableMutations( id: string | number, patch: WorkOrderTablePatch, baseRow?: WorkOrderTableRow, + fallbackRow?: WorkOrderTableRow, ) => { - const persistPatch = baseRow ? mergeAutoSchedulePatch(baseRow, patch) : resolvePatch(id, patch); - return patchMutation.mutateAsync({ id, patch, persistPatch, baseRow }); + const persistPatch = mergeAutoSchedulePatch(baseRow ?? getRow?.(id) ?? fallbackRow, patch); + return patchMutation.mutateAsync({ id, patch, persistPatch, baseRow, fallbackRow }); }; return { diff --git a/src/test/app/(protected)/workorders/use-work-orders-list-page-completion-doc.test.ts b/src/test/app/(protected)/workorders/use-work-orders-list-page-completion-doc.test.ts index 7e2de77c..db23aa9d 100644 --- a/src/test/app/(protected)/workorders/use-work-orders-list-page-completion-doc.test.ts +++ b/src/test/app/(protected)/workorders/use-work-orders-list-page-completion-doc.test.ts @@ -131,10 +131,11 @@ describe("useWorkOrdersListPage completion document (SH-341)", () => { row: { ...saved, status: "Completed" }, }); const { result } = renderHook(() => useWorkOrdersListPage()); + const docRow = closableRow(); - await result.current.saveCompletionDoc(closableRow(), { notes: "", generatedAt: "now" }, "Yes"); + await result.current.saveCompletionDoc(docRow, { notes: "", generatedAt: "now" }, "Yes"); - expect(patchFieldAsync).toHaveBeenNthCalledWith(1, 77, { docStatus: "Yes" }); + expect(patchFieldAsync).toHaveBeenNthCalledWith(1, 77, { docStatus: "Yes" }, undefined, docRow); expect(patchFieldAsync).toHaveBeenNthCalledWith(2, 77, { status: "Completed" }, saved); expect(patchField).not.toHaveBeenCalled(); expect(result.current.confirmComplete).toBeNull(); diff --git a/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-off-list.test.tsx b/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-off-list.test.tsx index 4e7ad698..8c3c8d55 100644 --- a/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-off-list.test.tsx +++ b/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-off-list.test.tsx @@ -123,6 +123,36 @@ describe("useWorkOrderTableMutations for a work order outside the loaded list", ).toBe("saved-2"); }); + it("chains the saved version when the panel's detail query never cached data", async () => { + let version = 0; + patchBoardField.mockImplementation(async () => { + version += 1; + return { ...DETAIL_ROW, rowVersion: `saved-${version}` }; + }); + const onSecondSuccess = vi.fn(); + const { result } = renderHook( + () => + useWorkOrderTableMutations({ + onPatch: vi.fn(), + clearPatch: vi.fn(), + getRow: () => undefined, + }), + { wrapper: makeWrapper() }, + ); + + act(() => { + result.current.patchField(9, { dueDate: "2026-10-09" }, { fallbackRow: DETAIL_ROW }); + result.current.patchField( + 9, + { dueDate: "2026-10-16" }, + { fallbackRow: DETAIL_ROW, onSuccess: onSecondSuccess }, + ); + }); + + await waitFor(() => expect(onSecondSuccess).toHaveBeenCalled()); + expect(patchBoardField.mock.calls[1][1]).toMatchObject({ workOrderVersion: "saved-1" }); + }); + it("prefers the table row when it still holds the work order", async () => { const tableRow = { ...DETAIL_ROW, rowVersion: "table-version" }; const onSuccess = vi.fn();