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 (
+
+ );
+}
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