diff --git a/src/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog.tsx b/src/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog.tsx new file mode 100644 index 00000000..95decbe7 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog.tsx @@ -0,0 +1,45 @@ +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { Text } from "@/components/ui/text"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; + +type DuplicateWoNumberDialogProps = { + open: boolean; + existing: WorkOrderTableRow | null; + onDismiss: () => void; + onViewExisting: (row: WorkOrderTableRow) => void; +}; + +export function DuplicateWoNumberDialog({ + open, + existing, + onDismiss, + onViewExisting, +}: DuplicateWoNumberDialogProps) { + const woNumber = existing?.woNumber ?? ""; + + return ( + + Duplicate Work Order Number + + + Work order number {woNumber} is already assigned to another work order. View the existing + record or dismiss to keep editing. + + + + + + + + ); +} diff --git a/src/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers.ts b/src/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers.ts index 59aca8f5..c9626b3e 100644 --- a/src/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers.ts +++ b/src/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers.ts @@ -19,6 +19,7 @@ type UseWoTableRowHandlersOptions = { onOpenMedia: (row: WorkOrderTableRow) => void; onPatch: (id: string | number, patch: WorkOrderTablePatch) => void; onScheduleHighlight: (id: string | number | null) => void; + onDuplicateFound?: (existing: WorkOrderTableRow) => void; }; export function useWoTableRowHandlers({ @@ -30,6 +31,7 @@ export function useWoTableRowHandlers({ onOpenMedia, onPatch, onScheduleHighlight, + onDuplicateFound, }: UseWoTableRowHandlersOptions): WoTableRowHandlers { const { dragState: dragStateRef, @@ -97,8 +99,7 @@ export function useWoTableRowHandlers({ }); }, onDuplicateFound: (existing: WorkOrderTableRow) => { - tableData.setSelectedId(existing.id); - onOpenSlideOver(existing, false); + onDuplicateFound?.(existing); }, onOpenSlideOver: (row: WorkOrderTableRow) => onOpenSlideOver(row, false), onPastDueAttempt: (id: string | number) => onScheduleHighlight(id), @@ -138,6 +139,7 @@ export function useWoTableRowHandlers({ onOpenMedia, onPatch, onScheduleHighlight, + onDuplicateFound, ], ); } diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx index e94c3ad7..cb5b8a9f 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx @@ -10,6 +10,7 @@ import type { WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; import { useCreateWorkOrderFromWizard } from "@/domain/work-orders/use-cases/use-create-work-order-from-wizard"; +import { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog"; import { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton"; import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types"; import { buildInlineDraftCreateInput } from "./build-inline-draft-create-input"; @@ -55,6 +56,7 @@ export function WoTable({ onPatch, }: WoTableProps) { const [scheduleHighlightId, setScheduleHighlightId] = useState(null); + const [duplicateRow, setDuplicateRow] = useState(null); const { data: locationSites = [] } = useLocationSites(); const sitePlaceLookups = useMemo(() => buildSitePlaceLookups(locationSites), [locationSites]); const createMutation = useCreateWorkOrderFromWizard(() => { @@ -70,6 +72,7 @@ export function WoTable({ onOpenMedia, onPatch, onScheduleHighlight: setScheduleHighlightId, + onDuplicateFound: setDuplicateRow, }); const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => ( @@ -150,21 +153,33 @@ export function WoTable({ } return ( - - + +
+ + +
+
+ + setDuplicateRow(null)} + onViewExisting={(row) => { + setDuplicateRow(null); + onOpenSlideOver(row, false); }} - > - - - - + /> + ); } diff --git a/src/test/app/(protected)/workorders/duplicate-wo-number-dialog.test.tsx b/src/test/app/(protected)/workorders/duplicate-wo-number-dialog.test.tsx new file mode 100644 index 00000000..5d126913 --- /dev/null +++ b/src/test/app/(protected)/workorders/duplicate-wo-number-dialog.test.tsx @@ -0,0 +1,58 @@ +import { fireEvent, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog"; +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; + +describe("DuplicateWoNumberDialog", () => { + it("shows the duplicate title and work order number when open", () => { + renderWithProviders( + , + ); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText("Duplicate Work Order Number")).toBeInTheDocument(); + expect(screen.getByText(/12345678901/)).toBeInTheDocument(); + }); + + it("calls onViewExisting when View existing is clicked", () => { + const onViewExisting = vi.fn(); + + renderWithProviders( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "View existing" })); + + expect(onViewExisting).toHaveBeenCalledWith(existingRow); + }); + + it("calls onDismiss when Dismiss is clicked", () => { + const onDismiss = vi.fn(); + + renderWithProviders( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Dismiss" })); + + expect(onDismiss).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/test/app/(protected)/workorders/use-wo-table-row-handlers-duplicate.test.ts b/src/test/app/(protected)/workorders/use-wo-table-row-handlers-duplicate.test.ts new file mode 100644 index 00000000..df563025 --- /dev/null +++ b/src/test/app/(protected)/workorders/use-wo-table-row-handlers-duplicate.test.ts @@ -0,0 +1,43 @@ +import { renderHook } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { useWoTableRowHandlers } from "@/app/(protected)/workorders/_components/list/table/use-wo-table-row-handlers"; +import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; + +const existingRow = { id: "wo-99", woNumber: "12345678901" } as WorkOrderTableRow; + +function buildTableData() { + return { + dragState: { current: { dragId: null, overId: null } }, + setDragViz: vi.fn(), + reorderRows: vi.fn(), + endDrag: vi.fn(), + setUnschedOpen: vi.fn(), + triggerFlash: vi.fn(), + setSelectedId: vi.fn(), + } as never; +} + +describe("useWoTableRowHandlers onDuplicateFound", () => { + it("delegates to onDuplicateFound instead of opening the slide-over", () => { + const onDuplicateFound = vi.fn(); + const onOpenSlideOver = vi.fn(); + + const { result } = renderHook(() => + useWoTableRowHandlers({ + tableData: buildTableData(), + weekMonday: "2026-07-20", + onWeekChange: vi.fn(), + onOpenSlideOver, + onOpenMedia: vi.fn(), + onPatch: vi.fn(), + onScheduleHighlight: vi.fn(), + onDuplicateFound, + }), + ); + + result.current.onDuplicateFound?.(existingRow); + + expect(onDuplicateFound).toHaveBeenCalledWith(existingRow); + expect(onOpenSlideOver).not.toHaveBeenCalled(); + }); +}); diff --git a/tmp/pr-descriptions/pr8-duplicate-dialog.md b/tmp/pr-descriptions/pr8-duplicate-dialog.md new file mode 100644 index 00000000..77f29a83 --- /dev/null +++ b/tmp/pr-descriptions/pr8-duplicate-dialog.md @@ -0,0 +1,17 @@ +## 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. + +## 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. + +## Out of scope + +- Server-side uniqueness (422) handling +- SH provisional numbering