From 48fc415d54edaf871902866c78ce9d6bffd6ee9b Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Wed, 30 Sep 2026 19:00:55 -0300 Subject: [PATCH] fix(workorders): guard service-linked type changes --- .../tabs/slide-over-info-tab-edit-view.tsx | 69 +++++-- .../detail/tabs/slide-over-info-tab.tsx | 3 + .../detail/work-order-slide-over-tabs.tsx | 3 + .../detail/work-order-slide-over.tsx | 3 + .../list/table/cells/pm-type-cell.tsx | 1 + .../list/table/cells/service-notes-dialog.tsx | 20 ++ .../table/cells/service-registry-status.tsx | 48 +++++ .../list/table/cells/type-cell.tsx | 33 ++- .../list/table/save-existing-row-type.ts | 6 +- .../list/table/wo-table-row-cells.tsx | 91 ++++++-- .../_components/list/table/wo-table-row.tsx | 3 + .../_components/list/table/wo-table.tsx | 4 + .../list/work-orders-list-page-panels.tsx | 1 + .../wizard/wizard-service-notes-fields.tsx | 4 + .../_hooks/use-work-order-service-registry.ts | 6 +- src/app/(protected)/workorders/index.tsx | 1 + .../workorders/aveta-service-notes.test.tsx | 195 ++++++++++++++++++ .../workorders/save-existing-row-type.test.ts | 16 +- ...tab-edit-view-additional-contacts.test.tsx | 5 +- .../use-work-order-service-registry.test.tsx | 49 +++++ 20 files changed, 504 insertions(+), 57 deletions(-) create mode 100644 src/app/(protected)/workorders/_components/list/table/cells/service-registry-status.tsx create mode 100644 src/test/app/(protected)/workorders/use-work-order-service-registry.test.tsx 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 1347384f..1a99666c 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 @@ -37,6 +37,7 @@ type SlideOverInfoTabEditViewProps = { services?: Service[]; serviceHistory?: Service[]; serviceQuery?: ServiceRegistryQueryState; + serviceHistoryQuery?: ServiceRegistryQueryState; serviceHistoryReady?: boolean; allOrders: WorkOrderTableRow[]; onDraftChange: (patch: Partial) => void; @@ -83,6 +84,50 @@ function SlideOverServiceField({ ); } +type SlideOverTypeFieldProps = Pick< + SlideOverInfoTabEditViewProps, + | "draft" + | "services" + | "serviceHistory" + | "serviceHistoryReady" + | "serviceHistoryQuery" + | "onDraftChange" +>; + +function SlideOverTypeField({ + draft, + services = [], + serviceHistory, + serviceHistoryReady = true, + serviceHistoryQuery, + onDraftChange, +}: SlideOverTypeFieldProps) { + const typeServices = serviceHistory ?? services; + const linkedService = typeServices.find( + (service) => String(service.id) === String(draft.serviceId), + ); + const registryUnavailable = draft.serviceId != null && (!serviceHistoryReady || !linkedService); + + return ( + { + if (registryUnavailable) return; + onDraftChange( + draft.serviceId != null && + linkedService != null && + !serviceSupportsWorkOrderType(linkedService, type) + ? { type, severity, serviceId: null, pm: "" } + : { type, severity }, + ); + }} + /> + ); +} + function SlideOverScheduleOnField({ draft, onDraftChange, @@ -164,6 +209,7 @@ export function SlideOverInfoTabEditView({ services = [], serviceHistory, serviceQuery, + serviceHistoryQuery, serviceHistoryReady = true, allOrders, onDraftChange, @@ -213,22 +259,13 @@ export function SlideOverInfoTabEditView({ /> - { - const typeServices = serviceHistory ?? services; - const selectedService = serviceHistoryReady - ? typeServices.find((service) => String(service.id) === String(draft.serviceId)) - : undefined; - onDraftChange( - draft.serviceId != null && - selectedService != null && - !serviceSupportsWorkOrderType(selectedService, type) - ? { type, severity, serviceId: null, pm: "" } - : { type, severity }, - ); - }} + diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab.tsx index 377f1603..d8f1e049 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab.tsx @@ -22,6 +22,7 @@ type SlideOverInfoTabProps = { services?: Service[]; serviceHistory?: Service[]; serviceQuery?: ServiceRegistryQueryState; + serviceHistoryQuery?: ServiceRegistryQueryState; serviceHistoryReady?: boolean; allOrders: WorkOrderTableRow[]; onDraftChange: (patch: Partial) => void; @@ -42,6 +43,7 @@ export function SlideOverInfoTab({ services = [], serviceHistory, serviceQuery, + serviceHistoryQuery, serviceHistoryReady = true, allOrders, onDraftChange, @@ -63,6 +65,7 @@ export function SlideOverInfoTab({ services={services} serviceHistory={serviceHistory} serviceQuery={serviceQuery} + serviceHistoryQuery={serviceHistoryQuery} serviceHistoryReady={serviceHistoryReady} allOrders={allOrders} onDraftChange={onDraftChange} diff --git a/src/app/(protected)/workorders/_components/detail/work-order-slide-over-tabs.tsx b/src/app/(protected)/workorders/_components/detail/work-order-slide-over-tabs.tsx index ec5e611b..e742b0f7 100644 --- a/src/app/(protected)/workorders/_components/detail/work-order-slide-over-tabs.tsx +++ b/src/app/(protected)/workorders/_components/detail/work-order-slide-over-tabs.tsx @@ -30,6 +30,7 @@ type WorkOrderSlideOverTabsProps = { services?: Service[]; serviceHistory?: Service[]; serviceQuery?: ServiceRegistryQueryState; + serviceHistoryQuery?: ServiceRegistryQueryState; serviceHistoryReady?: boolean; allOrders: WorkOrderTableRow[]; commentHighlight?: CommentHighlight | null; @@ -166,6 +167,7 @@ export function WorkOrderSlideOverTabs({ services = [], serviceHistory, serviceQuery, + serviceHistoryQuery, serviceHistoryReady = true, allOrders, commentHighlight, @@ -191,6 +193,7 @@ export function WorkOrderSlideOverTabs({ services={services} serviceHistory={serviceHistory} serviceQuery={serviceQuery} + serviceHistoryQuery={serviceHistoryQuery} serviceHistoryReady={serviceHistoryReady} allOrders={allOrders} onDraftChange={state.handleDraftChange} diff --git a/src/app/(protected)/workorders/_components/detail/work-order-slide-over.tsx b/src/app/(protected)/workorders/_components/detail/work-order-slide-over.tsx index 8a3a97f2..b080915a 100644 --- a/src/app/(protected)/workorders/_components/detail/work-order-slide-over.tsx +++ b/src/app/(protected)/workorders/_components/detail/work-order-slide-over.tsx @@ -33,6 +33,7 @@ type WorkOrderSlideOverProps = { services?: Service[]; serviceHistory?: Service[]; serviceQuery?: ServiceRegistryQueryState; + serviceHistoryQuery?: ServiceRegistryQueryState; serviceHistoryReady?: boolean; allOrders: WorkOrderTableRow[]; onSave: (id: string | number, patch: WorkOrderTablePatch, options?: SlideOverSaveOptions) => void; @@ -59,6 +60,7 @@ export function WorkOrderSlideOver({ services = [], serviceHistory, serviceQuery, + serviceHistoryQuery, serviceHistoryReady = true, allOrders, onSave, @@ -114,6 +116,7 @@ export function WorkOrderSlideOver({ services={services} serviceHistory={serviceHistory} serviceQuery={serviceQuery} + serviceHistoryQuery={serviceHistoryQuery} serviceHistoryReady={serviceHistoryReady} allOrders={allOrders} commentHighlight={commentHighlight} 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 61241964..4581bc96 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 @@ -131,6 +131,7 @@ function PMTypeCellDialog({ pmTypes={props.pmTypes} services={model.assignableServices} serviceId={props.serviceId} + serviceQuery={props.serviceQuery} viewOnly={props.viewOnly ?? false} onSave={props.onSaveDetails ?? (() => undefined)} /> diff --git a/src/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog.tsx index 737c729e..a9ed0377 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog.tsx @@ -5,6 +5,8 @@ import { WizardServiceNotesFields } from "@/app/(protected)/workorders/_componen import { Button } from "@/components/ui/button"; import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop"; import type { Service } from "@/domain/services/types/service"; +import type { ServiceRegistryQueryState } from "@/domain/services/types/service-registry-query-state"; +import { ServiceRegistryStatus } from "./service-registry-status"; export type ServiceNotesDetails = { pm: string; @@ -25,6 +27,7 @@ type ServiceNotesDialogProps = { pmTypes: string[]; services?: Service[]; serviceId?: string | number | null; + serviceQuery?: ServiceRegistryQueryState; viewOnly?: boolean; onSave: (details: ServiceNotesDetails) => void; }; @@ -40,6 +43,7 @@ export function ServiceNotesDialog({ pmTypes, services, serviceId, + serviceQuery, viewOnly = false, onSave, }: ServiceNotesDialogProps) { @@ -71,11 +75,21 @@ export function ServiceNotesDialog({ ] : pmTypes.map((value) => ({ id: value, value })); const saveServiceId = () => { + if (serviceOptionsUnavailable) return serviceId ?? undefined; const selectedId = services?.find((service) => service.name === sel[0])?.id; if (selectedId != null) return selectedId; if (sel[0] === pm) return serviceId ?? undefined; return serviceId == null ? undefined : null; }; + const registryManaged = services !== undefined; + const serviceOptionsUnavailable = + registryManaged && serviceQuery != null && !serviceQuery.hasData; + const serviceOptionsEmpty = + registryManaged && + serviceQuery?.hasData === true && + !serviceQuery.isLoading && + !serviceQuery.isError && + services.length === 0; return (
+ diff --git a/src/app/(protected)/workorders/_components/list/table/cells/service-registry-status.tsx b/src/app/(protected)/workorders/_components/list/table/cells/service-registry-status.tsx new file mode 100644 index 00000000..0d5506ad --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/table/cells/service-registry-status.tsx @@ -0,0 +1,48 @@ +import { QueryErrorPanel } from "@/components/ui/query-error-panel"; +import type { ServiceRegistryQueryState } from "@/domain/services/types/service-registry-query-state"; + +type ServiceRegistryStatusProps = { + query?: ServiceRegistryQueryState; + unavailable?: boolean; + empty?: boolean; + emptyMessage?: string; +}; + +export function ServiceRegistryStatus({ + query, + unavailable = false, + empty = false, + emptyMessage = "No services are available.", +}: ServiceRegistryStatusProps) { + if (query?.isLoading && !query.hasData) { + return
Loading services…
; + } + if (query?.isError) { + return ( +
+ +
+ ); + } + if (query?.isFetching && query.hasData) { + return
Updating services…
; + } + if (empty && query?.hasData) { + return
{emptyMessage}
; + } + if (unavailable) { + return ( +
+ Service history is unavailable. Retry before changing the work-order type. + +
+ ); + } + return null; +} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/type-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/type-cell.tsx index 949722d6..63c056ec 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/type-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/type-cell.tsx @@ -13,12 +13,16 @@ import { HoverChevron } from "./hover-chevron"; import { SeverityBadge } from "./severity-badge"; import { TypeSeverityModal } from "./type-severity-modal"; import { WoTableTypeLabel } from "./wo-table-type-label"; +import type { ServiceRegistryQueryState } from "@/domain/services/types/service-registry-query-state"; +import { ServiceRegistryStatus } from "./service-registry-status"; type TypeCellProps = { value: WOType | ""; severity?: WorkOrderSeverity | null; q?: string; viewOnly?: boolean; + registryUnavailable?: boolean; + serviceHistoryQuery?: ServiceRegistryQueryState; onSave: (type: WOType, severity: WorkOrderSeverity | null) => void; }; @@ -29,7 +33,15 @@ function TypeCellValue({ value, q }: { value: WOType | ""; q?: string }) { return Select type of WO; } -export function TypeCell({ value, severity = null, q, viewOnly = false, onSave }: TypeCellProps) { +export function TypeCell({ + value, + severity = null, + q, + viewOnly = false, + registryUnavailable = false, + serviceHistoryQuery, + onSave, +}: TypeCellProps) { const [anchorEl, setAnchorEl] = useState(null); const [modalOpen, setModalOpen] = useState(false); const [modalSeed, setModalSeed] = useState(value); @@ -65,6 +77,7 @@ export function TypeCell({ value, severity = null, q, viewOnly = false, onSave } <>