From 12fa18fc5e0f48691dc6e087c4afeb041641ead8 Mon Sep 17 00:00:00 2001 From: "arthur.bassi" Date: Mon, 3 Aug 2026 13:54:00 -0300 Subject: [PATCH] fix(work-orders): show duplicate wo number dialog on wizard create --- .../_components/wizard/new-wo-wizard.tsx | 52 +++++++++++++++++-- .../wizard/wizard-step-type-schedule.tsx | 3 ++ src/app/(protected)/workorders/index.tsx | 11 +++- .../utils/find-duplicate-wo-number-row.ts | 16 ++++++ .../new-wo-wizard-duplicate.test.tsx | 52 +++++++++++++++++++ .../find-duplicate-wo-number-row.test.ts | 19 +++++++ tmp/pr-descriptions/pr8-duplicate-dialog.md | 8 +-- 7 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 src/domain/work-orders/utils/find-duplicate-wo-number-row.ts create mode 100644 src/test/app/(protected)/workorders/new-wo-wizard-duplicate.test.tsx create mode 100644 src/test/domain/work-orders/utils/find-duplicate-wo-number-row.test.ts diff --git a/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx b/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx index 6643256d..123dfd11 100644 --- a/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx +++ b/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { Dialog } from "@mui/material"; import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop"; import { Text } from "@/components/ui/text"; +import { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog"; import { useDropdownOptionsByCategory } from "@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category"; import { isWoWizardMockEnabled } from "@/domain/work-orders/mocks/is-wo-wizard-mock-enabled"; import { @@ -12,6 +13,7 @@ import { MOCK_WO_WIZARD_VENDORS, } from "@/domain/work-orders/mocks/wo-wizard-mock-data"; import type { WorkOrderUser } from "@/domain/work-orders/types/work-order"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { EMPTY_WIZARD_DRAFT, WIZARD_STEPS, @@ -23,6 +25,7 @@ import { useWorkOrderVendors, } 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 { WizardFooter } from "@/app/(protected)/workorders/_components/wizard/wizard-footer"; import { WizardSidebar } from "@/app/(protected)/workorders/_components/wizard/wizard-sidebar"; import { WizardStepLocationService } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service"; @@ -33,6 +36,8 @@ type NewWoWizardProps = { open: boolean; onOpenChange: (open: boolean) => void; users: WorkOrderUser[]; + existingRows?: WorkOrderTableRow[]; + onViewExistingWo?: (row: WorkOrderTableRow) => void; }; function WizardStepHeader({ step }: { step: number }) { @@ -75,7 +80,13 @@ function WizardStepHeader({ step }: { step: number }) { ); } -export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) { +export function NewWoWizard({ + open, + onOpenChange, + users, + existingRows = [], + onViewExistingWo, +}: NewWoWizardProps) { const mockFallback = isWoWizardMockEnabled(); const [step, setStep] = useState(1); const [draft, setDraft] = useState({ @@ -83,6 +94,7 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) { status: "Incomplete", }); const [pocAutoFilledFor, setPocAutoFilledFor] = useState(""); + const [duplicateRow, setDuplicateRow] = useState(null); const { data: apiLocations = [] } = useWorkOrderLocations(); const { data: apiProblems = [] } = useDropdownOptionsByCategory("Problem"); @@ -117,6 +129,7 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) { setStep(1); setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" }); setPocAutoFilledFor(""); + setDuplicateRow(null); } }, [open]); @@ -149,10 +162,28 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) { } }; - const handleCreate = () => createMutation.mutate(draft); + const resolveDuplicate = (woNumber: string) => + findDuplicateWoNumberRow(woNumber, existingRows); + + const handleDuplicateFound = (woNumber: string) => { + const duplicate = resolveDuplicate(woNumber); + if (duplicate) { + setDuplicateRow(duplicate); + } + }; + + const handleCreate = () => { + const duplicate = resolveDuplicate(draft.woNumber); + if (duplicate) { + setDuplicateRow(duplicate); + return; + } + createMutation.mutate(draft); + }; return ( - + onOpenChange(false)} maxWidth={false} @@ -187,6 +218,7 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) { users={wizardUsers} onUpdate={setField} onPatch={patchDraft} + onDuplicateWoNumber={handleDuplicateFound} /> )} @@ -221,6 +253,18 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) { onCreate={handleCreate} isCreating={createMutation.isPending} /> - + + + setDuplicateRow(null)} + onViewExisting={(row) => { + setDuplicateRow(null); + onOpenChange(false); + onViewExistingWo?.(row); + }} + /> + ); } diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx index e3d4de1e..c75e3ec7 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule.tsx @@ -116,6 +116,7 @@ type WizardStepTypeScheduleProps = { users: WorkOrderUser[]; onUpdate: (key: K, value: WorkOrderWizardDraft[K]) => void; onPatch: (patch: Partial) => void; + onDuplicateWoNumber?: (woNumber: string) => void; }; export function WizardStepTypeSchedule({ @@ -123,6 +124,7 @@ export function WizardStepTypeSchedule({ users, onUpdate, onPatch, + onDuplicateWoNumber, }: WizardStepTypeScheduleProps) { return (
@@ -137,6 +139,7 @@ export function WizardStepTypeSchedule({ type="text" value={draft.woNumber} onChange={(e) => onUpdate("woNumber", e.target.value)} + onBlur={() => onDuplicateWoNumber?.(draft.woNumber)} placeholder="e.g. 24818" maxLength={20} className={DLG_INPUT_CLS} diff --git a/src/app/(protected)/workorders/index.tsx b/src/app/(protected)/workorders/index.tsx index 8e0173e6..d4e70189 100644 --- a/src/app/(protected)/workorders/index.tsx +++ b/src/app/(protected)/workorders/index.tsx @@ -146,7 +146,16 @@ export default function WorkOrdersListPage() { vendorTechs={vendorTechs} /> - + { + tableData.setSelectedId(row.id); + navigate(`/workorders/${row.id}`); + }} + /> {activeMediaRow != null && ( row.woNumber === normalized && String(row.id) !== String(excludeId ?? ""), + ); +} diff --git a/src/test/app/(protected)/workorders/new-wo-wizard-duplicate.test.tsx b/src/test/app/(protected)/workorders/new-wo-wizard-duplicate.test.tsx new file mode 100644 index 00000000..1522a345 --- /dev/null +++ b/src/test/app/(protected)/workorders/new-wo-wizard-duplicate.test.tsx @@ -0,0 +1,52 @@ +import { fireEvent, screen } 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; + +vi.mock("@/domain/work-orders/use-cases/use-create-work-order-from-wizard", () => ({ + useCreateWorkOrderFromWizard: () => ({ + mutate: vi.fn(), + isPending: false, + }), +})); + +vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({ + useWorkOrderLocations: () => ({ data: [] }), + useWorkOrderVendors: () => ({ data: [] }), +})); + +vi.mock("@/domain/locations/use-cases/use-location-detail", () => ({ + useLocationDetail: () => ({ data: undefined }), +})); + +vi.mock("@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category", () => ({ + useDropdownOptionsByCategory: () => ({ data: [] }), +})); + +describe("NewWoWizard duplicate wo number", () => { + it("shows the duplicate dialog when the wizard wo number matches an existing row", () => { + renderWithProviders( + , + ); + + const woNumberInput = screen.getByPlaceholderText("e.g. 24818"); + fireEvent.change(woNumberInput, { + target: { value: "12345678901" }, + }); + fireEvent.blur(woNumberInput); + + expect(screen.getByText("Duplicate Work Order Number")).toBeInTheDocument(); + expect( + screen.getByText(/is already assigned to another work order/i), + ).toBeInTheDocument(); + }); +}); diff --git a/src/test/domain/work-orders/utils/find-duplicate-wo-number-row.test.ts b/src/test/domain/work-orders/utils/find-duplicate-wo-number-row.test.ts new file mode 100644 index 00000000..f56dda8d --- /dev/null +++ b/src/test/domain/work-orders/utils/find-duplicate-wo-number-row.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { findDuplicateWoNumberRow } from "@/domain/work-orders/utils/find-duplicate-wo-number-row"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; + +const rows = [ + { id: "wo-1", woNumber: "12345678901" }, + { id: "wo-2", woNumber: "99999999999" }, +] as WorkOrderTableRow[]; + +describe("findDuplicateWoNumberRow", () => { + it("returns a matching row when the wo number already exists", () => { + expect(findDuplicateWoNumberRow("12345678901", rows)).toEqual(rows[0]); + }); + + it("ignores blank values and excluded ids", () => { + expect(findDuplicateWoNumberRow("", rows)).toBeUndefined(); + expect(findDuplicateWoNumberRow("12345678901", rows, "wo-1")).toBeUndefined(); + }); +}); diff --git a/tmp/pr-descriptions/pr8-duplicate-dialog.md b/tmp/pr-descriptions/pr8-duplicate-dialog.md index 77f29a83..cd85ffa3 100644 --- a/tmp/pr-descriptions/pr8-duplicate-dialog.md +++ b/tmp/pr-descriptions/pr8-duplicate-dialog.md @@ -1,15 +1,15 @@ ## Summary -- Show a confirmation dialog when inline WO number editing detects a duplicate instead of navigating immediately to the existing work order. -- Dialog title: **Duplicate Work Order Number**; actions: **View existing** (opens the existing WO slide-over) and **Dismiss** (closes the dialog and keeps the current row editable). -- Duplicate scan logic in `editable-wo-number-cell.tsx` is unchanged; only the post-detection UX is updated. +- Show a confirmation dialog when inline WO number editing **or wizard create** detects a duplicate instead of navigating immediately to the existing work order. +- Dialog title: **Duplicate Work Order Number**; actions: **View existing** (opens the existing WO via slide-over route) and **Dismiss** (closes the dialog and keeps the current row editable). +- Duplicate scan logic in `editable-wo-number-cell.tsx` is unchanged; dialog state lives in `wo-table.tsx` under `list/`. ## Test plan - [ ] Edit a WO number in the table to match an existing row → dialog opens with the duplicate number in the message. - [ ] Click **Dismiss** → dialog closes; edited row keeps its original number. - [ ] Repeat duplicate entry → click **View existing** → navigates to `/workorders/{id}` for the existing row. -- [ ] Confirm non-duplicate WO number edits still save normally. +- [ ] Wizard create with duplicate WO number → same dialog; **View existing** closes wizard and opens `/workorders/{id}`. ## Out of scope