From 41dd7d0957213a520b8a78abdf6a7f2af7c8189a Mon Sep 17 00:00:00 2001 From: "arthur.bassi" Date: Mon, 3 Aug 2026 14:36:21 -0300 Subject: [PATCH] refactor(workorders): extract wizard state hook and list overlays --- .../list/work-orders-list-media-overlay.tsx | 33 ++++ .../list/work-orders-list-wizard.tsx | 35 ++++ .../_components/wizard/new-wo-wizard.tsx | 177 ++++-------------- .../wizard/use-new-wo-wizard-state.ts | 148 +++++++++++++++ src/app/(protected)/workorders/index.tsx | 38 ++-- 5 files changed, 264 insertions(+), 167 deletions(-) create mode 100644 src/app/(protected)/workorders/_components/list/work-orders-list-media-overlay.tsx create mode 100644 src/app/(protected)/workorders/_components/list/work-orders-list-wizard.tsx create mode 100644 src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts diff --git a/src/app/(protected)/workorders/_components/list/work-orders-list-media-overlay.tsx b/src/app/(protected)/workorders/_components/list/work-orders-list-media-overlay.tsx new file mode 100644 index 00000000..cd7fc53b --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/work-orders-list-media-overlay.tsx @@ -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; + onMediaChange: (rowId: string | number, files: MediaFile[]) => void; +}; + +export function WorkOrdersListMediaOverlay({ + activeMediaRow, + onCloseMedia, + mediaFiles, + onMediaChange, +}: WorkOrdersListMediaOverlayProps) { + if (activeMediaRow == null) { + return null; + } + + return ( + { + if (!open) onCloseMedia(); + }} + uploads={mediaFiles[String(activeMediaRow.id)] ?? EMPTY_MEDIA} + onChange={(files) => onMediaChange(activeMediaRow.id, files)} + /> + ); +} diff --git a/src/app/(protected)/workorders/_components/list/work-orders-list-wizard.tsx b/src/app/(protected)/workorders/_components/list/work-orders-list-wizard.tsx new file mode 100644 index 00000000..624d6b09 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/work-orders-list-wizard.tsx @@ -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 ( + { + onSelectRow(row.id); + navigate(`/workorders/${row.id}`); + }} + /> + ); +} diff --git a/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx b/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx index c56a78ee..7b7d5326 100644 --- a/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx +++ b/src/app/(protected)/workorders/_components/wizard/new-wo-wizard.tsx @@ -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({ - ...EMPTY_WIZARD_DRAFT, - status: "Incomplete", - }); - const [pocAutoFilledFor, setPocAutoFilledFor] = useState(""); - const [duplicateRow, setDuplicateRow] = useState(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 = ( - key: K, - value: WorkOrderWizardDraft[K], - ) => { - setDraft((current) => ({ ...current, [key]: value })); - }; - - const patchDraft = (patch: Partial) => { - 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({ Guided setup for creating a new work order
- +
- + - {step === 1 && ( + {wizard.step === 1 && ( )} - {step === 2 && ( + {wizard.step === 2 && ( ({ 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 && ( )}
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} /> setDuplicateRow(null)} + open={wizard.duplicateRow != null} + existing={wizard.duplicateRow} + onDismiss={() => wizard.setDuplicateRow(null)} onViewExisting={(row) => { - setDuplicateRow(null); + wizard.setDuplicateRow(null); onOpenChange(false); onViewExistingWo?.(row); }} diff --git a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts new file mode 100644 index 00000000..3681a3be --- /dev/null +++ b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts @@ -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({ + ...EMPTY_WIZARD_DRAFT, + status: "Incomplete", + }); + const [pocAutoFilledFor, setPocAutoFilledFor] = useState(""); + const [duplicateRow, setDuplicateRow] = useState(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 = ( + key: K, + value: WorkOrderWizardDraft[K], + ) => { + setDraft((current) => ({ ...current, [key]: value })); + }; + + const patchDraft = (patch: Partial) => { + 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, + }; +} diff --git a/src/app/(protected)/workorders/index.tsx b/src/app/(protected)/workorders/index.tsx index d4e70189..965b363b 100644 --- a/src/app/(protected)/workorders/index.tsx +++ b/src/app/(protected)/workorders/index.tsx @@ -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} /> - { - tableData.setSelectedId(row.id); - navigate(`/workorders/${row.id}`); - }} + rows={tableData.rows} + onSelectRow={tableData.setSelectedId} /> - {activeMediaRow != null && ( - { - 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 }); - }} - /> - )} + setMediaRow(null)} + mediaFiles={mediaFiles} + onMediaChange={(rowId, files) => { + setMediaFiles((prev) => ({ ...prev, [String(rowId)]: files })); + patchField(rowId, { mediaCount: files.length }); + }} + /> ); }