mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
feat(work-orders): confirm dialog when duplicate wo number detected
This commit is contained in:
parent
6ac08c5851
commit
287517b08f
6 changed files with 197 additions and 17 deletions
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ type UseWoTableRowHandlersOptions = {
|
||||||
onOpenMedia: (row: WorkOrderTableRow) => void;
|
onOpenMedia: (row: WorkOrderTableRow) => void;
|
||||||
onPatch: (id: string | number, patch: WorkOrderTablePatch) => void;
|
onPatch: (id: string | number, patch: WorkOrderTablePatch) => void;
|
||||||
onScheduleHighlight: (id: string | number | null) => void;
|
onScheduleHighlight: (id: string | number | null) => void;
|
||||||
|
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useWoTableRowHandlers({
|
export function useWoTableRowHandlers({
|
||||||
|
|
@ -30,6 +31,7 @@ export function useWoTableRowHandlers({
|
||||||
onOpenMedia,
|
onOpenMedia,
|
||||||
onPatch,
|
onPatch,
|
||||||
onScheduleHighlight,
|
onScheduleHighlight,
|
||||||
|
onDuplicateFound,
|
||||||
}: UseWoTableRowHandlersOptions): WoTableRowHandlers {
|
}: UseWoTableRowHandlersOptions): WoTableRowHandlers {
|
||||||
const {
|
const {
|
||||||
dragState: dragStateRef,
|
dragState: dragStateRef,
|
||||||
|
|
@ -97,8 +99,7 @@ export function useWoTableRowHandlers({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onDuplicateFound: (existing: WorkOrderTableRow) => {
|
onDuplicateFound: (existing: WorkOrderTableRow) => {
|
||||||
tableData.setSelectedId(existing.id);
|
onDuplicateFound?.(existing);
|
||||||
onOpenSlideOver(existing, false);
|
|
||||||
},
|
},
|
||||||
onOpenSlideOver: (row: WorkOrderTableRow) => onOpenSlideOver(row, false),
|
onOpenSlideOver: (row: WorkOrderTableRow) => onOpenSlideOver(row, false),
|
||||||
onPastDueAttempt: (id: string | number) => onScheduleHighlight(id),
|
onPastDueAttempt: (id: string | number) => onScheduleHighlight(id),
|
||||||
|
|
@ -138,6 +139,7 @@ export function useWoTableRowHandlers({
|
||||||
onOpenMedia,
|
onOpenMedia,
|
||||||
onPatch,
|
onPatch,
|
||||||
onScheduleHighlight,
|
onScheduleHighlight,
|
||||||
|
onDuplicateFound,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
WorkOrderTableRow,
|
WorkOrderTableRow,
|
||||||
} from "@/domain/work-orders/types/work-order-table-row";
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import { useCreateWorkOrderFromWizard } from "@/domain/work-orders/use-cases/use-create-work-order-from-wizard";
|
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 { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton";
|
||||||
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
|
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
|
||||||
import { buildInlineDraftCreateInput } from "./build-inline-draft-create-input";
|
import { buildInlineDraftCreateInput } from "./build-inline-draft-create-input";
|
||||||
|
|
@ -55,6 +56,7 @@ export function WoTable({
|
||||||
onPatch,
|
onPatch,
|
||||||
}: WoTableProps) {
|
}: WoTableProps) {
|
||||||
const [scheduleHighlightId, setScheduleHighlightId] = useState<string | number | null>(null);
|
const [scheduleHighlightId, setScheduleHighlightId] = useState<string | number | null>(null);
|
||||||
|
const [duplicateRow, setDuplicateRow] = useState<WorkOrderTableRow | null>(null);
|
||||||
const { data: locationSites = [] } = useLocationSites();
|
const { data: locationSites = [] } = useLocationSites();
|
||||||
const sitePlaceLookups = useMemo(() => buildSitePlaceLookups(locationSites), [locationSites]);
|
const sitePlaceLookups = useMemo(() => buildSitePlaceLookups(locationSites), [locationSites]);
|
||||||
const createMutation = useCreateWorkOrderFromWizard(() => {
|
const createMutation = useCreateWorkOrderFromWizard(() => {
|
||||||
|
|
@ -70,6 +72,7 @@ export function WoTable({
|
||||||
onOpenMedia,
|
onOpenMedia,
|
||||||
onPatch,
|
onPatch,
|
||||||
onScheduleHighlight: setScheduleHighlightId,
|
onScheduleHighlight: setScheduleHighlightId,
|
||||||
|
onDuplicateFound: setDuplicateRow,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => (
|
const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => (
|
||||||
|
|
@ -150,21 +153,33 @@ export function WoTable({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WoTableShell>
|
<>
|
||||||
<table
|
<WoTableShell>
|
||||||
style={{
|
<table
|
||||||
width: "100%",
|
style={{
|
||||||
minWidth: WO_TABLE_MIN_WIDTH,
|
width: "100%",
|
||||||
borderCollapse: "collapse",
|
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
17
tmp/pr-descriptions/pr8-duplicate-dialog.md
Normal file
17
tmp/pr-descriptions/pr8-duplicate-dialog.md
Normal 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
|
||||||
Loading…
Add table
Reference in a new issue