diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx index 59abae96..1347384f 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx @@ -44,6 +44,45 @@ type SlideOverInfoTabEditViewProps = { onRequestComplete?: () => void; }; +type SlideOverServiceFieldProps = Pick< + SlideOverInfoTabEditViewProps, + "draft" | "pmTypes" | "services" | "serviceQuery" | "onDraftChange" +>; + +function SlideOverServiceField({ + draft, + pmTypes, + services = [], + serviceQuery, + onDraftChange, +}: SlideOverServiceFieldProps) { + return ( + + onDraftChange(id != null ? { pm, serviceId: id } : { pm })} + onSaveDetails={(details) => + onDraftChange({ + ...serviceIdentityPatch(draft.serviceId, draft.pm, details.pm, details.serviceId), + serviceNotes: details.serviceNotes, + extraServices: details.extraServices, + avetaRequired: details.avetaRequired, + }) + } + /> + + ); +} + function SlideOverScheduleOnField({ draft, onDraftChange, @@ -216,29 +255,13 @@ export function SlideOverInfoTabEditView({ onSave={(apptTime) => onDraftChange({ apptTime })} /> - - onDraftChange(id != null ? { pm, serviceId: id } : { pm })} - onSaveDetails={(details) => - onDraftChange({ - ...serviceIdentityPatch(draft.serviceId, draft.pm, details.pm, details.serviceId), - serviceNotes: details.serviceNotes, - extraServices: details.extraServices, - avetaRequired: details.avetaRequired, - }) - } - /> - + void; }; -export function WorkOrderSlideOverTabs({ +type SecondarySlideOverTabProps = Pick< + WorkOrderSlideOverTabsProps, + "tab" | "state" | "users" | "commentHighlight" | "onOpenCompletionDoc" +>; + +function SecondarySlideOverTabs({ tab, state, - saving, users, - sites, - vendors, - pmTypes, - services = [], - serviceHistory, - serviceQuery, - serviceHistoryReady = true, - allOrders, commentHighlight, - onDuplicateFound, onOpenCompletionDoc, - onRequestComplete, -}: WorkOrderSlideOverTabsProps) { +}: SecondarySlideOverTabProps) { const hasError = Boolean(state.error) && !state.detailForRow; const showContent = Boolean(state.detailForRow || !state.isLoading); const { infoSource, activeDraft } = state; return ( - - {tab === "info" && infoSource != null && activeDraft != null && ( - - )} - + <> {tab === "comments" && ( )} - {tab === "completion" && state.infoSource != null && ( + {tab === "completion" && infoSource != null && ( { - state.uploadMediaRequests(requests); - }} + onUploadMedia={(requests) => state.uploadMediaRequests(requests)} onCategorizeMedia={state.categorizeMedia} onRemoveMedia={state.removeMedia} onRetryMedia={state.retryUpload} @@ -175,14 +145,67 @@ export function WorkOrderSlideOverTabs({ )} isReadOnly={state.isExtrasReadOnly} pending={state.mediaPending} - onUpload={(files) => { - void state.uploadExtraFiles(files); - }} + onUpload={(files) => void state.uploadExtraFiles(files)} onRemove={state.removeMedia} onCategorize={state.categorizeMedia} /> )} + + ); +} + +export function WorkOrderSlideOverTabs({ + tab, + state, + saving, + users, + sites, + vendors, + pmTypes, + services = [], + serviceHistory, + serviceQuery, + serviceHistoryReady = true, + allOrders, + commentHighlight, + onDuplicateFound, + onOpenCompletionDoc, + onRequestComplete, +}: WorkOrderSlideOverTabsProps) { + const { infoSource, activeDraft } = state; + + return ( + + {tab === "info" && infoSource != null && activeDraft != null && ( + + )} + + ); } diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections.tsx index ebc227ea..1d470409 100644 --- a/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections.tsx +++ b/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-sections.tsx @@ -34,6 +34,205 @@ import { QueryErrorPanel } from "@/components/ui/query-error-panel"; import { Text } from "@/components/ui/text"; import { WIZARD_STATUS_DOTS } from "@/domain/work-orders/types/work-order-wizard"; +type FilterActions = Pick; + +function AdvSelectAllRow({ + setMany, + keyName, + values, + selectedCount, +}: { + setMany: AdvancedSearchFilterSectionsProps["setMany"]; + keyName: AdvancedFilterSetKey; + values: string[]; + selectedCount: number; +}) { + return ( +
+ 0} + onClick={() => setMany(keyName, values, selectedCount === 0)} + /> +
+ ); +} + +function AdvancedStatusSection({ + draft, + setDraft, + toggleIn, + setMany, +}: FilterActions & Pick) { + return ( + + +
+ {ALL_WIZARD_STATUSES.map((status) => ( + toggleIn("statuses", status)} + /> + ))} + setDraft((prev) => ({ ...prev, pastDue: !prev.pastDue }))} + /> +
+
+ ); +} + +type AdvancedServiceSectionProps = FilterActions & + Pick; + +function buildServiceFilterOptions( + draft: WorkOrderAdvancedFilters, + services: Service[] | undefined, + pmTypes: string[] | undefined, +) { + const serviceRegistryManaged = services !== undefined; + const serviceList = services ?? []; + const options = serviceRegistryManaged + ? [ + ...serviceList.map((service) => ({ value: service.name, label: service.name })), + ...[...draft.pmTypes] + .filter((name) => !serviceList.some((service) => service.name === name)) + .map((name) => ({ value: name, label: name })), + ] + : (pmTypes ?? []).map((name) => ({ value: name, label: name })); + return { serviceRegistryManaged, serviceList, options }; +} + +function AdvancedServiceRegistryFeedback({ + query, + managed, + services, +}: { + query?: ServiceRegistryQueryState; + managed: boolean; + services: Service[]; +}) { + const showEmpty = + managed && + query?.hasData === true && + !query.isLoading && + !query.isError && + services.length === 0; + + return ( + <> + {Boolean(query?.isLoading && !query.hasData) && ( +
+ + Loading services… + +
+ )} + {Boolean(query?.isFetching && query.hasData && !query.isError) && ( +
+ + Updating services… + +
+ )} + {Boolean(query?.isError) && ( +
+ +
+ )} + {showEmpty && ( + + No services are available. + + )} + + ); +} + +function AdvancedServiceSection({ + draft, + toggleIn, + setMany, + services, + serviceQuery, + pmTypes, +}: AdvancedServiceSectionProps) { + const { serviceRegistryManaged, serviceList, options } = buildServiceFilterOptions( + draft, + services, + pmTypes, + ); + const unavailable = + serviceRegistryManaged && + (serviceQuery?.isLoading === true || + (serviceQuery?.isError === true && serviceQuery.hasData === false)); + + return ( + + + toggleIn("pmTypes", value)} + onBulkToggle={(values, select) => setMany("pmTypes", values, select)} + placeholder="Search service…" + disabled={unavailable} + renderLeft={(name) => ( + + )} + /> + + ); +} + +function AdvancedCompletionDocSection({ draft, toggleIn, setMany }: FilterActions) { + return ( + + +
+ {COMPLETION_DOC_OPTIONS.map((doc) => ( + toggleIn("docs", doc)} + /> + ))} +
+
+ ); +} + type AdvancedSearchFilterSectionsProps = { draft: WorkOrderAdvancedFilters; setDraft: Dispatch>; @@ -59,29 +258,6 @@ export function AdvancedSearchFilterSections({ pmTypes = [], vendorTechs, }: AdvancedSearchFilterSectionsProps) { - const selectAllRow = (key: AdvancedFilterSetKey, all: string[], selectedSize: number) => ( -
- 0} - onClick={() => setMany(key, all, selectedSize === 0)} - /> -
- ); - const serviceRegistryManaged = services !== undefined; - const serviceList = services ?? []; - const serviceOptions = serviceRegistryManaged - ? [ - ...serviceList.map((service) => ({ value: service.name, label: service.name })), - ...[...draft.pmTypes] - .filter((name) => !serviceList.some((service) => service.name === name)) - .map((name) => ({ value: name, label: name })), - ] - : pmTypes.map((name) => ({ value: name, label: name })); - const serviceChoicesUnavailable = - serviceRegistryManaged && - (serviceQuery?.isLoading === true || - (serviceQuery?.isError === true && serviceQuery.hasData === false)); - return (
@@ -131,83 +307,23 @@ export function AdvancedSearchFilterSections({ /> - - {selectAllRow("statuses", [...ALL_WIZARD_STATUSES], draft.statuses.size)} -
- {ALL_WIZARD_STATUSES.map((status) => ( - toggleIn("statuses", status)} - /> - ))} - setDraft((prev) => ({ ...prev, pastDue: !prev.pastDue }))} - /> -
-
+ - - {Boolean(serviceQuery?.isLoading && !serviceQuery.hasData) && ( -
- - Loading services… - -
- )} - {Boolean(serviceQuery?.isFetching && serviceQuery.hasData && !serviceQuery.isError) && ( -
- - Updating services… - -
- )} - {Boolean(serviceQuery?.isError) && ( -
- -
- )} - {Boolean( - serviceRegistryManaged && - serviceQuery?.hasData && - !serviceQuery.isLoading && - !serviceQuery.isError && - serviceList.length === 0, - ) && ( - - No services are available. - - )} - toggleIn("pmTypes", value)} - onBulkToggle={(values, select) => setMany("pmTypes", values, select)} - placeholder="Search service…" - disabled={serviceChoicesUnavailable} - renderLeft={(name) => ( - - )} - /> -
+ - - {selectAllRow("docs", [...COMPLETION_DOC_OPTIONS], draft.docs.size)} -
- {COMPLETION_DOC_OPTIONS.map((doc) => ( - toggleIn("docs", doc)} - /> - ))} -
-
+ String(site.id) === String(draft.locationId)); + const locationName = draft.location || draft.site || location?.name || ""; + return { + locationName, + siteCode: location ? resolveSiteCode(location) : resolveSiteCode({ name: locationName }), + }; +} + +function resolveInlineDraftSchedule(draft: WorkOrderTableDraft) { + const hasWeekTarget = Boolean(draft.targetWeek?.trim()); + const scheduleWeekOnly = draft.scheduleWeekOnly && (hasWeekTarget || Boolean(draft.scheduledOn)); + return { + scheduleWeekOnly, + status: resolveCreationStatus({ + status: draft.status || "Incomplete", + scheduledOn: draft.scheduledOn, + scheduleWeekOnly, + }), + }; +} + export function buildInlineDraftCreateInput( draft: WorkOrderTableDraft, users: WorkOrderUser[], sites: LocationOption[], ): WorkOrderWizardDraft { const assignee = users.find((u) => String(u.id) === draft.dispatcherId); - const location = sites.find((s) => String(s.id) === String(draft.locationId)); - const locationName = draft.location || draft.site || location?.name || ""; - - const hasWeekTarget = Boolean(draft.targetWeek?.trim()); - const scheduleWeekOnly = draft.scheduleWeekOnly && (hasWeekTarget || Boolean(draft.scheduledOn)); + const location = resolveInlineDraftLocation(draft, sites); + const schedule = resolveInlineDraftSchedule(draft); return { locationId: String(draft.locationId || ""), - locationName, - siteCode: location ? resolveSiteCode(location) : resolveSiteCode({ name: locationName }), + locationName: location.locationName, + siteCode: location.siteCode, woNumber: draft.woNumber, type: draft.type || "PM", severity: draft.severity, @@ -39,16 +58,12 @@ export function buildInlineDraftCreateInput( vendorNotes: "", apptStart: draft.apptStart, apptEnd: draft.apptEnd, - status: resolveCreationStatus({ - status: draft.status || "Incomplete", - scheduledOn: draft.scheduledOn, - scheduleWeekOnly, - }), + status: schedule.status, pocName: draft.pocName, pocPhone: draft.pocPhone, pocNotes: draft.pocNotes, additionalContacts: draft.additionalContacts ?? [], - scheduleWeekOnly, + scheduleWeekOnly: schedule.scheduleWeekOnly, targetWeek: draft.targetWeek || "", scheduleEnd: draft.scheduleEnd || "", schedMode: draft.schedMode, diff --git a/src/app/(protected)/workorders/_components/list/table/cells/pm-type-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/pm-type-cell.tsx index df9a2c04..61241964 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/pm-type-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/pm-type-cell.tsx @@ -36,183 +36,277 @@ type PMTypeCellProps = { serviceQuery?: ServiceRegistryQueryState; }; -export function PMTypeCell({ - value, - pmTypes, - q, - onSave, - serviceNotes = "", - extraServices = [], - avetaRequired = false, - avetaAttached = false, - woNumber, - onSaveDetails, - viewOnly = false, - services, - serviceId, - workOrderType = "", - serviceQuery, -}: PMTypeCellProps) { - const [anchorEl, setAnchorEl] = useState(null); - const [search, setSearch] = useState(""); - const [modalOpen, setModalOpen] = useState(false); +type PMTypeCellModel = { + assignableServices?: Service[]; + displayValue: string; + filtered: string[]; + registryManaged: boolean; + choicesUnavailable: boolean; + catalogEmpty: boolean; + hasDetails: boolean; + notePreview: string; +}; - const assignableServices = - services == null ? undefined : servicesForWorkOrderType(services, workOrderType); - const serviceNames = assignableServices?.map((service) => service.name) ?? pmTypes; - const filtered = search - ? serviceNames.filter((p) => p.toLowerCase().includes(search.toLowerCase())) - : serviceNames; - const linkedService = services?.find((service) => String(service.id) === String(serviceId)); - const displayValue = !viewOnly && linkedService?.isActive ? linkedService.name : value; - const serviceRegistryManaged = services !== undefined; - const serviceChoicesUnavailable = - serviceRegistryManaged && - (serviceQuery?.isLoading === true || - (serviceQuery?.isError === true && serviceQuery.hasData === false)); - const serviceCatalogEmpty = - serviceRegistryManaged && - (serviceQuery?.hasData ?? true) && - !serviceQuery?.isLoading && - !serviceQuery?.isError && - serviceNames.length === 0; +function getAssignableServices(props: PMTypeCellProps) { + return props.services == null + ? undefined + : servicesForWorkOrderType(props.services, props.workOrderType ?? ""); +} - const extras = extraServices ?? []; - const hasDetails = hasServiceNotesDetails(serviceNotes, extras, avetaRequired); - const notePreview = serviceNotes?.trim() ? truncateServiceNotesPreview(serviceNotes) : ""; +function getCellDisplayValue(props: PMTypeCellProps) { + const linkedService = props.services?.find( + (service) => String(service.id) === String(props.serviceId), + ); + return !props.viewOnly && linkedService?.isActive ? linkedService.name : props.value; +} - const closePopover = () => { - setAnchorEl(null); - setSearch(""); +function getServiceChoicesUnavailable(props: PMTypeCellProps, registryManaged: boolean) { + const query = props.serviceQuery; + return ( + registryManaged && + (query?.isLoading === true || (query?.isError === true && query.hasData === false)) + ); +} + +function isServiceCatalogEmpty( + props: PMTypeCellProps, + registryManaged: boolean, + services: Service[] | undefined, +) { + const query = props.serviceQuery; + return ( + registryManaged && + (query?.hasData ?? true) && + !query?.isLoading && + !query?.isError && + (services?.length ?? props.pmTypes.length) === 0 + ); +} + +function resolvePMTypeCellModel(props: PMTypeCellProps, search: string): PMTypeCellModel { + const assignableServices = getAssignableServices(props); + const serviceNames = assignableServices?.map((service) => service.name) ?? props.pmTypes; + const registryManaged = props.services !== undefined; + + return { + assignableServices, + displayValue: getCellDisplayValue(props), + filtered: search + ? serviceNames.filter((name) => name.toLowerCase().includes(search.toLowerCase())) + : serviceNames, + registryManaged, + choicesUnavailable: getServiceChoicesUnavailable(props, registryManaged), + catalogEmpty: isServiceCatalogEmpty(props, registryManaged, assignableServices), + hasDetails: hasServiceNotesDetails( + props.serviceNotes ?? "", + props.extraServices ?? [], + props.avetaRequired ?? false, + ), + notePreview: props.serviceNotes?.trim() ? truncateServiceNotesPreview(props.serviceNotes) : "", }; +} - const modal = - onSaveDetails != null || viewOnly ? ( - undefined)} +function PMTypeCellDialog({ + props, + model, + open, + onOpenChange, +}: { + props: PMTypeCellProps; + model: PMTypeCellModel; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + if (!props.viewOnly && props.onSaveDetails == null) return null; + + return ( + undefined)} + /> + ); +} + +function PMTypeCellReadOnly({ + props, + model, + onOpenDetails, +}: { + props: PMTypeCellProps; + model: PMTypeCellModel; + onOpenDetails: () => void; +}) { + return ( + - {modal} - - ); - } + + + ) : ( + — + )} + {Boolean(props.avetaRequired) && } + + ); +} - if (hasDetails && onSaveDetails) { - return ( - <> - setModalOpen(true)} - /> - {modal} - - ); - } +function PMTypeCellDetails({ + props, + model, + onOpenDetails, +}: { + props: PMTypeCellProps; + model: PMTypeCellModel; + onOpenDetails: () => void; +}) { + return ( + + ); +} + +function PMTypeCellPicker({ + props, + model, + anchorEl, + search, + onOpenPicker, + onSearchChange, + onClose, + onOpenDetails, +}: { + props: PMTypeCellProps; + model: PMTypeCellModel; + anchorEl: HTMLElement | null; + search: string; + onOpenPicker: (anchor: HTMLElement) => void; + onSearchChange: (search: string) => void; + onClose: () => void; + onOpenDetails: () => void; +}) { + const onPick = (name: string) => { + const service = model.assignableServices?.find((candidate) => candidate.name === name); + props.onSave(name, service?.id); + onClose(); + }; return ( <> - { - const picked = assignableServices?.find((service) => service.name === pm); - onSave(pm, picked?.id); - closePopover(); - }} + filtered={model.filtered} + value={model.displayValue} + showAddDetails={Boolean(props.onSaveDetails)} + onSearchChange={onSearchChange} + onClose={onClose} + registryManaged={model.registryManaged} + loading={props.serviceQuery?.isLoading === true && !props.serviceQuery.hasData} + fetching={props.serviceQuery?.isFetching === true && props.serviceQuery.hasData} + error={props.serviceQuery?.isError === true} + empty={model.catalogEmpty} + selectionDisabled={model.choicesUnavailable} + onRetry={props.serviceQuery?.retry} + onPick={onPick} onAddDetails={() => { - closePopover(); - setModalOpen(true); + onClose(); + onOpenDetails(); }} /> - {modal} + + ); +} + +export function PMTypeCell(props: PMTypeCellProps) { + const [anchorEl, setAnchorEl] = useState(null); + const [search, setSearch] = useState(""); + const [modalOpen, setModalOpen] = useState(false); + const model = resolvePMTypeCellModel(props, search); + const closePicker = () => { + setAnchorEl(null); + setSearch(""); + }; + const openDetails = () => setModalOpen(true); + const content = props.viewOnly ? ( + + ) : model.hasDetails && props.onSaveDetails ? ( + + ) : ( + + ); + + return ( + <> + {content} + ); } diff --git a/src/app/(protected)/workorders/_components/list/table/save-existing-row-type.ts b/src/app/(protected)/workorders/_components/list/table/save-existing-row-type.ts index 53271e82..ef6cf584 100644 --- a/src/app/(protected)/workorders/_components/list/table/save-existing-row-type.ts +++ b/src/app/(protected)/workorders/_components/list/table/save-existing-row-type.ts @@ -18,14 +18,23 @@ type PersistPatchFn = ( options?: { onSuccess?: () => void }, ) => void; -export function saveExistingRowType( - onPatch: PersistPatchFn, - row: Pick, - type: WOType, - severity: WorkOrderSeverity | null, - serviceHistory: Service[] = [], +type SaveExistingRowTypeOptions = { + onPatch: PersistPatchFn; + row: Pick; + type: WOType; + severity: WorkOrderSeverity | null; + serviceHistory?: Service[]; + serviceHistoryReady?: boolean; +}; + +export function saveExistingRowType({ + onPatch, + row, + type, + severity, + serviceHistory = [], serviceHistoryReady = true, -) { +}: SaveExistingRowTypeOptions) { const selectedService = serviceHistoryReady ? serviceHistory.find((service) => String(service.id) === String(row.serviceId)) : undefined; diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx index 0a2ca545..b04eba58 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row-cells.tsx @@ -127,14 +127,14 @@ export function WoTableRowCells({ q={search} viewOnly={isWorkOrderCompletionSurfaceReadOnly(row.status)} onSave={(t, sev) => - saveExistingRowType( - handlers.onPatch, + saveExistingRowType({ + onPatch: handlers.onPatch, row, - t, - sev, - serviceHistory ?? services, + type: t, + severity: sev, + serviceHistory: serviceHistory ?? services, serviceHistoryReady, - ) + }) } /> diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx index 063f193b..4de3a8ee 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table.tsx @@ -93,6 +93,112 @@ function WoTablePager({ tableData }: { tableData: ReturnTypeOfUseWorkOrdersTable ); } +type WoTableRowRenderProps = { + row: WorkOrderTableRow; + allRows: WorkOrderTableRow[]; + users: WorkOrderUser[]; + sites: LocationOption[]; + sitePlace?: { city: string; state: string }; + vendors: VendorDropdownItem[]; + services: Service[]; + serviceHistory: Service[]; + serviceQuery?: ServiceRegistryQueryState; + serviceHistoryReady: boolean; + pmTypes: string[]; + search: string; + forceScheduleHighlight: boolean; + forceDueDateHighlight: boolean; + isSelected: boolean; + isFlashing: boolean; + dragViz: ReturnTypeOfUseWorkOrdersTableData["dragViz"]; + handlers: ReturnType; +}; + +function WoTableRenderedRow(props: WoTableRowRenderProps) { + return ; +} + +type WoTableInlineRenderProps = { + tableData: ReturnTypeOfUseWorkOrdersTableData; + dayKey: string; + users: WorkOrderUser[]; + sites: LocationOption[]; + sitePlace?: { city: string; state: string }; + vendors: VendorDropdownItem[]; + services: Service[]; + serviceQuery?: ServiceRegistryQueryState; + pmTypes: string[]; + saving: boolean; + handleInlineSave: () => void; + onDuplicateFound: (row: WorkOrderTableRow) => void; +}; + +function WoTableRenderedInlineRow({ + tableData, + dayKey, + users, + sites, + sitePlace, + vendors, + services, + serviceQuery, + pmTypes, + saving, + handleInlineSave, + onDuplicateFound, +}: WoTableInlineRenderProps) { + if (tableData.draftDayKey !== dayKey) return null; + return ( + + ); +} + +function WoTableEmptyResult({ + tableData, + onOpenAdvancedFilters, + onClearSearch, +}: { + tableData: ReturnTypeOfUseWorkOrdersTableData; + onOpenAdvancedFilters?: () => void; + onClearSearch?: () => void; +}) { + if (tableData.advActive && tableData.scopeTotalCount === 0) { + return ( + + ); + } + if (!tableData.advActive && tableData.noSearchResults) { + return ( + + ); + } + return null; +} + export function WoTable({ tableData, users, @@ -141,7 +247,7 @@ export function WoTable({ }); const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => ( - ); - const renderInlineRow = (dayKey: string) => - tableData.draftDayKey === dayKey && ( - - ); + const renderInlineRow = (dayKey: string) => ( + + ); if (tableData.isLoading) { return ( @@ -197,29 +299,14 @@ export function WoTable({ ); } - const emptyAdv = tableData.advActive && tableData.scopeTotalCount === 0; - - if (emptyAdv) { - return ( - - ); - } - - const showSearchEmpty = !tableData.advActive && tableData.noSearchResults; - - if (showSearchEmpty) { - return ( - - ); - } + const emptyResult = ( + + ); + if (emptyResult != null) return emptyResult; return ( <> diff --git a/src/app/(protected)/workorders/_hooks/use-work-order-service-registry.ts b/src/app/(protected)/workorders/_hooks/use-work-order-service-registry.ts new file mode 100644 index 00000000..a294ae7b --- /dev/null +++ b/src/app/(protected)/workorders/_hooks/use-work-order-service-registry.ts @@ -0,0 +1,39 @@ +import type { UseQueryResult } from "@tanstack/react-query"; +import type { Service } from "@/domain/services/types/service"; +import type { ServiceRegistryQueryState } from "@/domain/services/types/service-registry-query-state"; +import { + useActiveServices, + useAllServices, +} from "@/domain/services/use-cases/use-services-for-work-order-type"; + +function toServiceRegistryQueryState( + query: UseQueryResult, +): ServiceRegistryQueryState { + return { + isLoading: query.isPending && query.data === undefined, + isError: query.isError, + isFetching: query.isFetching, + hasData: query.data !== undefined, + retry: () => void query.refetch(), + }; +} + +export function useWorkOrderServiceRegistry() { + const activeQuery = useActiveServices(); + const historyQuery = useAllServices(); + const servicePickerState = toServiceRegistryQueryState(activeQuery); + const serviceHistoryState = toServiceRegistryQueryState(historyQuery); + const serviceHistoryReadyForTypeChange = + serviceHistoryState.hasData && + !serviceHistoryState.isLoading && + !serviceHistoryState.isError && + !serviceHistoryState.isFetching; + + return { + services: activeQuery.data ?? [], + serviceHistory: historyQuery.data ?? [], + servicePickerState, + serviceHistoryState, + serviceHistoryReadyForTypeChange, + }; +} diff --git a/src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts b/src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts index 1cbc0b2d..87987653 100644 --- a/src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts +++ b/src/app/(protected)/workorders/_hooks/use-work-orders-list-page.ts @@ -9,11 +9,6 @@ import { } from "@/domain/work-orders/use-cases/use-work-order-detail"; import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list"; import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations"; -import { - useActiveServices, - useAllServices, -} from "@/domain/services/use-cases/use-services-for-work-order-type"; -import type { ServiceRegistryQueryState } from "@/domain/services/types/service-registry-query-state"; import { shouldCompleteAfterCompletionDoc } from "@/domain/work-orders/utils/completion-doc-auto-complete"; import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link"; import { @@ -23,6 +18,7 @@ import { import { guardDialogClose } from "@/lib/dialog-guard"; import { useWorkOrdersListFilters } from "./use-work-orders-list-filters"; import { useWorkOrdersTableData } from "./use-work-orders-table-data"; +import { useWorkOrderServiceRegistry } from "./use-work-order-service-registry"; function useSlideOverOpenState() { const [slideOverRow, setSlideOverRow] = useState(null); @@ -96,29 +92,7 @@ export function useWorkOrdersListPage() { const { data: locations = [] } = useWorkOrderLocations(); const { data: vendors = [] } = useWorkOrderVendors(); const { data: problemOptions = [] } = useDropdownOptionsByCategory("Problem"); - const activeServicesQuery = useActiveServices(); - const serviceHistoryQuery = useAllServices(); - const services = activeServicesQuery.data ?? []; - const serviceHistory = serviceHistoryQuery.data ?? []; - const servicePickerState: ServiceRegistryQueryState = { - isLoading: activeServicesQuery.isPending && activeServicesQuery.data === undefined, - isError: activeServicesQuery.isError, - isFetching: activeServicesQuery.isFetching, - hasData: activeServicesQuery.data !== undefined, - retry: () => void activeServicesQuery.refetch(), - }; - const serviceHistoryState: ServiceRegistryQueryState = { - isLoading: serviceHistoryQuery.isPending && serviceHistoryQuery.data === undefined, - isError: serviceHistoryQuery.isError, - isFetching: serviceHistoryQuery.isFetching, - hasData: serviceHistoryQuery.data !== undefined, - retry: () => void serviceHistoryQuery.refetch(), - }; - const serviceHistoryReadyForTypeChange = - serviceHistoryState.hasData && - !serviceHistoryState.isLoading && - !serviceHistoryState.isError && - !serviceHistoryState.isFetching; + const serviceRegistry = useWorkOrderServiceRegistry(); const pmTypes = useMemo( () => problemOptions.map((option) => option.value).filter(Boolean), @@ -223,11 +197,7 @@ export function useWorkOrdersListPage() { locations, vendors, pmTypes, - services, - serviceHistory, - servicePickerState, - serviceHistoryState, - serviceHistoryReadyForTypeChange, + ...serviceRegistry, vendorTechs, activeSlideOverRow, activeDocRow, diff --git a/src/test/app/(protected)/workorders/save-existing-row-type.test.ts b/src/test/app/(protected)/workorders/save-existing-row-type.test.ts index 4ec3a799..5f199fa9 100644 --- a/src/test/app/(protected)/workorders/save-existing-row-type.test.ts +++ b/src/test/app/(protected)/workorders/save-existing-row-type.test.ts @@ -19,7 +19,7 @@ describe("saveExistingRowType", () => { it("warns when severity newly enters SEV 1 only after persist onSuccess", () => { const onPatch = vi.fn(); - saveExistingRowType(onPatch, row, "Emergency", 1); + saveExistingRowType({ onPatch, row, type: "Emergency", severity: 1 }); expect(toastWarn).not.toHaveBeenCalled(); expect(onPatch).toHaveBeenCalledWith( @@ -36,7 +36,7 @@ describe("saveExistingRowType", () => { it("does not warn when staying on a critical severity", () => { const onPatch = vi.fn(); - saveExistingRowType(onPatch, { ...row, severity: 1 }, "Emergency", 1); + saveExistingRowType({ onPatch, row: { ...row, severity: 1 }, type: "Emergency", severity: 1 }); expect(toastWarn).not.toHaveBeenCalled(); expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", severity: 1 }); @@ -46,7 +46,7 @@ describe("saveExistingRowType", () => { it("does not warn when moving between SEV 3–5", () => { const onPatch = vi.fn(); - saveExistingRowType(onPatch, { ...row, severity: 5 }, "Reactive", 3); + saveExistingRowType({ onPatch, row: { ...row, severity: 5 }, type: "Reactive", severity: 3 }); expect(toastWarn).not.toHaveBeenCalled(); expect(onPatch).toHaveBeenCalledWith(7, { type: "Reactive", severity: 3 }); @@ -56,7 +56,7 @@ describe("saveExistingRowType", () => { it("does not warn when clearing severity on a PM correction", () => { const onPatch = vi.fn(); - saveExistingRowType(onPatch, { ...row, severity: 2 }, "PM", null); + saveExistingRowType({ onPatch, row: { ...row, severity: 2 }, type: "PM", severity: null }); expect(toastWarn).not.toHaveBeenCalled(); expect(onPatch).toHaveBeenCalledWith(7, { type: "PM", severity: null }); @@ -78,7 +78,13 @@ describe("saveExistingRowType", () => { }, ]; - saveExistingRowType(onPatch, { ...row, serviceId: 701 }, "Emergency", null, services); + saveExistingRowType({ + onPatch, + row: { ...row, serviceId: 701 }, + type: "Emergency", + severity: null, + serviceHistory: services, + }); expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", @@ -103,7 +109,13 @@ describe("saveExistingRowType", () => { }, ]; - saveExistingRowType(onPatch, { ...row, serviceId: 702 }, "Reactive", null, services); + saveExistingRowType({ + onPatch, + row: { ...row, serviceId: 702 }, + type: "Reactive", + severity: null, + serviceHistory: services, + }); expect(onPatch).toHaveBeenCalledWith(7, { type: "Reactive", severity: null }); }); @@ -123,7 +135,13 @@ describe("saveExistingRowType", () => { }, ]; - saveExistingRowType(onPatch, { ...row, serviceId: 701 }, "Emergency", null, services); + saveExistingRowType({ + onPatch, + row: { ...row, serviceId: 701 }, + type: "Emergency", + severity: null, + serviceHistory: services, + }); expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", @@ -146,14 +164,14 @@ describe("saveExistingRowType", () => { supportedWorkOrderTypes: ["PM"], }; - saveExistingRowType( + saveExistingRowType({ onPatch, - { ...row, serviceId: 701 }, - "Emergency", - null, - [inactiveService], - false, - ); + row: { ...row, serviceId: 701 }, + type: "Emergency", + severity: null, + serviceHistory: [inactiveService], + serviceHistoryReady: false, + }); expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", severity: null }); });