fix(work-orders): check duplicate numbers globally

This commit is contained in:
arthur.bassi 2026-08-04 10:54:07 -03:00
parent 41dd7d0957
commit 6ac1b5cd42
4 changed files with 126 additions and 15 deletions

View file

@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { toast } from "react-toastify";
import { isWoWizardMockEnabled } from "@/domain/work-orders/mocks/is-wo-wizard-mock-enabled";
import {
getMockWoWizardLocationDetail,
@ -21,6 +22,7 @@ import {
} from "@/domain/work-orders/use-cases/use-work-order-detail";
import { useCreateWorkOrderFromWizard } from "@/domain/work-orders/use-cases/use-create-work-order-from-wizard";
import { findDuplicateWoNumberRow } from "@/domain/work-orders/utils/find-duplicate-wo-number-row";
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
type UseNewWoWizardStateArgs = {
open: boolean;
@ -43,6 +45,7 @@ export function useNewWoWizardState({
});
const [pocAutoFilledFor, setPocAutoFilledFor] = useState("");
const [duplicateRow, setDuplicateRow] = useState<WorkOrderTableRow | null>(null);
const [isCheckingDuplicate, setIsCheckingDuplicate] = useState(false);
const { data: apiLocations = [] } = useWorkOrderLocations();
const { data: apiProblems = [] } = useDropdownOptionsByCategory("Problem");
@ -110,22 +113,40 @@ export function useNewWoWizardState({
}
};
const resolveDuplicate = (woNumber: string) => findDuplicateWoNumberRow(woNumber, existingRows);
const resolveDuplicate = async (woNumber: string) => {
const localDuplicate = findDuplicateWoNumberRow(woNumber, existingRows);
if (localDuplicate) {
return localDuplicate;
}
// The loaded board rows are only an immediate UX hint; the unfiltered API lookup is authoritative.
return workOrdersApi.findByWoNumber(woNumber);
};
const handleDuplicateFound = (woNumber: string) => {
const duplicate = resolveDuplicate(woNumber);
if (duplicate) {
setDuplicateRow(duplicate);
}
void resolveDuplicate(woNumber).then((duplicate) => {
if (duplicate && woNumber === draft.woNumber) {
setDuplicateRow(duplicate);
}
});
};
const handleCreate = () => {
const duplicate = resolveDuplicate(draft.woNumber);
if (duplicate) {
setDuplicateRow(duplicate);
return;
}
createMutation.mutate(draft);
void (async () => {
setIsCheckingDuplicate(true);
try {
const duplicate = await resolveDuplicate(draft.woNumber);
if (duplicate) {
setDuplicateRow(duplicate);
return;
}
createMutation.mutate(draft);
} catch {
toast.error("Unable to verify the work order number. Please try again.");
} finally {
setIsCheckingDuplicate(false);
}
})();
};
return {
@ -143,6 +164,6 @@ export function useNewWoWizardState({
patchDraft,
handleDuplicateFound,
handleCreate,
isCreating: createMutation.isPending,
isCreating: createMutation.isPending || isCheckingDuplicate,
};
}

View file

@ -191,6 +191,22 @@ export const workOrdersApi = {
};
},
findByWoNumber: async (woNumber: string): Promise<WorkOrderTableRow | undefined> => {
const normalized = woNumber.trim();
if (normalized === "") {
return undefined;
}
// `board/search` without date or board filters searches the entire work-order collection.
const result = await workOrdersApi.searchBoard({
search: normalized,
page: 1,
pageSize: 20,
});
return result.items.find((row) => row.woNumber === normalized);
},
getListByLocation: async (
locationId: string | number,
search = "",

View file

@ -1,10 +1,11 @@
import { fireEvent, screen } from "@testing-library/react";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { NewWoWizard } from "@/app/(protected)/workorders/_components/wizard/new-wo-wizard";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { renderWithProviders } from "@/test/test-utils";
const existingRow = { id: "wo-99", woNumber: "12345678901" } as WorkOrderTableRow;
const findByWoNumber = vi.fn();
vi.mock("@/domain/work-orders/use-cases/use-create-work-order-from-wizard", () => ({
useCreateWorkOrderFromWizard: () => ({
@ -26,8 +27,14 @@ vi.mock("@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-ca
useDropdownOptionsByCategory: () => ({ data: [] }),
}));
vi.mock("@/domain/work-orders/api/work-orders-api", () => ({
workOrdersApi: {
findByWoNumber: (...args: unknown[]) => findByWoNumber(...args),
},
}));
describe("NewWoWizard duplicate wo number", () => {
it("shows the duplicate dialog when the wizard wo number matches an existing row", () => {
it("shows the duplicate dialog when the wizard wo number matches an existing row", async () => {
renderWithProviders(
<NewWoWizard
open
@ -44,7 +51,38 @@ describe("NewWoWizard duplicate wo number", () => {
});
fireEvent.blur(woNumberInput);
expect(screen.getByText("Duplicate Work Order Number")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("Duplicate Work Order Number")).toBeInTheDocument();
});
expect(screen.getByText(/is already assigned to another work order/i)).toBeInTheDocument();
});
it("finds a duplicate outside the loaded board rows", async () => {
const offPageRow = {
id: "wo-off-page",
woNumber: "98765432109",
} as WorkOrderTableRow;
findByWoNumber.mockResolvedValueOnce(offPageRow);
renderWithProviders(
<NewWoWizard
open
onOpenChange={vi.fn()}
users={[]}
existingRows={[]}
onViewExistingWo={vi.fn()}
/>,
);
const woNumberInput = screen.getByPlaceholderText("e.g. 24818");
fireEvent.change(woNumberInput, {
target: { value: "98765432109" },
});
fireEvent.blur(woNumberInput);
await waitFor(() => {
expect(screen.getByText("Duplicate Work Order Number")).toBeInTheDocument();
});
expect(findByWoNumber).toHaveBeenCalledWith("98765432109");
});
});

View file

@ -291,6 +291,42 @@ describe("workOrdersApi.searchBoard", () => {
});
});
describe("workOrdersApi.findByWoNumber", () => {
beforeEach(() => {
apiGet.mockReset();
apiGet.mockResolvedValue({
items: [
{
id: 505,
woNumber: "10000001235",
lifecycleStatusLabel: "Incomplete",
},
],
totalCount: 1,
page: 1,
pageSize: 20,
});
});
it("searches globally without the active board filters", async () => {
const result = await workOrdersApi.findByWoNumber("10000001235");
expect(apiGet).toHaveBeenCalledWith(
API_PATHS.workOrder.boardSearch,
expect.objectContaining({
searchParams: expect.any(URLSearchParams),
}),
);
const searchParams = apiGet.mock.calls[0]?.[1]?.searchParams as URLSearchParams;
expect(Object.fromEntries(searchParams)).toEqual({
search: "10000001235",
page: "1",
pageSize: "20",
});
expect(result).toMatchObject({ id: 505, woNumber: "10000001235" });
});
});
describe("workOrdersApi.patchBoardField", () => {
beforeEach(() => {
apiPatchFn.mockReset();