mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-03 12:43:24 +00:00
fix(workorders): save from the detail panel after the order leaves the list
The board patch read the work order version from the loaded list only. Once a save moved the order out of that list (e.g. to another week), the next save, Cancel or Complete from the open panel failed for lack of a version. The panel now passes its server row as the fallback patch base.
This commit is contained in:
parent
84cfc90282
commit
f9b5d3e0ea
9 changed files with 189 additions and 8 deletions
|
|
@ -167,6 +167,7 @@ export function useSlideOverEditState({
|
|||
return;
|
||||
}
|
||||
onSave(row.id, patch, {
|
||||
fallbackRow: infoSource ?? row,
|
||||
onSuccess: () => {
|
||||
notifySlideOverSaveSuccess(patch);
|
||||
setBaseline(activeDraft);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import { WorkOrderSlideOverTabs } from "./work-order-slide-over-tabs";
|
|||
export type SlideOverSaveOptions = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
/** The panel's server row, used when the loaded list no longer holds this work order. */
|
||||
fallbackRow?: WorkOrderTableRow;
|
||||
};
|
||||
|
||||
type WorkOrderSlideOverProps = {
|
||||
|
|
|
|||
|
|
@ -131,14 +131,14 @@ export function useWorkOrdersListPage() {
|
|||
|
||||
const handleConfirmCancel = () => {
|
||||
if (!confirmCancel) return;
|
||||
patchWorkOrderAsCanceled(patchField, confirmCancel);
|
||||
patchWorkOrderAsCanceled(patchField, confirmCancel, confirmCancel);
|
||||
setConfirmCancel(null);
|
||||
handleCloseSlideOver();
|
||||
};
|
||||
|
||||
const handleConfirmComplete = () => {
|
||||
if (!confirmComplete) return;
|
||||
patchWorkOrderAsCompleted(patchField, confirmComplete.id);
|
||||
patchWorkOrderAsCompleted(patchField, confirmComplete.id, confirmComplete);
|
||||
setConfirmComplete(null);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ export type { WorkOrderTablePatch };
|
|||
export type WorkOrderTablePatchOptions = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
/** Patch base when the loaded list no longer holds the work order (e.g. moved to another week). */
|
||||
fallbackRow?: WorkOrderTableRow;
|
||||
};
|
||||
|
||||
export type WorkOrderTablePatchFn = (
|
||||
|
|
@ -194,8 +196,9 @@ export function useWorkOrderTableMutations(
|
|||
mergeAutoSchedulePatch(getRow?.(id), patch);
|
||||
|
||||
const patchField: WorkOrderTablePatchFn = (id, patch, options) => {
|
||||
const persistPatch = resolvePatch(id, patch);
|
||||
void patchMutation.mutateAsync({ id, patch, persistPatch }).then(
|
||||
const baseRow = getRow?.(id) ? undefined : options?.fallbackRow;
|
||||
const persistPatch = baseRow ? mergeAutoSchedulePatch(baseRow, patch) : resolvePatch(id, patch);
|
||||
void patchMutation.mutateAsync({ id, patch, persistPatch, baseRow }).then(
|
||||
() => options?.onSuccess?.(),
|
||||
(error: Error) => options?.onError?.(error),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { toast } from "react-toastify";
|
||||
import type { WorkOrderTablePatch } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type {
|
||||
WorkOrderTablePatch,
|
||||
WorkOrderTableRow,
|
||||
} from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { VendorAssignmentFields } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||
|
||||
|
|
@ -9,6 +12,7 @@ type PersistPatchFn = (
|
|||
options?: {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
fallbackRow?: WorkOrderTableRow;
|
||||
},
|
||||
) => void;
|
||||
|
||||
|
|
@ -57,17 +61,27 @@ export function notifySlideOverSaveSuccess(patch: WorkOrderTablePatch) {
|
|||
export function patchWorkOrderAsCanceled(
|
||||
patchField: PersistPatchFn,
|
||||
row: { id: string | number; woNumber: string },
|
||||
fallbackRow?: WorkOrderTableRow,
|
||||
) {
|
||||
patchField(
|
||||
row.id,
|
||||
{ status: "Canceled" },
|
||||
{
|
||||
onSuccess: () => notifyWorkOrderCanceled(row.woNumber),
|
||||
fallbackRow,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function patchWorkOrderAsCompleted(patchField: PersistPatchFn, id: string | number) {
|
||||
export function patchWorkOrderAsCompleted(
|
||||
patchField: PersistPatchFn,
|
||||
id: string | number,
|
||||
fallbackRow?: WorkOrderTableRow,
|
||||
) {
|
||||
if (fallbackRow) {
|
||||
patchField(id, { status: "Completed" }, { fallbackRow });
|
||||
return;
|
||||
}
|
||||
patchField(id, { status: "Completed" });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -203,4 +203,35 @@ describe("useSlideOverEditState completed lock", () => {
|
|||
expect(toast.success).toHaveBeenCalledWith(workOrderSavedMessage());
|
||||
expect(toast.success).not.toHaveBeenCalledWith(vendorAssignedMessage());
|
||||
});
|
||||
|
||||
it("sends the panel's server row so a save works after the work order left the list", () => {
|
||||
const onSave = vi.fn();
|
||||
const opened = baseRow({ rowVersion: "opened" });
|
||||
const fresh = baseRow({ scheduledOn: "2026-07-22", rowVersion: "after-first-save" });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSlideOverEditState({
|
||||
row: opened,
|
||||
editMode: true,
|
||||
infoSource: fresh,
|
||||
onSave,
|
||||
onClose: vi.fn(),
|
||||
closeDisabled: false,
|
||||
setTab: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.handleDraftChange({ dispatcherName: "Blair" });
|
||||
});
|
||||
act(() => {
|
||||
result.current.commit();
|
||||
});
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
100,
|
||||
expect.anything(),
|
||||
expect.objectContaining({ fallbackRow: fresh }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ describe("useWorkOrdersListPage cancel toast", () => {
|
|||
expect(patchField).toHaveBeenCalledWith(
|
||||
77,
|
||||
{ status: "Canceled" },
|
||||
expect.objectContaining({ onSuccess: expect.any(Function) }),
|
||||
expect.objectContaining({ onSuccess: expect.any(Function), fallbackRow: row }),
|
||||
);
|
||||
expect(toast.warning).not.toHaveBeenCalled();
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ describe("useWorkOrdersListPage cancel toast", () => {
|
|||
result.current.handleConfirmComplete();
|
||||
});
|
||||
|
||||
expect(patchField).toHaveBeenCalledWith(77, { status: "Completed" });
|
||||
expect(patchField).toHaveBeenCalledWith(77, { status: "Completed" }, { fallbackRow: row });
|
||||
expect(toast.success).not.toHaveBeenCalled();
|
||||
expect(toast.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
|
||||
const patchBoardField = vi.fn();
|
||||
|
||||
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
|
||||
workOrdersApi: {
|
||||
patchBoardField: (...args: unknown[]) => patchBoardField(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("react-toastify", () => ({
|
||||
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() },
|
||||
}));
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import {
|
||||
applySchedulePatch,
|
||||
useWorkOrderTableMutations,
|
||||
} from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||
|
||||
function makeWrapper() {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
||||
};
|
||||
}
|
||||
|
||||
const DETAIL_ROW = {
|
||||
id: 9,
|
||||
woNumber: "00000000009",
|
||||
scheduledOn: "2026-10-07",
|
||||
status: "Scheduled",
|
||||
scheduleWeekOnly: false,
|
||||
targetWeek: "",
|
||||
rowVersion: "AAAAAAAAB9E=",
|
||||
dispatchRowVersion: "",
|
||||
primaryDispatchId: null,
|
||||
} as WorkOrderTableRow;
|
||||
|
||||
describe("useWorkOrderTableMutations for a work order outside the loaded list", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
patchBoardField.mockImplementation(async () => ({ ...DETAIL_ROW, rowVersion: "next" }));
|
||||
});
|
||||
|
||||
it("patches against the fallback row when the table no longer holds the work order", async () => {
|
||||
const onSuccess = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useWorkOrderTableMutations({
|
||||
onPatch: vi.fn(),
|
||||
clearPatch: vi.fn(),
|
||||
getRow: () => undefined,
|
||||
}),
|
||||
{ wrapper: makeWrapper() },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.patchField(9, applySchedulePatch("2026-10-02", false), {
|
||||
onSuccess,
|
||||
onError,
|
||||
fallbackRow: DETAIL_ROW,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(onSuccess).toHaveBeenCalled());
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
expect(patchBoardField).toHaveBeenCalledWith(
|
||||
9,
|
||||
expect.objectContaining({ workOrderVersion: "AAAAAAAAB9E=" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers the table row when it still holds the work order", async () => {
|
||||
const tableRow = { ...DETAIL_ROW, rowVersion: "table-version" };
|
||||
const onSuccess = vi.fn();
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useWorkOrderTableMutations({
|
||||
onPatch: vi.fn(),
|
||||
clearPatch: vi.fn(),
|
||||
getRow: () => tableRow,
|
||||
}),
|
||||
{ wrapper: makeWrapper() },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.patchField(9, applySchedulePatch("2026-10-02", false), {
|
||||
onSuccess,
|
||||
fallbackRow: DETAIL_ROW,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(onSuccess).toHaveBeenCalled());
|
||||
expect(patchBoardField).toHaveBeenCalledWith(
|
||||
9,
|
||||
expect.objectContaining({ workOrderVersion: "table-version" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { toast } from "react-toastify";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||
import {
|
||||
applyAssignedToTableSave,
|
||||
|
|
@ -185,4 +186,25 @@ describe("work-order-feedback-toasts", () => {
|
|||
expect(toast.success).not.toHaveBeenCalled();
|
||||
expect(toast.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forwards the panel row to cancel and complete patches", () => {
|
||||
const patchField = vi.fn();
|
||||
const row = { id: 42, woNumber: "20260819001", rowVersion: "v7" } as WorkOrderTableRow;
|
||||
|
||||
patchWorkOrderAsCanceled(patchField, row, row);
|
||||
patchWorkOrderAsCompleted(patchField, 42, row);
|
||||
|
||||
expect(patchField).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
42,
|
||||
{ status: "Canceled" },
|
||||
expect.objectContaining({ fallbackRow: row }),
|
||||
);
|
||||
expect(patchField).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
42,
|
||||
{ status: "Completed" },
|
||||
{ fallbackRow: row },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue