refactor(workorders): extract wizard state hook and list overlays

This commit is contained in:
arthur.bassi 2026-08-03 14:36:21 -03:00
parent 1ba7ce0003
commit 41dd7d0957
5 changed files with 264 additions and 167 deletions

View file

@ -0,0 +1,33 @@
import { MediaUploadModal } from "@/app/(protected)/workorders/_components/media/media-upload-modal";
import { EMPTY_MEDIA, type MediaFile } from "@/domain/work-orders/types/work-order-media";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
type WorkOrdersListMediaOverlayProps = {
activeMediaRow: WorkOrderTableRow | null;
onCloseMedia: () => void;
mediaFiles: Record<string, MediaFile[]>;
onMediaChange: (rowId: string | number, files: MediaFile[]) => void;
};
export function WorkOrdersListMediaOverlay({
activeMediaRow,
onCloseMedia,
mediaFiles,
onMediaChange,
}: WorkOrdersListMediaOverlayProps) {
if (activeMediaRow == null) {
return null;
}
return (
<MediaUploadModal
row={activeMediaRow}
open={Boolean(activeMediaRow)}
onOpenChange={(open) => {
if (!open) onCloseMedia();
}}
uploads={mediaFiles[String(activeMediaRow.id)] ?? EMPTY_MEDIA}
onChange={(files) => onMediaChange(activeMediaRow.id, files)}
/>
);
}

View file

@ -0,0 +1,35 @@
import { useNavigate } from "react-router";
import { NewWoWizard } from "@/app/(protected)/workorders/_components/wizard/new-wo-wizard";
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
type WorkOrdersListWizardProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
users: WorkOrderUser[];
rows: WorkOrderTableRow[];
onSelectRow: (rowId: string | number) => void;
};
export function WorkOrdersListWizard({
open,
onOpenChange,
users,
rows,
onSelectRow,
}: WorkOrdersListWizardProps) {
const navigate = useNavigate();
return (
<NewWoWizard
open={open}
onOpenChange={onOpenChange}
users={users}
existingRows={rows}
onViewExistingWo={(row) => {
onSelectRow(row.id);
navigate(`/workorders/${row.id}`);
}}
/>
);
}

View file

@ -1,36 +1,16 @@
import { useEffect, useMemo, useState } from "react";
import { Dialog } from "@mui/material";
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
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 { isWoWizardMockEnabled } from "@/domain/work-orders/mocks/is-wo-wizard-mock-enabled";
import {
getMockWoWizardLocationDetail,
MOCK_WO_WIZARD_LOCATIONS,
MOCK_WO_WIZARD_SERVICES,
MOCK_WO_WIZARD_USERS,
MOCK_WO_WIZARD_VENDORS,
} from "@/domain/work-orders/mocks/wo-wizard-mock-data";
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import {
EMPTY_WIZARD_DRAFT,
WIZARD_STEPS,
type WorkOrderWizardDraft,
} from "@/domain/work-orders/types/work-order-wizard";
import { useLocationDetail } from "@/domain/locations/use-cases/use-location-detail";
import {
useWorkOrderLocations,
useWorkOrderVendors,
} 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 { findDuplicateWoNumberRow } from "@/domain/work-orders/utils/find-duplicate-wo-number-row";
import { WIZARD_STEPS } from "@/domain/work-orders/types/work-order-wizard";
import { WizardFooter } from "@/app/(protected)/workorders/_components/wizard/wizard-footer";
import { WizardSidebar } from "@/app/(protected)/workorders/_components/wizard/wizard-sidebar";
import { WizardStepLocationService } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service";
import { WizardStepTypeSchedule } from "@/app/(protected)/workorders/_components/wizard/wizard-step-type-schedule";
import { WizardStepVendorTime } from "@/app/(protected)/workorders/_components/wizard/wizard-step-vendor-time";
import { useNewWoWizardState } from "@/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state";
type NewWoWizardProps = {
open: boolean;
@ -87,98 +67,7 @@ export function NewWoWizard({
existingRows = [],
onViewExistingWo,
}: NewWoWizardProps) {
const mockFallback = isWoWizardMockEnabled();
const [step, setStep] = useState(1);
const [draft, setDraft] = useState<WorkOrderWizardDraft>({
...EMPTY_WIZARD_DRAFT,
status: "Incomplete",
});
const [pocAutoFilledFor, setPocAutoFilledFor] = useState("");
const [duplicateRow, setDuplicateRow] = useState<WorkOrderTableRow | null>(null);
const { data: apiLocations = [] } = useWorkOrderLocations();
const { data: apiProblems = [] } = useDropdownOptionsByCategory("Problem");
const isMockLocation = draft.locationId.startsWith("mock-");
const { data: apiLocationDetail } = useLocationDetail(
isMockLocation ? undefined : draft.locationId || undefined,
);
const mockLocationDetail = isMockLocation
? getMockWoWizardLocationDetail(draft.locationId)
: undefined;
const locationDetail = mockLocationDetail ?? apiLocationDetail;
const { data: apiVendors = [] } = useWorkOrderVendors(
undefined,
isMockLocation ? undefined : locationDetail?.zipCode,
);
const createMutation = useCreateWorkOrderFromWizard(() => onOpenChange(false));
const wizardUsers = useMemo(() => {
if (users.length > 0) {
return users;
}
return mockFallback ? MOCK_WO_WIZARD_USERS : [];
}, [mockFallback, users]);
const locations =
apiLocations.length > 0 ? apiLocations : mockFallback ? MOCK_WO_WIZARD_LOCATIONS : [];
const problems =
apiProblems.length > 0 ? apiProblems : mockFallback ? MOCK_WO_WIZARD_SERVICES : [];
const vendors = apiVendors.length > 0 ? apiVendors : mockFallback ? MOCK_WO_WIZARD_VENDORS : [];
useEffect(() => {
if (open) {
setStep(1);
setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" });
setPocAutoFilledFor("");
setDuplicateRow(null);
}
}, [open]);
useEffect(() => {
if (!draft.locationId || !locationDetail) {
return;
}
if (pocAutoFilledFor === draft.locationId) {
return;
}
setDraft((current) => ({
...current,
pocName: locationDetail.contact ?? current.pocName,
pocPhone: locationDetail.phone ?? current.pocPhone,
}));
setPocAutoFilledFor(draft.locationId);
}, [draft.locationId, locationDetail, pocAutoFilledFor]);
const setField = <K extends keyof WorkOrderWizardDraft>(
key: K,
value: WorkOrderWizardDraft[K],
) => {
setDraft((current) => ({ ...current, [key]: value }));
};
const patchDraft = (patch: Partial<WorkOrderWizardDraft>) => {
setDraft((current) => ({ ...current, ...patch }));
if (patch.locationId !== undefined && patch.locationId !== pocAutoFilledFor) {
setPocAutoFilledFor("");
}
};
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);
};
const wizard = useNewWoWizardState({ open, users, existingRows, onOpenChange });
return (
<>
@ -203,63 +92,63 @@ export function NewWoWizard({
<span className="sr-only">Guided setup for creating a new work order</span>
<div className="flex min-h-0 flex-1">
<WizardSidebar step={step} draft={draft} />
<WizardSidebar step={wizard.step} draft={wizard.draft} />
<div
className="min-h-0 flex-1 overflow-y-auto"
style={{ padding: 32, background: "var(--color-bg-surface)" }}
>
<WizardStepHeader step={step} />
<WizardStepHeader step={wizard.step} />
{step === 1 && (
{wizard.step === 1 && (
<WizardStepTypeSchedule
draft={draft}
users={wizardUsers}
onUpdate={setField}
onPatch={patchDraft}
onDuplicateWoNumber={handleDuplicateFound}
draft={wizard.draft}
users={wizard.wizardUsers}
onUpdate={wizard.setField}
onPatch={wizard.patchDraft}
onDuplicateWoNumber={wizard.handleDuplicateFound}
/>
)}
{step === 2 && (
{wizard.step === 2 && (
<WizardStepLocationService
draft={draft}
locations={locations}
locationDetail={locationDetail}
services={problems.map((p) => ({ id: p.id, value: p.value }))}
onUpdate={setField}
onPatch={patchDraft}
draft={wizard.draft}
locations={wizard.locations}
locationDetail={wizard.locationDetail}
services={wizard.problems.map((p) => ({ id: p.id, value: p.value }))}
onUpdate={wizard.setField}
onPatch={wizard.patchDraft}
/>
)}
{step === 3 && (
{wizard.step === 3 && (
<WizardStepVendorTime
draft={draft}
vendors={vendors}
onUpdate={setField}
onPatch={patchDraft}
draft={wizard.draft}
vendors={wizard.vendors}
onUpdate={wizard.setField}
onPatch={wizard.patchDraft}
/>
)}
</div>
</div>
<WizardFooter
step={step}
draft={draft}
step={wizard.step}
draft={wizard.draft}
onCancel={() => onOpenChange(false)}
onBack={() => setStep((s) => s - 1)}
onContinue={() => setStep((s) => s + 1)}
onCreate={handleCreate}
isCreating={createMutation.isPending}
onBack={() => wizard.setStep((s) => s - 1)}
onContinue={() => wizard.setStep((s) => s + 1)}
onCreate={wizard.handleCreate}
isCreating={wizard.isCreating}
/>
</Dialog>
<DuplicateWoNumberDialog
open={duplicateRow != null}
existing={duplicateRow}
onDismiss={() => setDuplicateRow(null)}
open={wizard.duplicateRow != null}
existing={wizard.duplicateRow}
onDismiss={() => wizard.setDuplicateRow(null)}
onViewExisting={(row) => {
setDuplicateRow(null);
wizard.setDuplicateRow(null);
onOpenChange(false);
onViewExistingWo?.(row);
}}

View file

@ -0,0 +1,148 @@
import { useEffect, useMemo, useState } from "react";
import { isWoWizardMockEnabled } from "@/domain/work-orders/mocks/is-wo-wizard-mock-enabled";
import {
getMockWoWizardLocationDetail,
MOCK_WO_WIZARD_LOCATIONS,
MOCK_WO_WIZARD_SERVICES,
MOCK_WO_WIZARD_USERS,
MOCK_WO_WIZARD_VENDORS,
} from "@/domain/work-orders/mocks/wo-wizard-mock-data";
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import {
EMPTY_WIZARD_DRAFT,
type WorkOrderWizardDraft,
} from "@/domain/work-orders/types/work-order-wizard";
import { useLocationDetail } from "@/domain/locations/use-cases/use-location-detail";
import { useDropdownOptionsByCategory } from "@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category";
import {
useWorkOrderLocations,
useWorkOrderVendors,
} 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 { findDuplicateWoNumberRow } from "@/domain/work-orders/utils/find-duplicate-wo-number-row";
type UseNewWoWizardStateArgs = {
open: boolean;
users: WorkOrderUser[];
existingRows: WorkOrderTableRow[];
onOpenChange: (open: boolean) => void;
};
export function useNewWoWizardState({
open,
users,
existingRows,
onOpenChange,
}: UseNewWoWizardStateArgs) {
const mockFallback = isWoWizardMockEnabled();
const [step, setStep] = useState(1);
const [draft, setDraft] = useState<WorkOrderWizardDraft>({
...EMPTY_WIZARD_DRAFT,
status: "Incomplete",
});
const [pocAutoFilledFor, setPocAutoFilledFor] = useState("");
const [duplicateRow, setDuplicateRow] = useState<WorkOrderTableRow | null>(null);
const { data: apiLocations = [] } = useWorkOrderLocations();
const { data: apiProblems = [] } = useDropdownOptionsByCategory("Problem");
const isMockLocation = draft.locationId.startsWith("mock-");
const { data: apiLocationDetail } = useLocationDetail(
isMockLocation ? undefined : draft.locationId || undefined,
);
const mockLocationDetail = isMockLocation
? getMockWoWizardLocationDetail(draft.locationId)
: undefined;
const locationDetail = mockLocationDetail ?? apiLocationDetail;
const { data: apiVendors = [] } = useWorkOrderVendors(
undefined,
isMockLocation ? undefined : locationDetail?.zipCode,
);
const createMutation = useCreateWorkOrderFromWizard(() => onOpenChange(false));
const wizardUsers = useMemo(() => {
if (users.length > 0) {
return users;
}
return mockFallback ? MOCK_WO_WIZARD_USERS : [];
}, [mockFallback, users]);
const locations =
apiLocations.length > 0 ? apiLocations : mockFallback ? MOCK_WO_WIZARD_LOCATIONS : [];
const problems =
apiProblems.length > 0 ? apiProblems : mockFallback ? MOCK_WO_WIZARD_SERVICES : [];
const vendors = apiVendors.length > 0 ? apiVendors : mockFallback ? MOCK_WO_WIZARD_VENDORS : [];
useEffect(() => {
if (open) {
setStep(1);
setDraft({ ...EMPTY_WIZARD_DRAFT, status: "Incomplete" });
setPocAutoFilledFor("");
setDuplicateRow(null);
}
}, [open]);
useEffect(() => {
if (!draft.locationId || !locationDetail) {
return;
}
if (pocAutoFilledFor === draft.locationId) {
return;
}
setDraft((current) => ({
...current,
pocName: locationDetail.contact ?? current.pocName,
pocPhone: locationDetail.phone ?? current.pocPhone,
}));
setPocAutoFilledFor(draft.locationId);
}, [draft.locationId, locationDetail, pocAutoFilledFor]);
const setField = <K extends keyof WorkOrderWizardDraft>(
key: K,
value: WorkOrderWizardDraft[K],
) => {
setDraft((current) => ({ ...current, [key]: value }));
};
const patchDraft = (patch: Partial<WorkOrderWizardDraft>) => {
setDraft((current) => ({ ...current, ...patch }));
if (patch.locationId !== undefined && patch.locationId !== pocAutoFilledFor) {
setPocAutoFilledFor("");
}
};
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 {
step,
setStep,
draft,
duplicateRow,
setDuplicateRow,
wizardUsers,
locations,
problems,
vendors,
locationDetail,
setField,
patchDraft,
handleDuplicateFound,
handleCreate,
isCreating: createMutation.isPending,
};
}

View file

@ -3,13 +3,13 @@ import { Box } from "@mui/material";
import { useNavigate } from "react-router";
import { PageHeader } from "@/components/ui/page-header";
import { QueryErrorPanel } from "@/components/ui/query-error-panel";
import { MediaUploadModal } from "@/app/(protected)/workorders/_components/media/media-upload-modal";
import { WorkOrdersListMediaOverlay } from "@/app/(protected)/workorders/_components/list/work-orders-list-media-overlay";
import { WorkOrdersListWizard } from "@/app/(protected)/workorders/_components/list/work-orders-list-wizard";
import { WorkOrdersAdvancedSearchSheet } from "@/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet";
import { WorkOrdersAdvancedFiltersBanner } from "@/app/(protected)/workorders/_components/list/work-orders-advanced-filters-banner";
import { WorkOrdersFilterBar } from "@/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar";
import { WorkOrdersListHeader } from "@/app/(protected)/workorders/_components/list/work-orders-list-header";
import { WoTable } from "@/app/(protected)/workorders/_components/list/table/wo-table";
import { NewWoWizard } from "@/app/(protected)/workorders/_components/wizard/new-wo-wizard";
import { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
import { useWorkOrdersTableData } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-data";
import { useDropdownOptionsByCategory } from "@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category";
@ -18,7 +18,7 @@ import {
useWorkOrderVendors,
} from "@/domain/work-orders/use-cases/use-work-order-detail";
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
import { EMPTY_MEDIA, type MediaFile } from "@/domain/work-orders/types/work-order-media";
import type { MediaFile } from "@/domain/work-orders/types/work-order-media";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
import { useAuthContext } from "@/providers/auth-context";
@ -146,31 +146,23 @@ export default function WorkOrdersListPage() {
vendorTechs={vendorTechs}
/>
<NewWoWizard
<WorkOrdersListWizard
open={wizardOpen}
onOpenChange={setWizardOpen}
users={users}
existingRows={tableData.rows}
onViewExistingWo={(row) => {
tableData.setSelectedId(row.id);
navigate(`/workorders/${row.id}`);
}}
rows={tableData.rows}
onSelectRow={tableData.setSelectedId}
/>
{activeMediaRow != null && (
<MediaUploadModal
row={activeMediaRow}
open={!!activeMediaRow}
onOpenChange={(open) => {
if (!open) setMediaRow(null);
}}
uploads={mediaFiles[String(activeMediaRow.id)] ?? EMPTY_MEDIA}
onChange={(files) => {
setMediaFiles((prev) => ({ ...prev, [String(activeMediaRow.id)]: files }));
patchField(activeMediaRow.id, { mediaCount: files.length });
}}
/>
)}
<WorkOrdersListMediaOverlay
activeMediaRow={activeMediaRow}
onCloseMedia={() => setMediaRow(null)}
mediaFiles={mediaFiles}
onMediaChange={(rowId, files) => {
setMediaFiles((prev) => ({ ...prev, [String(rowId)]: files }));
patchField(rowId, { mediaCount: files.length });
}}
/>
</Box>
);
}