mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
fix(work-orders): show duplicate wo number dialog on wizard create
This commit is contained in:
parent
287517b08f
commit
12fa18fc5e
7 changed files with 152 additions and 9 deletions
|
|
@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||||
import { Dialog } from "@mui/material";
|
import { Dialog } from "@mui/material";
|
||||||
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
||||||
import { Text } from "@/components/ui/text";
|
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 { 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 { isWoWizardMockEnabled } from "@/domain/work-orders/mocks/is-wo-wizard-mock-enabled";
|
||||||
import {
|
import {
|
||||||
|
|
@ -12,6 +13,7 @@ import {
|
||||||
MOCK_WO_WIZARD_VENDORS,
|
MOCK_WO_WIZARD_VENDORS,
|
||||||
} from "@/domain/work-orders/mocks/wo-wizard-mock-data";
|
} from "@/domain/work-orders/mocks/wo-wizard-mock-data";
|
||||||
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import {
|
import {
|
||||||
EMPTY_WIZARD_DRAFT,
|
EMPTY_WIZARD_DRAFT,
|
||||||
WIZARD_STEPS,
|
WIZARD_STEPS,
|
||||||
|
|
@ -23,6 +25,7 @@ import {
|
||||||
useWorkOrderVendors,
|
useWorkOrderVendors,
|
||||||
} from "@/domain/work-orders/use-cases/use-work-order-detail";
|
} 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 { 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 { WizardFooter } from "@/app/(protected)/workorders/_components/wizard/wizard-footer";
|
||||||
import { WizardSidebar } from "@/app/(protected)/workorders/_components/wizard/wizard-sidebar";
|
import { WizardSidebar } from "@/app/(protected)/workorders/_components/wizard/wizard-sidebar";
|
||||||
import { WizardStepLocationService } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service";
|
import { WizardStepLocationService } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service";
|
||||||
|
|
@ -33,6 +36,8 @@ type NewWoWizardProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
users: WorkOrderUser[];
|
users: WorkOrderUser[];
|
||||||
|
existingRows?: WorkOrderTableRow[];
|
||||||
|
onViewExistingWo?: (row: WorkOrderTableRow) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function WizardStepHeader({ step }: { step: number }) {
|
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 mockFallback = isWoWizardMockEnabled();
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [draft, setDraft] = useState<WorkOrderWizardDraft>({
|
const [draft, setDraft] = useState<WorkOrderWizardDraft>({
|
||||||
|
|
@ -83,6 +94,7 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) {
|
||||||
status: "Incomplete",
|
status: "Incomplete",
|
||||||
});
|
});
|
||||||
const [pocAutoFilledFor, setPocAutoFilledFor] = useState("");
|
const [pocAutoFilledFor, setPocAutoFilledFor] = useState("");
|
||||||
|
const [duplicateRow, setDuplicateRow] = useState<WorkOrderTableRow | null>(null);
|
||||||
|
|
||||||
const { data: apiLocations = [] } = useWorkOrderLocations();
|
const { data: apiLocations = [] } = useWorkOrderLocations();
|
||||||
const { data: apiProblems = [] } = useDropdownOptionsByCategory("Problem");
|
const { data: apiProblems = [] } = useDropdownOptionsByCategory("Problem");
|
||||||
|
|
@ -117,6 +129,7 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) {
|
||||||
setStep(1);
|
setStep(1);
|
||||||
setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" });
|
setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" });
|
||||||
setPocAutoFilledFor("");
|
setPocAutoFilledFor("");
|
||||||
|
setDuplicateRow(null);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [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 (
|
return (
|
||||||
<Dialog
|
<>
|
||||||
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={() => onOpenChange(false)}
|
onClose={() => onOpenChange(false)}
|
||||||
maxWidth={false}
|
maxWidth={false}
|
||||||
|
|
@ -187,6 +218,7 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) {
|
||||||
users={wizardUsers}
|
users={wizardUsers}
|
||||||
onUpdate={setField}
|
onUpdate={setField}
|
||||||
onPatch={patchDraft}
|
onPatch={patchDraft}
|
||||||
|
onDuplicateWoNumber={handleDuplicateFound}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -221,6 +253,18 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) {
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
isCreating={createMutation.isPending}
|
isCreating={createMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<DuplicateWoNumberDialog
|
||||||
|
open={duplicateRow != null}
|
||||||
|
existing={duplicateRow}
|
||||||
|
onDismiss={() => setDuplicateRow(null)}
|
||||||
|
onViewExisting={(row) => {
|
||||||
|
setDuplicateRow(null);
|
||||||
|
onOpenChange(false);
|
||||||
|
onViewExistingWo?.(row);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ type WizardStepTypeScheduleProps = {
|
||||||
users: WorkOrderUser[];
|
users: WorkOrderUser[];
|
||||||
onUpdate: <K extends keyof WorkOrderWizardDraft>(key: K, value: WorkOrderWizardDraft[K]) => void;
|
onUpdate: <K extends keyof WorkOrderWizardDraft>(key: K, value: WorkOrderWizardDraft[K]) => void;
|
||||||
onPatch: (patch: Partial<WorkOrderWizardDraft>) => void;
|
onPatch: (patch: Partial<WorkOrderWizardDraft>) => void;
|
||||||
|
onDuplicateWoNumber?: (woNumber: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WizardStepTypeSchedule({
|
export function WizardStepTypeSchedule({
|
||||||
|
|
@ -123,6 +124,7 @@ export function WizardStepTypeSchedule({
|
||||||
users,
|
users,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onPatch,
|
onPatch,
|
||||||
|
onDuplicateWoNumber,
|
||||||
}: WizardStepTypeScheduleProps) {
|
}: WizardStepTypeScheduleProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -137,6 +139,7 @@ export function WizardStepTypeSchedule({
|
||||||
type="text"
|
type="text"
|
||||||
value={draft.woNumber}
|
value={draft.woNumber}
|
||||||
onChange={(e) => onUpdate("woNumber", e.target.value)}
|
onChange={(e) => onUpdate("woNumber", e.target.value)}
|
||||||
|
onBlur={() => onDuplicateWoNumber?.(draft.woNumber)}
|
||||||
placeholder="e.g. 24818"
|
placeholder="e.g. 24818"
|
||||||
maxLength={20}
|
maxLength={20}
|
||||||
className={DLG_INPUT_CLS}
|
className={DLG_INPUT_CLS}
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,16 @@ export default function WorkOrdersListPage() {
|
||||||
vendorTechs={vendorTechs}
|
vendorTechs={vendorTechs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NewWoWizard open={wizardOpen} onOpenChange={setWizardOpen} users={users} />
|
<NewWoWizard
|
||||||
|
open={wizardOpen}
|
||||||
|
onOpenChange={setWizardOpen}
|
||||||
|
users={users}
|
||||||
|
existingRows={tableData.rows}
|
||||||
|
onViewExistingWo={(row) => {
|
||||||
|
tableData.setSelectedId(row.id);
|
||||||
|
navigate(`/workorders/${row.id}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{activeMediaRow != null && (
|
{activeMediaRow != null && (
|
||||||
<MediaUploadModal
|
<MediaUploadModal
|
||||||
|
|
|
||||||
16
src/domain/work-orders/utils/find-duplicate-wo-number-row.ts
Normal file
16
src/domain/work-orders/utils/find-duplicate-wo-number-row.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
export function findDuplicateWoNumberRow(
|
||||||
|
woNumber: string,
|
||||||
|
rows: WorkOrderTableRow[],
|
||||||
|
excludeId?: string | number,
|
||||||
|
): WorkOrderTableRow | undefined {
|
||||||
|
const normalized = woNumber.trim();
|
||||||
|
if (normalized === "") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.find(
|
||||||
|
(row) => row.woNumber === normalized && String(row.id) !== String(excludeId ?? ""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
<NewWoWizard
|
||||||
|
open
|
||||||
|
onOpenChange={vi.fn()}
|
||||||
|
users={[]}
|
||||||
|
existingRows={[existingRow]}
|
||||||
|
onViewExistingWo={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
- Show a confirmation dialog when inline WO number editing detects a duplicate instead of navigating immediately to the existing work order.
|
- 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 slide-over) and **Dismiss** (closes the dialog and keeps the current row editable).
|
- 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; only the post-detection UX is updated.
|
- Duplicate scan logic in `editable-wo-number-cell.tsx` is unchanged; dialog state lives in `wo-table.tsx` under `list/`.
|
||||||
|
|
||||||
## Test plan
|
## Test plan
|
||||||
|
|
||||||
- [ ] Edit a WO number in the table to match an existing row → dialog opens with the duplicate number in the message.
|
- [ ] 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.
|
- [ ] Click **Dismiss** → dialog closes; edited row keeps its original number.
|
||||||
- [ ] Repeat duplicate entry → click **View existing** → navigates to `/workorders/{id}` for the existing row.
|
- [ ] 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
|
## Out of scope
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue