diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx index fe4a798c..f7eef6e6 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-display-view.tsx @@ -12,6 +12,7 @@ import type { } from "@/domain/work-orders/types/work-order"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; +import type { Service } from "@/domain/services/types/service"; import { formatApptRange, parseApptTime } from "@/domain/work-orders/utils/wizard-date-utils"; import { effectiveCompany, @@ -31,6 +32,7 @@ type SlideOverInfoTabDisplayViewProps = { sites: LocationOption[]; vendors: VendorDropdownItem[]; pmTypes: string[]; + services?: Service[]; }; type FrozenPoc = ReturnType; @@ -115,14 +117,22 @@ function SlideOverSiteField({ return ; } -function SlideOverOpenService({ source }: { source: WorkOrderTableRow }) { +function SlideOverOpenService({ + source, + services, +}: { + source: WorkOrderTableRow; + services: Service[]; +}) { if (!source.pm) { return —; } + const name = + services.find((service) => String(service.id) === String(source.serviceId))?.name ?? source.pm; return ( - - {source.pm} + + {name} {Boolean(source.avetaRequired) && ( )} @@ -134,16 +144,21 @@ function SlideOverServiceField({ source, isCompleted, pmTypes, + services, }: { source: WorkOrderTableRow; isCompleted: boolean; pmTypes: string[]; + services: Service[]; }) { - if (!isCompleted) return ; + if (!isCompleted) return ; return ( {formatApptDisplay(source.apptTime)} - + 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 2d718b4f..b7b7c0e3 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 @@ -16,6 +16,9 @@ import type { } from "@/domain/work-orders/types/work-order"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard"; +import type { Service } from "@/domain/services/types/service"; +import { isServiceEligibleForWorkOrderType } from "@/domain/services/utils/services-for-work-order-type"; +import { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch"; import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch"; import { DocBadge } from "./slide-over-doc-badge"; @@ -30,6 +33,7 @@ type SlideOverInfoTabEditViewProps = { sites: LocationOption[]; vendors: VendorDropdownItem[]; pmTypes: string[]; + services?: Service[]; allOrders: WorkOrderTableRow[]; onDraftChange: (patch: Partial) => void; onDuplicateFound?: (existing: WorkOrderTableRow) => void; @@ -114,6 +118,7 @@ export function SlideOverInfoTabEditView({ sites, vendors, pmTypes, + services = [], allOrders, onDraftChange, onDuplicateFound, @@ -165,7 +170,18 @@ export function SlideOverInfoTabEditView({ onDraftChange({ type, severity })} + onSave={(type, severity) => { + const selectedService = services.find( + (service) => String(service.id) === String(draft.serviceId), + ); + onDraftChange( + draft.serviceId != null && + selectedService != null && + !isServiceEligibleForWorkOrderType(selectedService, type) + ? { type, severity, serviceId: null, pm: "" } + : { type, severity }, + ); + }} /> @@ -196,14 +212,17 @@ export function SlideOverInfoTabEditView({ onDraftChange({ pm })} + onSave={(pm, id) => onDraftChange(id != null ? { pm, serviceId: id } : { pm })} onSaveDetails={(details) => onDraftChange({ - pm: details.pm, + ...serviceIdentityPatch(draft.serviceId, draft.pm, details.pm, details.serviceId), serviceNotes: details.serviceNotes, extraServices: details.extraServices, avetaRequired: details.avetaRequired, 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 a9a186f2..f8a63799 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 @@ -4,6 +4,7 @@ import type { WorkOrderUser, } from "@/domain/work-orders/types/work-order"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; +import type { Service } from "@/domain/services/types/service"; import { SlideOverInfoTabDisplayView } from "./slide-over-info-tab-display-view"; import { SlideOverInfoTabEditView } from "./slide-over-info-tab-edit-view"; @@ -17,6 +18,7 @@ type SlideOverInfoTabProps = { sites: LocationOption[]; vendors: VendorDropdownItem[]; pmTypes: string[]; + services?: Service[]; allOrders: WorkOrderTableRow[]; onDraftChange: (patch: Partial) => void; onDuplicateFound?: (existing: WorkOrderTableRow) => void; @@ -33,6 +35,7 @@ export function SlideOverInfoTab({ sites, vendors, pmTypes, + services = [], allOrders, onDraftChange, onDuplicateFound, @@ -50,6 +53,7 @@ export function SlideOverInfoTab({ sites={sites} vendors={vendors} pmTypes={pmTypes} + services={services} allOrders={allOrders} onDraftChange={onDraftChange} onDuplicateFound={onDuplicateFound} @@ -67,6 +71,7 @@ export function SlideOverInfoTab({ sites={sites} vendors={vendors} pmTypes={pmTypes} + services={services} /> ); } 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 12d015fa..1b54d82a 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 @@ -12,6 +12,7 @@ import type { import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { resolveSlideOverAvetaRequired } from "@/domain/work-orders/utils/resolve-slide-over-aveta-required"; import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link"; +import type { Service } from "@/domain/services/types/service"; import type { useWorkOrderSlideOver, SlideOverTab } from "./use-work-order-slide-over"; import { WorkOrderSlideOverTabPanel } from "./work-order-slide-over-tab-panel"; @@ -25,6 +26,7 @@ type WorkOrderSlideOverTabsProps = { sites: LocationOption[]; vendors: VendorDropdownItem[]; pmTypes: string[]; + services?: Service[]; allOrders: WorkOrderTableRow[]; commentHighlight?: CommentHighlight | null; onDuplicateFound?: (existing: WorkOrderTableRow) => void; @@ -40,6 +42,7 @@ export function WorkOrderSlideOverTabs({ sites, vendors, pmTypes, + services = [], allOrders, commentHighlight, onDuplicateFound, @@ -63,6 +66,7 @@ export function WorkOrderSlideOverTabs({ sites={sites} vendors={vendors} pmTypes={pmTypes} + services={services} allOrders={allOrders} onDraftChange={state.handleDraftChange} onDuplicateFound={onDuplicateFound} 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 762d5aa4..d59009fa 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 @@ -12,6 +12,7 @@ import type { WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link"; +import type { Service } from "@/domain/services/types/service"; import { useWorkOrderSlideOver, type SlideOverTab } from "./use-work-order-slide-over"; import { WorkOrderSlideOverTabNav } from "./work-order-slide-over-tab-nav"; import { WorkOrderSlideOverTabs } from "./work-order-slide-over-tabs"; @@ -28,6 +29,7 @@ type WorkOrderSlideOverProps = { sites: LocationOption[]; vendors: VendorDropdownItem[]; pmTypes: string[]; + services?: Service[]; allOrders: WorkOrderTableRow[]; onSave: (id: string | number, patch: WorkOrderTablePatch, options?: SlideOverSaveOptions) => void; onOpenCompletionDoc: (row: WorkOrderTableRow) => void; @@ -50,6 +52,7 @@ export function WorkOrderSlideOver({ sites, vendors, pmTypes, + services = [], allOrders, onSave, onOpenCompletionDoc, @@ -101,6 +104,7 @@ export function WorkOrderSlideOver({ sites={sites} vendors={vendors} pmTypes={pmTypes} + services={services} allOrders={allOrders} commentHighlight={commentHighlight} onDuplicateFound={onDuplicateFound} 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 148aeee9..b2d72bad 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 @@ -28,6 +28,7 @@ import { type WorkOrderAdvancedFilters, } from "@/domain/work-orders/types/work-order-advanced-filters"; import type { WorkOrderUser } from "@/domain/work-orders/types/work-order"; +import type { Service } from "@/domain/services/types/service"; import { WIZARD_STATUS_DOTS } from "@/domain/work-orders/types/work-order-wizard"; type AdvancedSearchFilterSectionsProps = { @@ -37,7 +38,8 @@ type AdvancedSearchFilterSectionsProps = { setMany: (key: AdvancedFilterSetKey, values: string[], select: boolean) => void; sites: { id: string | number; name: string }[]; users: WorkOrderUser[]; - pmTypes: string[]; + services?: Service[]; + pmTypes?: string[]; vendorTechs: string[]; }; @@ -48,7 +50,8 @@ export function AdvancedSearchFilterSections({ setMany, sites, users, - pmTypes, + services = [], + pmTypes = [], vendorTechs, }: AdvancedSearchFilterSectionsProps) { const selectAllRow = (key: AdvancedFilterSetKey, all: string[], selectedSize: number) => ( @@ -141,13 +144,17 @@ export function AdvancedSearchFilterSections({ ({ value: pmType, label: pmType }))} + options={ + services.length > 0 + ? services.map((service) => ({ value: service.name, label: service.name })) + : pmTypes.map((name) => ({ value: name, label: name })) + } selected={draft.pmTypes} onToggle={(value) => toggleIn("pmTypes", value)} onBulkToggle={(values, select) => setMany("pmTypes", values, select)} placeholder="Search service…" - renderLeft={(pmType) => ( - + renderLeft={(name) => ( + )} /> diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet.tsx index b18415f5..797034f0 100644 --- a/src/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet.tsx +++ b/src/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet.tsx @@ -6,6 +6,7 @@ import { IconCloseButton } from "@/components/ui/icon-close-button"; import { SideSheet } from "@/components/ui/side-sheet"; import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters"; import type { WorkOrderUser } from "@/domain/work-orders/types/work-order"; +import type { Service } from "@/domain/services/types/service"; type WorkOrdersAdvancedSearchSheetProps = { open: boolean; @@ -15,7 +16,7 @@ type WorkOrdersAdvancedSearchSheetProps = { onClearAll: () => void; sites: { id: string | number; name: string }[]; users: WorkOrderUser[]; - pmTypes: string[]; + services?: Service[]; vendorTechs: string[]; }; @@ -27,7 +28,7 @@ export function WorkOrdersAdvancedSearchSheet({ onClearAll, sites, users, - pmTypes, + services = [], vendorTechs, }: WorkOrdersAdvancedSearchSheetProps) { const { draft, setDraft, activeCount, toggleIn, setMany, handleClearAll, handleApply } = @@ -59,7 +60,7 @@ export function WorkOrdersAdvancedSearchSheet({ setMany={setMany} sites={sites} users={users} - pmTypes={pmTypes} + services={services} vendorTechs={vendorTechs} /> diff --git a/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts b/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts index a11d067b..1d6066b7 100644 --- a/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts +++ b/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts @@ -28,7 +28,7 @@ export function buildInlineDraftCreateInput( scheduledOn: draft.scheduledOn, dueDate: draft.dueDate, pm: draft.pm, - serviceId: null, + serviceId: draft.serviceId ?? null, extraServices: draft.extraServices ?? [], avetaRequired: Boolean(draft.avetaRequired), pmNote: draft.serviceNotes ?? "", 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 be03e7fe..e5152aa7 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 @@ -13,12 +13,15 @@ import { hasServiceNotesDetails, truncateServiceNotesPreview, } from "@/domain/work-orders/utils/service-notes-ui"; +import type { Service } from "@/domain/services/types/service"; +import { servicesForWorkOrderType } from "@/domain/services/utils/services-for-work-order-type"; +import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; type PMTypeCellProps = { value: string; pmTypes: string[]; q?: string; - onSave: (v: string) => void; + onSave: (v: string, serviceId?: Service["id"]) => void; serviceNotes?: string; extraServices?: string[]; avetaRequired?: boolean; @@ -26,6 +29,9 @@ type PMTypeCellProps = { woNumber?: string; onSaveDetails?: (details: ServiceNotesDetails) => void; viewOnly?: boolean; + services?: Service[]; + serviceId?: string | number | null; + workOrderType?: WOType | ""; }; export function PMTypeCell({ @@ -40,14 +46,22 @@ export function PMTypeCell({ woNumber, onSaveDetails, viewOnly = false, + services, + serviceId, + workOrderType = "", }: PMTypeCellProps) { const [anchorEl, setAnchorEl] = useState(null); const [search, setSearch] = useState(""); const [modalOpen, setModalOpen] = useState(false); + const assignableServices = + services == null ? undefined : servicesForWorkOrderType(services, workOrderType); + const serviceNames = assignableServices?.map((service) => service.name) ?? pmTypes; const filtered = search - ? pmTypes.filter((p) => p.toLowerCase().includes(search.toLowerCase())) - : pmTypes; + ? serviceNames.filter((p) => p.toLowerCase().includes(search.toLowerCase())) + : serviceNames; + const displayValue = + services?.find((service) => String(service.id) === String(serviceId))?.name ?? value; const extras = extraServices ?? []; const hasDetails = hasServiceNotesDetails(serviceNotes, extras, avetaRequired); @@ -63,12 +77,14 @@ export function PMTypeCell({ undefined)} /> @@ -86,17 +102,17 @@ export function PMTypeCell({ className="group/cell flex w-full items-center gap-1.5 transition-opacity hover:opacity-80" > - {value ? ( + {displayValue ? ( - + ) : ( — @@ -112,7 +128,7 @@ export function PMTypeCell({ return ( <> - {value ? ( + {displayValue ? ( - + ) : ( Select … @@ -161,12 +177,13 @@ export function PMTypeCell({ anchorEl={anchorEl} search={search} filtered={filtered} - value={value} + value={displayValue} showAddDetails={Boolean(onSaveDetails)} onSearchChange={setSearch} onClose={closePopover} onPick={(pm) => { - onSave(pm); + const picked = assignableServices?.find((service) => service.name === pm); + onSave(pm, picked?.id); closePopover(); }} onAddDetails={() => { 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 124357ce..737c729e 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 @@ -4,12 +4,14 @@ import { Dialog, DialogContent, DialogTitle } from "@mui/material"; import { WizardServiceNotesFields } from "@/app/(protected)/workorders/_components/wizard/wizard-service-notes-fields"; import { Button } from "@/components/ui/button"; import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop"; +import type { Service } from "@/domain/services/types/service"; export type ServiceNotesDetails = { pm: string; extraServices: string[]; serviceNotes: string; avetaRequired: boolean; + serviceId?: Service["id"] | null; }; type ServiceNotesDialogProps = { @@ -21,6 +23,8 @@ type ServiceNotesDialogProps = { avetaRequired?: boolean; woNumber?: string; pmTypes: string[]; + services?: Service[]; + serviceId?: string | number | null; viewOnly?: boolean; onSave: (details: ServiceNotesDetails) => void; }; @@ -34,6 +38,8 @@ export function ServiceNotesDialog({ avetaRequired = false, woNumber, pmTypes, + services, + serviceId, viewOnly = false, onSave, }: ServiceNotesDialogProps) { @@ -56,7 +62,20 @@ export function ServiceNotesDialog({ setSel((cur) => (cur.includes(value) ? cur.filter((x) => x !== value) : [...cur, value])); }; - const services = pmTypes.map((value) => ({ id: value, value })); + const serviceOptions = services + ? [ + ...services.map((service) => ({ id: service.id, value: service.name })), + ...[pm, ...extraServices] + .filter((value) => value && !services.some((service) => service.name === value)) + .map((value) => ({ id: `historical:${value}`, value })), + ] + : pmTypes.map((value) => ({ id: value, value })); + const saveServiceId = () => { + 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; + }; return (
{ onSave({ pm: sel[0] ?? "", + serviceId: saveServiceId(), extraServices: sel.slice(1), serviceNotes: note, avetaRequired: aveta, 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 6af21133..e0cb2334 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 @@ -5,6 +5,8 @@ import type { WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; +import type { Service } from "@/domain/services/types/service"; +import { isServiceEligibleForWorkOrderType } from "@/domain/services/utils/services-for-work-order-type"; import { formatSeverityEscalationToast, shouldAnnounceSeverityEscalation, @@ -18,11 +20,18 @@ type PersistPatchFn = ( export function saveExistingRowType( onPatch: PersistPatchFn, - row: Pick, + row: Pick, type: WOType, severity: WorkOrderSeverity | null, + services: Service[] = [], ) { - const patch = { type, severity }; + const selectedService = services.find((service) => String(service.id) === String(row.serviceId)); + const clearService = + row.serviceId != null && + selectedService != null && + selectedService.isActive && + !isServiceEligibleForWorkOrderType(selectedService, type); + const patch = clearService ? { type, severity, serviceId: null, pm: "" } : { type, severity }; if (!shouldAnnounceSeverityEscalation(row.severity, severity)) { onPatch(row.id, patch); return; diff --git a/src/app/(protected)/workorders/_components/list/table/to-inline-draft-row.ts b/src/app/(protected)/workorders/_components/list/table/to-inline-draft-row.ts index 65bd25a9..f0343c47 100644 --- a/src/app/(protected)/workorders/_components/list/table/to-inline-draft-row.ts +++ b/src/app/(protected)/workorders/_components/list/table/to-inline-draft-row.ts @@ -25,6 +25,7 @@ export function toInlineDraftRow(draft: WorkOrderTableDraft) { completedDate: "", dueDate: draft.dueDate, pm: draft.pm, + serviceId: draft.serviceId, serviceNotes: draft.serviceNotes, extraServices: draft.extraServices, company: draft.company, diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx index 01dac15d..3d8f07a8 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row-cells.tsx @@ -8,6 +8,9 @@ import type { WorkOrderTableDraft, WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; +import type { Service } from "@/domain/services/types/service"; +import { isServiceEligibleForWorkOrderType } from "@/domain/services/utils/services-for-work-order-type"; +import { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch"; import { AssignedToCell } from "./cells/assigned-to-cell"; import { ApptTimeCell } from "./cells/appt-time-cell"; import { DateCell } from "./cells/date-cell"; @@ -37,6 +40,7 @@ type WoTableInlineRowCellsProps = { sitePlace?: { city: string; state: string }; vendors: VendorDropdownItem[]; pmTypes: string[]; + services?: Service[]; allRows: { id: string | number; woNumber: string }[]; siteCellRef: RefObject; woCellRef: RefObject; @@ -61,17 +65,30 @@ function InlineFieldCell({ children }: { children: ReactNode }) { function InlineTypeSeverityCell({ draft, + services = [], onPatch, }: { draft: WorkOrderTableDraft; + services: Service[]; onPatch: (patch: Partial) => void; }) { + const selectedService = services.find( + (service) => String(service.id) === String(draft.serviceId), + ); return ( onPatch({ type: t, severity: sev })} + onSave={(t, sev) => + onPatch( + draft.serviceId != null && + selectedService != null && + !isServiceEligibleForWorkOrderType(selectedService, t) + ? { type: t, severity: sev, serviceId: null, pm: "" } + : { type: t, severity: sev }, + ) + } /> ); @@ -174,6 +191,7 @@ export function WoTableInlineRowCells({ sitePlace, vendors, pmTypes, + services = [], allRows, siteCellRef, woCellRef, @@ -202,7 +220,7 @@ export function WoTableInlineRowCells({ onDuplicateFound={onDuplicateFound} /> - + onPatch({ pm })} + onSave={(pm, id) => onPatch(id != null ? { pm, serviceId: id } : { pm })} onSaveDetails={(d) => onPatch({ - pm: d.pm, + ...serviceIdentityPatch(draft.serviceId, draft.pm, d.pm, d.serviceId), extraServices: d.extraServices, serviceNotes: d.serviceNotes, avetaRequired: d.avetaRequired, diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx index ffce7992..c37b37e1 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx @@ -11,6 +11,7 @@ import type { WorkOrderTableDraft, WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; +import type { Service } from "@/domain/services/types/service"; import { UNASSIGNED_DAY_KEY, UNSCHEDULED_DAY_KEY, @@ -33,6 +34,7 @@ type WoTableInlineRowProps = { sitePlace?: { city: string; state: string }; vendors: VendorDropdownItem[]; pmTypes: string[]; + services?: Service[]; allRows: { id: string | number; woNumber: string }[]; saving?: boolean; onChange: (draft: WorkOrderTableDraft) => void; @@ -62,6 +64,7 @@ export function WoTableInlineRow({ sitePlace, vendors, pmTypes, + services = [], allRows, saving = false, onChange, @@ -136,6 +139,7 @@ export function WoTableInlineRow({ sitePlace={sitePlace} vendors={vendors} pmTypes={pmTypes} + services={services} allRows={allRows} siteCellRef={siteCellRef} woCellRef={woCellRef} 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 1cd88e6b..7218d868 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 @@ -17,12 +17,14 @@ import { SiteCell } from "./cells/site-cell"; import { TypeCell } from "./cells/type-cell"; import type { WoTableRowHandlers } from "./wo-table-row"; import { resolveDueDateFloor } from "@/domain/work-orders/utils/due-date-floor"; +import type { Service } from "@/domain/services/types/service"; type WoTableRowCellsProps = { row: WorkOrderTableRow; allRows: WorkOrderTableRow[]; users: WorkOrderUser[]; sites: LocationOption[]; + services?: Service[]; sitePlace?: { city: string; state: string }; search?: string; forceScheduleHighlight?: boolean; @@ -39,6 +41,7 @@ export function WoTableRowCells({ allRows, users, sites, + services = [], sitePlace, search, forceScheduleHighlight, @@ -119,7 +122,7 @@ export function WoTableRowCells({ severity={row.severity} q={search} viewOnly={isWorkOrderCompletionSurfaceReadOnly(row.status)} - onSave={(t, sev) => saveExistingRowType(handlers.onPatch, row, t, sev)} + onSave={(t, sev) => saveExistingRowType(handlers.onPatch, row, t, sev, services)} /> onPatchRow({ pm })} + onSave={(pm, id) => onPatchRow(id != null ? { pm, serviceId: id } : { pm })} onSaveDetails={(d) => onPatchRow({ - pm: d.pm, + ...serviceIdentityPatch(row.serviceId, row.pm, d.pm, d.serviceId), extraServices: d.extraServices, serviceNotes: d.serviceNotes, avetaRequired: d.avetaRequired, diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx index 8a7814ec..597f85d0 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-row.tsx @@ -23,6 +23,7 @@ import { resolveFirstCellShadow, resolveWoTableRowStyle, } from "./wo-table-row-style"; +import type { Service } from "@/domain/services/types/service"; export type WoTableRowHandlers = { onRowClick: (row: WorkOrderTableRow) => void; @@ -59,6 +60,7 @@ type WoTableRowProps = { sitePlace?: { city: string; state: string }; vendors: VendorDropdownItem[]; pmTypes: string[]; + services?: Service[]; search?: string; forceScheduleHighlight?: boolean; forceDueDateHighlight?: boolean; @@ -101,6 +103,7 @@ export function WoTableRow({ sitePlace, vendors, pmTypes, + services = [], search, forceScheduleHighlight, forceDueDateHighlight, @@ -197,6 +200,7 @@ export function WoTableRow({ allRows={allRows} users={users} sites={sites} + services={services} sitePlace={sitePlace} search={search} forceScheduleHighlight={forceScheduleHighlight} @@ -210,6 +214,7 @@ export function WoTableRow({ void; onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void; @@ -93,6 +95,7 @@ export function WoTable({ sites, vendors, pmTypes, + services = [], weekMonday, onNavigateToDay, onOpenSlideOver, @@ -141,6 +144,7 @@ export function WoTable({ effectiveSitePlace(row) ?? resolveSitePlace(row.locationId, row.site, sitePlaceLookups) } vendors={vendors} + services={services} pmTypes={pmTypes} search={tableData.search} forceScheduleHighlight={forceScheduleHighlight} @@ -165,6 +169,7 @@ export function WoTable({ sitePlaceLookups, )} vendors={vendors} + services={services} pmTypes={pmTypes} allRows={tableData.rows} saving={saving} diff --git a/src/app/(protected)/workorders/_components/list/work-orders-list-page-panels.tsx b/src/app/(protected)/workorders/_components/list/work-orders-list-page-panels.tsx index 3f230e88..f7797011 100644 --- a/src/app/(protected)/workorders/_components/list/work-orders-list-page-panels.tsx +++ b/src/app/(protected)/workorders/_components/list/work-orders-list-page-panels.tsx @@ -23,8 +23,18 @@ type WorkOrdersListPagePanelsProps = { }; export function WorkOrdersListPagePanels({ page }: WorkOrdersListPagePanelsProps) { - const { filters, tableData, activeDocRow, users, locations, vendors, pmTypes, vendorTechs } = - page; + const { + filters, + tableData, + activeDocRow, + users, + locations, + vendors, + pmTypes, + services, + serviceHistory, + vendorTechs, + } = page; return ( <> @@ -44,7 +54,7 @@ export function WorkOrdersListPagePanels({ page }: WorkOrdersListPagePanelsProps onClearAll={filters.clearAdvancedFilters} sites={locations} users={users} - pmTypes={pmTypes} + services={serviceHistory} vendorTechs={vendorTechs} /> @@ -58,6 +68,7 @@ export function WorkOrdersListPagePanels({ page }: WorkOrdersListPagePanelsProps sites={locations} vendors={vendors} pmTypes={pmTypes} + services={services} allOrders={tableData.rows} onSave={(id, patch, options) => page.patchField(id, patch, options)} onOpenCompletionDoc={(row) => openCompletionDoc(row, page.setDocRow)} 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 113dec56..e322964e 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,6 +9,10 @@ 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 { shouldCompleteAfterCompletionDoc } from "@/domain/work-orders/utils/completion-doc-auto-complete"; import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link"; import { @@ -91,6 +95,8 @@ export function useWorkOrdersListPage() { const { data: locations = [] } = useWorkOrderLocations(); const { data: vendors = [] } = useWorkOrderVendors(); const { data: problemOptions = [] } = useDropdownOptionsByCategory("Problem"); + const { data: services = [] } = useActiveServices(); + const { data: serviceHistory = [] } = useAllServices(); const pmTypes = useMemo( () => problemOptions.map((option) => option.value).filter(Boolean), @@ -195,6 +201,8 @@ export function useWorkOrdersListPage() { locations, vendors, pmTypes, + services, + serviceHistory, vendorTechs, activeSlideOverRow, activeDocRow, diff --git a/src/app/(protected)/workorders/index.tsx b/src/app/(protected)/workorders/index.tsx index 95c54dfe..875662c0 100644 --- a/src/app/(protected)/workorders/index.tsx +++ b/src/app/(protected)/workorders/index.tsx @@ -110,6 +110,7 @@ export default function WorkOrdersListPage() { sites={page.locations} vendors={page.vendors} pmTypes={page.pmTypes} + services={page.services} weekMonday={filters.weekMonday} onNavigateToDay={filters.navigateToDay} onOpenSlideOver={page.handleOpenSlideOver} diff --git a/src/domain/services/use-cases/use-services-for-work-order-type.ts b/src/domain/services/use-cases/use-services-for-work-order-type.ts index f712051e..0816dd27 100644 --- a/src/domain/services/use-cases/use-services-for-work-order-type.ts +++ b/src/domain/services/use-cases/use-services-for-work-order-type.ts @@ -22,3 +22,12 @@ export function useActiveServices(): UseQueryResult { staleTime: 60 * 1000, }); } + +/** Registry history is available to filters so inactive historical WOs remain findable. */ +export function useAllServices(): UseQueryResult { + return useQuery({ + queryKey: queryKeys.services.list(), + queryFn: () => servicesApi.getAll(), + staleTime: 60 * 1000, + }); +} diff --git a/src/domain/services/utils/services-for-work-order-type.ts b/src/domain/services/utils/services-for-work-order-type.ts new file mode 100644 index 00000000..c11641cf --- /dev/null +++ b/src/domain/services/utils/services-for-work-order-type.ts @@ -0,0 +1,14 @@ +import type { Service, ServiceWorkOrderType } from "@/domain/services/types/service"; +import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; + +export function serviceWorkOrderType(type: WOType | ""): ServiceWorkOrderType { + return type === "Reactive" || type === "Emergency" ? type : "PM"; +} + +export function servicesForWorkOrderType(services: Service[], type: WOType | ""): Service[] { + return services.filter((service) => isServiceEligibleForWorkOrderType(service, type)); +} + +export function isServiceEligibleForWorkOrderType(service: Service, type: WOType | ""): boolean { + return service.isActive && service.supportedWorkOrderTypes.includes(serviceWorkOrderType(type)); +} diff --git a/src/domain/work-orders/mappers/expand-scalar-board-ops.ts b/src/domain/work-orders/mappers/expand-scalar-board-ops.ts index c91f11b6..72d6abe5 100644 --- a/src/domain/work-orders/mappers/expand-scalar-board-ops.ts +++ b/src/domain/work-orders/mappers/expand-scalar-board-ops.ts @@ -27,6 +27,11 @@ const SCALAR_BOARD_OP_RULES: ScalarBoardOpRule[] = [ field: "severity", toValue: (value) => (value == null ? "" : String(value)), }, + { + key: "serviceId", + field: "serviceId", + toValue: (value) => (value == null ? "" : String(value)), + }, { key: "type", field: "workOrderType", toValue: (value) => value as string }, { key: "site", field: "siteCode", toValue: (value) => value as string }, { key: "status", field: "lifecycleStatus", toValue: (value) => value as string }, @@ -58,6 +63,7 @@ export function expandScalarBoardOps(patch: WorkOrderTablePatch): BoardFieldOp[] for (const rule of SCALAR_BOARD_OP_RULES) { const value = patch[rule.key]; if (value === undefined) continue; + if (rule.key === "pm" && patch.serviceId !== undefined) continue; ops.push({ field: rule.field, value: rule.toValue(value), diff --git a/src/domain/work-orders/types/work-order-board.ts b/src/domain/work-orders/types/work-order-board.ts index e5d8a619..489181b3 100644 --- a/src/domain/work-orders/types/work-order-board.ts +++ b/src/domain/work-orders/types/work-order-board.ts @@ -163,6 +163,7 @@ export type WorkOrderBoardEditableField = | "apptTime" | "docStatus" | "completedDate" + | "serviceId" | "pm" | "serviceNotes" | "avetaRequired" diff --git a/src/domain/work-orders/types/work-order-table-row.ts b/src/domain/work-orders/types/work-order-table-row.ts index 0dd54c32..9b973ebf 100644 --- a/src/domain/work-orders/types/work-order-table-row.ts +++ b/src/domain/work-orders/types/work-order-table-row.ts @@ -129,6 +129,7 @@ export interface WorkOrderTableDraft { location: string; locationId: string | number; pm: string; + serviceId?: string | number | null; serviceNotes: string; extraServices: string[]; avetaRequired?: boolean; @@ -164,6 +165,7 @@ export const EMPTY_TABLE_DRAFT: WorkOrderTableDraft = { location: "", locationId: "", pm: "", + serviceId: null, serviceNotes: "", extraServices: [], avetaRequired: false, diff --git a/src/domain/work-orders/utils/service-identity-patch.ts b/src/domain/work-orders/utils/service-identity-patch.ts new file mode 100644 index 00000000..6938427c --- /dev/null +++ b/src/domain/work-orders/utils/service-identity-patch.ts @@ -0,0 +1,19 @@ +import type { WorkOrderTablePatch } from "@/domain/work-orders/types/work-order-table-row"; + +export function serviceIdentityPatch( + currentServiceId: string | number | null | undefined, + currentName: string, + nextName: string, + nextServiceId: string | number | null | undefined, +): Pick { + const sameIdentity = + currentServiceId != null && + nextServiceId != null && + String(currentServiceId) === String(nextServiceId); + if (sameIdentity) return {}; + + if (nextServiceId != null) return { pm: nextName, serviceId: nextServiceId }; + if (currentServiceId != null) return { pm: nextName, serviceId: null }; + + return nextName === currentName ? {} : { pm: nextName }; +} diff --git a/src/test/app/(protected)/workorders/aveta-service-notes.test.tsx b/src/test/app/(protected)/workorders/aveta-service-notes.test.tsx index 3df1516b..389f58ae 100644 --- a/src/test/app/(protected)/workorders/aveta-service-notes.test.tsx +++ b/src/test/app/(protected)/workorders/aveta-service-notes.test.tsx @@ -5,6 +5,50 @@ import { ServiceNotesDialog } from "@/app/(protected)/workorders/_components/lis import { renderWithProviders } from "@/test/test-utils"; describe("Aveta Service & Notes", () => { + it("shows eligible registry services and saves the selected service identity", () => { + const onSave = vi.fn(); + renderWithProviders( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /Old snapshot/i })); + + expect(screen.getByRole("button", { name: "QA Test Service" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Leak" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Reactive Only" })).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "QA Test Service" })); + expect(onSave).toHaveBeenCalledWith("QA Test Service", 701); + }); + it("shows the Aveta shield on the Service cell when required", () => { renderWithProviders( { + it("preserves the selected registry service identity", () => { + const draft = Object.assign( + { + ...EMPTY_TABLE_DRAFT, + site: "BK5", + locationId: "9", + pm: "QA Test Service", + }, + { serviceId: 701 }, + ); + + const input = buildInlineDraftCreateInput(draft, [], [{ id: 9, name: "BK5 · Dallas, TX" }]); + + expect(input.pm).toBe("QA Test Service"); + expect(input.serviceId).toBe(701); + + const payload = mapWizardDraftToBoardCreatePayload(input); + expect(payload.primaryService).toBe("QA Test Service"); + expect(payload.serviceId).toBe(701); + }); + it("defaults type to PM and omits schedule flags when undated", () => { const draft = { ...EMPTY_TABLE_DRAFT, 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 86b6020c..83476f3a 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 @@ -7,6 +7,7 @@ vi.mock("react-toastify", () => ({ })); import { saveExistingRowType } from "@/app/(protected)/workorders/_components/list/table/save-existing-row-type"; +import type { Service } from "@/domain/services/types/service"; const row = { id: 7, woNumber: "00000024965", severity: 3 as const }; @@ -61,4 +62,49 @@ describe("saveExistingRowType", () => { expect(onPatch).toHaveBeenCalledWith(7, { type: "PM", severity: null }); expect(onPatch.mock.calls[0]![2]).toBeUndefined(); }); + + it("clears an active service before changing to an unsupported work-order type", () => { + const onPatch = vi.fn(); + const services: Service[] = [ + { + id: 701, + name: "PM Only Service", + trade: "Electrical", + iconKey: "wrench", + requiresCompletionDocument: false, + completionDocTemplate: null, + isActive: true, + supportedWorkOrderTypes: ["PM"], + }, + ]; + + saveExistingRowType(onPatch, { ...row, serviceId: 701 }, "Reactive", null, services); + + expect(onPatch).toHaveBeenCalledWith(7, { + type: "Reactive", + severity: null, + serviceId: null, + pm: "", + }); + }); + + it("preserves an inactive historical service on type change", () => { + const onPatch = vi.fn(); + const services: Service[] = [ + { + id: 701, + name: "Retired Service", + trade: "Electrical", + iconKey: "wrench", + requiresCompletionDocument: false, + completionDocTemplate: null, + isActive: false, + supportedWorkOrderTypes: ["PM"], + }, + ]; + + saveExistingRowType(onPatch, { ...row, serviceId: 701 }, "Reactive", null, services); + + expect(onPatch).toHaveBeenCalledWith(7, { type: "Reactive", severity: null }); + }); }); diff --git a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx index 3927bfaf..9097d2cc 100644 --- a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx +++ b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx @@ -102,9 +102,65 @@ vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({ })); vi.mock("@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category", () => ({ - useDropdownOptionsByCategory: () => ({ data: [{ value: "HVAC" }] }), + useDropdownOptionsByCategory: () => ({ + data: [ + { value: "HVAC" }, + { value: "Leak" }, + { value: "No Power" }, + { value: "Not Cooling" }, + { value: "Other" }, + ], + }), })); +vi.mock("@/domain/services/use-cases/use-services-for-work-order-type", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/domain/services/use-cases/use-services-for-work-order-type") + >(); + return { + ...actual, + useActiveServices: () => ({ + data: [ + { + id: 701, + name: "QA Test Service", + trade: "Electrical", + iconKey: "wrench", + requiresCompletionDocument: false, + completionDocTemplate: null, + isActive: true, + supportedWorkOrderTypes: ["PM", "Reactive", "Emergency"], + }, + ], + }), + useAllServices: () => ({ + data: [ + { + id: 701, + name: "QA Test Service", + trade: "Electrical", + iconKey: "wrench", + requiresCompletionDocument: false, + completionDocTemplate: null, + isActive: true, + supportedWorkOrderTypes: ["PM", "Reactive", "Emergency"], + }, + { + id: 702, + name: "Retired Service", + trade: "Electrical", + iconKey: "wrench", + requiresCompletionDocument: false, + completionDocTemplate: null, + isActive: false, + supportedWorkOrderTypes: ["PM"], + }, + ], + }), + }; +}); + vi.mock("@/domain/work-orders/mocks/is-wo-table-mock-enabled", () => ({ isWoTableMockEnabled: () => false, })); @@ -126,6 +182,24 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => { clearAdvancedFilters.mockClear(); }); + it("offers active registry services instead of legacy Problem dropdown values", () => { + const view = renderWithProviders(, { + route: "/workorders", + withAuth: false, + }); + + fireEvent.click(screen.getByRole("button", { name: "Advanced Filters" })); + advOpen = true; + view.rerender(); + + const sheet = screen.getByRole("dialog", { name: "Advanced Filters" }); + fireEvent.click(within(sheet).getByRole("button", { name: /^Service/i })); + + expect(within(sheet).getByText("QA Test Service")).toBeInTheDocument(); + expect(within(sheet).getByText("Retired Service")).toBeInTheDocument(); + expect(within(sheet).queryByText("Leak")).not.toBeInTheDocument(); + }); + it("opens the sheet from Advanced Filters, applies, then clears", () => { const view = renderWithProviders(, { route: "/workorders", @@ -149,7 +223,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => { } expect(within(sheet).getByText("Seattle")).toBeInTheDocument(); expect(within(sheet).getByText("Dispatcher One")).toBeInTheDocument(); - expect(within(sheet).getByText("HVAC")).toBeInTheDocument(); + expect(within(sheet).getByText("QA Test Service")).toBeInTheDocument(); expect(within(sheet).getByText("Vendor Co")).toBeInTheDocument(); fireEvent.click(within(sheet).getByRole("button", { name: /Aveta Compliance/i })); @@ -218,7 +292,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => { const sheet = screen.getByRole("dialog", { name: "Advanced Filters" }); fireEvent.click(within(sheet).getByRole("button", { name: /^Service/i })); - fireEvent.click(within(sheet).getByText("HVAC")); + fireEvent.click(within(sheet).getByText("QA Test Service")); fireEvent.click(within(sheet).getByRole("button", { name: /Completion doc/i })); fireEvent.click(within(sheet).getByRole("button", { name: /^Pending$/i })); @@ -230,13 +304,13 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => { expect(applyAdvancedFilters).toHaveBeenCalled(); const appliedCalls = applyAdvancedFilters.mock.calls; const applied = appliedCalls[appliedCalls.length - 1]?.[0] as typeof EMPTY_ADVANCED_FILTERS; - expect(applied.pmTypes.has("HVAC")).toBe(true); + expect(applied.pmTypes.has("QA Test Service")).toBe(true); expect(applied.docs.has("No")).toBe(true); expect(applied.docs.has("Yes")).toBe(true); expect(applied.docs.has("NN")).toBe(true); const params = toBoardSearchQueryParams({ filters: applied }); - expect(params.pmTypes).toEqual(["HVAC"]); + expect(params.pmTypes).toEqual(["QA Test Service"]); expect(params.docStatuses).toEqual([2, 1, 3]); }); diff --git a/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts index 9eec3d6d..82acf13f 100644 --- a/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-board-patch-mapper.test.ts @@ -65,6 +65,19 @@ describe("expandTablePatchToBoardFieldOps", () => { expect(expandTablePatchToBoardFieldOps({ pm: "HVAC" })).toEqual([ { field: "pm", value: "HVAC" }, ]); + expect(expandTablePatchToBoardFieldOps({ pm: "QA Test Service", serviceId: 701 })).toEqual([ + { field: "serviceId", value: "701" }, + ]); + expect(expandTablePatchToBoardFieldOps({ serviceId: null, pm: "" })).toEqual([ + { field: "serviceId", value: "" }, + ]); + expect( + expandTablePatchToBoardFieldOps({ severity: 2, serviceId: null, type: "Reactive", pm: "" }), + ).toEqual([ + { field: "severity", value: "2" }, + { field: "serviceId", value: "" }, + { field: "workOrderType", value: "Reactive" }, + ]); expect(expandTablePatchToBoardFieldOps({ docStatus: "NN" })).toEqual([ { field: "docStatus", value: "NotNeeded" }, ]); diff --git a/src/test/domain/work-orders/utils/service-identity-patch.test.ts b/src/test/domain/work-orders/utils/service-identity-patch.test.ts new file mode 100644 index 00000000..6128d94d --- /dev/null +++ b/src/test/domain/work-orders/utils/service-identity-patch.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch"; + +describe("serviceIdentityPatch", () => { + it("keeps an unchanged registry identity out of unrelated detail saves", () => { + expect(serviceIdentityPatch(701, "Old snapshot", "QA Test Service", 701)).toEqual({}); + }); + + it("emits the registry identity when selecting a registered service", () => { + expect(serviceIdentityPatch(null, "Legacy value", "QA Test Service", 701)).toEqual({ + pm: "QA Test Service", + serviceId: 701, + }); + }); + + it("clears a registry identity when the selection is removed", () => { + expect(serviceIdentityPatch(701, "QA Test Service", "", null)).toEqual({ + pm: "", + serviceId: null, + }); + }); + + it("keeps legacy PM edits label-based", () => { + expect(serviceIdentityPatch(null, "Legacy value", "Another legacy value", undefined)).toEqual({ + pm: "Another legacy value", + }); + }); +});