feat(work-orders): confirm dialog when duplicate wo number detected

This commit is contained in:
arthur.bassi 2026-07-31 16:33:41 -03:00
parent 6ac08c5851
commit 287517b08f
6 changed files with 197 additions and 17 deletions

View file

@ -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 (
<Dialog open={open} onClose={onDismiss} aria-labelledby="duplicate-wo-number-dialog-title">
<DialogTitle id="duplicate-wo-number-dialog-title">Duplicate Work Order Number</DialogTitle>
<DialogContent>
<Text variant="body">
Work order number {woNumber} is already assigned to another work order. View the existing
record or dismiss to keep editing.
</Text>
</DialogContent>
<DialogActions>
<Button onClick={onDismiss}>Dismiss</Button>
<Button
variant="contained"
disabled={existing == null}
onClick={() => {
if (existing != null) {
onViewExisting(existing);
}
}}
>
View existing
</Button>
</DialogActions>
</Dialog>
);
}

View file

@ -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,
],
);
}

View file

@ -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<string | number | null>(null);
const [duplicateRow, setDuplicateRow] = useState<WorkOrderTableRow | null>(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 (
<WoTableShell>
<table
style={{
width: "100%",
minWidth: WO_TABLE_MIN_WIDTH,
borderCollapse: "collapse",
<>
<WoTableShell>
<table
style={{
width: "100%",
minWidth: WO_TABLE_MIN_WIDTH,
borderCollapse: "collapse",
}}
>
<WoTableHead />
<WoTableBody
tableData={tableData}
renderRow={renderRow}
renderInlineRow={renderInlineRow}
/>
</table>
</WoTableShell>
<DuplicateWoNumberDialog
open={duplicateRow != null}
existing={duplicateRow}
onDismiss={() => setDuplicateRow(null)}
onViewExisting={(row) => {
setDuplicateRow(null);
onOpenSlideOver(row, false);
}}
>
<WoTableHead />
<WoTableBody
tableData={tableData}
renderRow={renderRow}
renderInlineRow={renderInlineRow}
/>
</table>
</WoTableShell>
/>
</>
);
}

View file

@ -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(
<DuplicateWoNumberDialog
open
existing={existingRow}
onDismiss={vi.fn()}
onViewExisting={vi.fn()}
/>,
);
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(
<DuplicateWoNumberDialog
open
existing={existingRow}
onDismiss={vi.fn()}
onViewExisting={onViewExisting}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "View existing" }));
expect(onViewExisting).toHaveBeenCalledWith(existingRow);
});
it("calls onDismiss when Dismiss is clicked", () => {
const onDismiss = vi.fn();
renderWithProviders(
<DuplicateWoNumberDialog
open
existing={existingRow}
onDismiss={onDismiss}
onViewExisting={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
expect(onDismiss).toHaveBeenCalledTimes(1);
});
});

View file

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

View file

@ -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