refactor(workorders): meet maintainability gate

This commit is contained in:
Alexandre Brandizzi 2026-09-30 18:43:00 -03:00
parent ffbf60d16a
commit d40d4eaffb
11 changed files with 821 additions and 439 deletions

View file

@ -44,6 +44,45 @@ type SlideOverInfoTabEditViewProps = {
onRequestComplete?: () => void; onRequestComplete?: () => void;
}; };
type SlideOverServiceFieldProps = Pick<
SlideOverInfoTabEditViewProps,
"draft" | "pmTypes" | "services" | "serviceQuery" | "onDraftChange"
>;
function SlideOverServiceField({
draft,
pmTypes,
services = [],
serviceQuery,
onDraftChange,
}: SlideOverServiceFieldProps) {
return (
<SlideOverInfoRow label="Service">
<PMTypeCell
value={draft.pm}
pmTypes={pmTypes}
services={services}
serviceQuery={serviceQuery}
serviceId={draft.serviceId}
workOrderType={draft.type}
serviceNotes={draft.serviceNotes}
extraServices={draft.extraServices}
avetaRequired={draft.avetaRequired}
woNumber={draft.woNumber}
onSave={(pm, id) => 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,
})
}
/>
</SlideOverInfoRow>
);
}
function SlideOverScheduleOnField({ function SlideOverScheduleOnField({
draft, draft,
onDraftChange, onDraftChange,
@ -216,29 +255,13 @@ export function SlideOverInfoTabEditView({
onSave={(apptTime) => onDraftChange({ apptTime })} onSave={(apptTime) => onDraftChange({ apptTime })}
/> />
</SlideOverInfoRow> </SlideOverInfoRow>
<SlideOverInfoRow label="Service"> <SlideOverServiceField
<PMTypeCell draft={draft}
value={draft.pm} pmTypes={pmTypes}
pmTypes={pmTypes} services={services}
services={services} serviceQuery={serviceQuery}
serviceQuery={serviceQuery} onDraftChange={onDraftChange}
serviceId={draft.serviceId} />
workOrderType={draft.type}
serviceNotes={draft.serviceNotes}
extraServices={draft.extraServices}
avetaRequired={draft.avetaRequired}
woNumber={draft.woNumber}
onSave={(pm, id) => 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,
})
}
/>
</SlideOverInfoRow>
<SlideOverInfoRow label="Vendor"> <SlideOverInfoRow label="Vendor">
<VendorCell <VendorCell
vendorId={draft.vendorId} vendorId={draft.vendorId}

View file

@ -38,52 +38,24 @@ type WorkOrderSlideOverTabsProps = {
onRequestComplete?: () => void; onRequestComplete?: () => void;
}; };
export function WorkOrderSlideOverTabs({ type SecondarySlideOverTabProps = Pick<
WorkOrderSlideOverTabsProps,
"tab" | "state" | "users" | "commentHighlight" | "onOpenCompletionDoc"
>;
function SecondarySlideOverTabs({
tab, tab,
state, state,
saving,
users, users,
sites,
vendors,
pmTypes,
services = [],
serviceHistory,
serviceQuery,
serviceHistoryReady = true,
allOrders,
commentHighlight, commentHighlight,
onDuplicateFound,
onOpenCompletionDoc, onOpenCompletionDoc,
onRequestComplete, }: SecondarySlideOverTabProps) {
}: WorkOrderSlideOverTabsProps) {
const hasError = Boolean(state.error) && !state.detailForRow; const hasError = Boolean(state.error) && !state.detailForRow;
const showContent = Boolean(state.detailForRow || !state.isLoading); const showContent = Boolean(state.detailForRow || !state.isLoading);
const { infoSource, activeDraft } = state; const { infoSource, activeDraft } = state;
return ( return (
<Box className="flex-1 overflow-y-auto p-5"> <>
{tab === "info" && infoSource != null && activeDraft != null && (
<SlideOverInfoTab
row={infoSource}
draft={activeDraft}
editing={state.editing && !saving}
isCancelled={state.isCancelled}
isCompleted={state.isCompleted}
users={users}
sites={sites}
vendors={vendors}
pmTypes={pmTypes}
services={services}
serviceHistory={serviceHistory}
serviceQuery={serviceQuery}
serviceHistoryReady={serviceHistoryReady}
allOrders={allOrders}
onDraftChange={state.handleDraftChange}
onDuplicateFound={onDuplicateFound}
onRequestComplete={onRequestComplete}
/>
)}
{tab === "comments" && ( {tab === "comments" && (
<WorkOrderSlideOverTabPanel <WorkOrderSlideOverTabPanel
variant="comments" variant="comments"
@ -125,7 +97,7 @@ export function WorkOrderSlideOverTabs({
</WorkOrderSlideOverTabPanel> </WorkOrderSlideOverTabPanel>
)} )}
{tab === "completion" && state.infoSource != null && ( {tab === "completion" && infoSource != null && (
<WorkOrderSlideOverTabPanel <WorkOrderSlideOverTabPanel
variant="completion" variant="completion"
showSkeleton={state.showDetailSkeleton} showSkeleton={state.showDetailSkeleton}
@ -135,16 +107,14 @@ export function WorkOrderSlideOverTabs({
showContent={showContent} showContent={showContent}
> >
<SlideOverCompletionTab <SlideOverCompletionTab
row={state.infoSource} row={infoSource}
isReadOnly={state.isDocumentsReadOnly} isReadOnly={state.isDocumentsReadOnly}
completion={state.detailForRow?.completion} completion={state.detailForRow?.completion}
media={state.completionMedia} media={state.completionMedia}
mediaCounts={state.mediaCounts} mediaCounts={state.mediaCounts}
mediaPending={state.mediaPending} mediaPending={state.mediaPending}
completionPending={state.completionPending} completionPending={state.completionPending}
onUploadMedia={(requests) => { onUploadMedia={(requests) => state.uploadMediaRequests(requests)}
state.uploadMediaRequests(requests);
}}
onCategorizeMedia={state.categorizeMedia} onCategorizeMedia={state.categorizeMedia}
onRemoveMedia={state.removeMedia} onRemoveMedia={state.removeMedia}
onRetryMedia={state.retryUpload} onRetryMedia={state.retryUpload}
@ -175,14 +145,67 @@ export function WorkOrderSlideOverTabs({
)} )}
isReadOnly={state.isExtrasReadOnly} isReadOnly={state.isExtrasReadOnly}
pending={state.mediaPending} pending={state.mediaPending}
onUpload={(files) => { onUpload={(files) => void state.uploadExtraFiles(files)}
void state.uploadExtraFiles(files);
}}
onRemove={state.removeMedia} onRemove={state.removeMedia}
onCategorize={state.categorizeMedia} onCategorize={state.categorizeMedia}
/> />
</WorkOrderSlideOverTabPanel> </WorkOrderSlideOverTabPanel>
)} )}
</>
);
}
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 (
<Box className="flex-1 overflow-y-auto p-5">
{tab === "info" && infoSource != null && activeDraft != null && (
<SlideOverInfoTab
row={infoSource}
draft={activeDraft}
editing={state.editing && !saving}
isCancelled={state.isCancelled}
isCompleted={state.isCompleted}
users={users}
sites={sites}
vendors={vendors}
pmTypes={pmTypes}
services={services}
serviceHistory={serviceHistory}
serviceQuery={serviceQuery}
serviceHistoryReady={serviceHistoryReady}
allOrders={allOrders}
onDraftChange={state.handleDraftChange}
onDuplicateFound={onDuplicateFound}
onRequestComplete={onRequestComplete}
/>
)}
<SecondarySlideOverTabs
tab={tab}
state={state}
users={users}
commentHighlight={commentHighlight}
onOpenCompletionDoc={onOpenCompletionDoc}
/>
</Box> </Box>
); );
} }

View file

@ -34,6 +34,205 @@ import { QueryErrorPanel } from "@/components/ui/query-error-panel";
import { Text } from "@/components/ui/text"; import { Text } from "@/components/ui/text";
import { WIZARD_STATUS_DOTS } from "@/domain/work-orders/types/work-order-wizard"; import { WIZARD_STATUS_DOTS } from "@/domain/work-orders/types/work-order-wizard";
type FilterActions = Pick<AdvancedSearchFilterSectionsProps, "draft" | "toggleIn" | "setMany">;
function AdvSelectAllRow({
setMany,
keyName,
values,
selectedCount,
}: {
setMany: AdvancedSearchFilterSectionsProps["setMany"];
keyName: AdvancedFilterSetKey;
values: string[];
selectedCount: number;
}) {
return (
<div className="mb-2 flex justify-end">
<SelectAllAction
anySelected={selectedCount > 0}
onClick={() => setMany(keyName, values, selectedCount === 0)}
/>
</div>
);
}
function AdvancedStatusSection({
draft,
setDraft,
toggleIn,
setMany,
}: FilterActions & Pick<AdvancedSearchFilterSectionsProps, "setDraft">) {
return (
<AdvSection
icon={CheckCircle2}
label="Status"
count={draft.statuses.size + (draft.pastDue ? 1 : 0)}
>
<AdvSelectAllRow
setMany={setMany}
keyName="statuses"
values={[...ALL_WIZARD_STATUSES]}
selectedCount={draft.statuses.size}
/>
<div className="flex flex-wrap gap-1.5">
{ALL_WIZARD_STATUSES.map((status) => (
<AdvStatusPill
key={status}
label={status}
dotColor={WIZARD_STATUS_DOTS[status]}
selected={draft.statuses.has(status)}
selectedBg="var(--color-primary-tint)"
onClick={() => toggleIn("statuses", status)}
/>
))}
<AdvStatusPill
label="Past Due"
dotColor="var(--status-pastdue-text)"
selected={draft.pastDue}
selectedBg="var(--status-pastdue-bg)"
selectedText="var(--status-pastdue-text)"
onClick={() => setDraft((prev) => ({ ...prev, pastDue: !prev.pastDue }))}
/>
</div>
</AdvSection>
);
}
type AdvancedServiceSectionProps = FilterActions &
Pick<AdvancedSearchFilterSectionsProps, "services" | "serviceQuery" | "pmTypes">;
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) && (
<div role="status" aria-label="Loading services">
<Text as="span" variant="description">
Loading services…
</Text>
</div>
)}
{Boolean(query?.isFetching && query.hasData && !query.isError) && (
<div role="status" aria-label="Updating services">
<Text as="span" variant="description">
Updating services…
</Text>
</div>
)}
{Boolean(query?.isError) && (
<div role="alert" aria-label="Service filters unavailable">
<QueryErrorPanel
compact
message="Service filters couldn't load. Retry to load registry services."
onRetry={query?.retry}
/>
</div>
)}
{showEmpty && (
<Text as="div" variant="description">
No services are available.
</Text>
)}
</>
);
}
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 (
<AdvSection icon={Flame} label="Service" count={draft.pmTypes.size}>
<AdvancedServiceRegistryFeedback
query={serviceQuery}
managed={serviceRegistryManaged}
services={serviceList}
/>
<AdvMultiSelect
options={options}
selected={draft.pmTypes}
onToggle={(value) => toggleIn("pmTypes", value)}
onBulkToggle={(values, select) => setMany("pmTypes", values, select)}
placeholder="Search service…"
disabled={unavailable}
renderLeft={(name) => (
<PmTypeIcon pmType={name} size={14} className="shrink-0 text-muted-foreground" />
)}
/>
</AdvSection>
);
}
function AdvancedCompletionDocSection({ draft, toggleIn, setMany }: FilterActions) {
return (
<AdvSection icon={FileText} label="Completion doc" count={draft.docs.size}>
<AdvSelectAllRow
setMany={setMany}
keyName="docs"
values={[...COMPLETION_DOC_OPTIONS]}
selectedCount={draft.docs.size}
/>
<div className="flex items-center gap-2">
{COMPLETION_DOC_OPTIONS.map((doc) => (
<AdvDocPill
key={doc}
label={DOC_FILTER_LABELS[doc]}
selected={draft.docs.has(doc)}
onClick={() => toggleIn("docs", doc)}
/>
))}
</div>
</AdvSection>
);
}
type AdvancedSearchFilterSectionsProps = { type AdvancedSearchFilterSectionsProps = {
draft: WorkOrderAdvancedFilters; draft: WorkOrderAdvancedFilters;
setDraft: Dispatch<SetStateAction<WorkOrderAdvancedFilters>>; setDraft: Dispatch<SetStateAction<WorkOrderAdvancedFilters>>;
@ -59,29 +258,6 @@ export function AdvancedSearchFilterSections({
pmTypes = [], pmTypes = [],
vendorTechs, vendorTechs,
}: AdvancedSearchFilterSectionsProps) { }: AdvancedSearchFilterSectionsProps) {
const selectAllRow = (key: AdvancedFilterSetKey, all: string[], selectedSize: number) => (
<div className="mb-2 flex justify-end">
<SelectAllAction
anySelected={selectedSize > 0}
onClick={() => setMany(key, all, selectedSize === 0)}
/>
</div>
);
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 ( return (
<div className="min-h-0 flex-1 overflow-y-auto px-4"> <div className="min-h-0 flex-1 overflow-y-auto px-4">
<AdvDateRangeSection draft={draft} setDraft={setDraft} /> <AdvDateRangeSection draft={draft} setDraft={setDraft} />
@ -131,83 +307,23 @@ export function AdvancedSearchFilterSections({
/> />
</AdvSection> </AdvSection>
<AdvSection <AdvancedStatusSection
icon={CheckCircle2} draft={draft}
label="Status" setDraft={setDraft}
count={draft.statuses.size + (draft.pastDue ? 1 : 0)} toggleIn={toggleIn}
> setMany={setMany}
{selectAllRow("statuses", [...ALL_WIZARD_STATUSES], draft.statuses.size)} />
<div className="flex flex-wrap gap-1.5">
{ALL_WIZARD_STATUSES.map((status) => (
<AdvStatusPill
key={status}
label={status}
dotColor={WIZARD_STATUS_DOTS[status]}
selected={draft.statuses.has(status)}
selectedBg="var(--color-primary-tint)"
onClick={() => toggleIn("statuses", status)}
/>
))}
<AdvStatusPill
label="Past Due"
dotColor="var(--status-pastdue-text)"
selected={draft.pastDue}
selectedBg="var(--status-pastdue-bg)"
selectedText="var(--status-pastdue-text)"
onClick={() => setDraft((prev) => ({ ...prev, pastDue: !prev.pastDue }))}
/>
</div>
</AdvSection>
<AdvIndicatorsSection draft={draft} setDraft={setDraft} /> <AdvIndicatorsSection draft={draft} setDraft={setDraft} />
<AdvSection icon={Flame} label="Service" count={draft.pmTypes.size}> <AdvancedServiceSection
{Boolean(serviceQuery?.isLoading && !serviceQuery.hasData) && ( draft={draft}
<div role="status" aria-label="Loading services"> toggleIn={toggleIn}
<Text as="span" variant="description"> setMany={setMany}
Loading services… services={services}
</Text> serviceQuery={serviceQuery}
</div> pmTypes={pmTypes}
)} />
{Boolean(serviceQuery?.isFetching && serviceQuery.hasData && !serviceQuery.isError) && (
<div role="status" aria-label="Updating services">
<Text as="span" variant="description">
Updating services…
</Text>
</div>
)}
{Boolean(serviceQuery?.isError) && (
<div role="alert" aria-label="Service filters unavailable">
<QueryErrorPanel
compact
message="Service filters couldn't load. Retry to load registry services."
onRetry={serviceQuery?.retry}
/>
</div>
)}
{Boolean(
serviceRegistryManaged &&
serviceQuery?.hasData &&
!serviceQuery.isLoading &&
!serviceQuery.isError &&
serviceList.length === 0,
) && (
<Text as="div" variant="description">
No services are available.
</Text>
)}
<AdvMultiSelect
options={serviceOptions}
selected={draft.pmTypes}
onToggle={(value) => toggleIn("pmTypes", value)}
onBulkToggle={(values, select) => setMany("pmTypes", values, select)}
placeholder="Search service…"
disabled={serviceChoicesUnavailable}
renderLeft={(name) => (
<PmTypeIcon pmType={name} size={14} className="shrink-0 text-muted-foreground" />
)}
/>
</AdvSection>
<AdvSection icon={Truck} label="Vendor / tech" count={draft.vendorTechs.size}> <AdvSection icon={Truck} label="Vendor / tech" count={draft.vendorTechs.size}>
<AdvMultiSelect <AdvMultiSelect
@ -219,19 +335,7 @@ export function AdvancedSearchFilterSections({
/> />
</AdvSection> </AdvSection>
<AdvSection icon={FileText} label="Completion doc" count={draft.docs.size}> <AdvancedCompletionDocSection draft={draft} toggleIn={toggleIn} setMany={setMany} />
{selectAllRow("docs", [...COMPLETION_DOC_OPTIONS], draft.docs.size)}
<div className="flex items-center gap-2">
{COMPLETION_DOC_OPTIONS.map((doc) => (
<AdvDocPill
key={doc}
label={DOC_FILTER_LABELS[doc]}
selected={draft.docs.has(doc)}
onClick={() => toggleIn("docs", doc)}
/>
))}
</div>
</AdvSection>
<AdvFlagInternalUpliftSections <AdvFlagInternalUpliftSections
draft={draft} draft={draft}

View file

@ -4,22 +4,41 @@ import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order
import { resolveSiteCode } from "@/domain/work-orders/mappers/work-order-board-mapper"; import { resolveSiteCode } from "@/domain/work-orders/mappers/work-order-board-mapper";
import { resolveCreationStatus } from "@/domain/work-orders/utils/wo-lifecycle-transitions"; import { resolveCreationStatus } from "@/domain/work-orders/utils/wo-lifecycle-transitions";
function resolveInlineDraftLocation(draft: WorkOrderTableDraft, sites: LocationOption[]) {
const location = sites.find((site) => 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( export function buildInlineDraftCreateInput(
draft: WorkOrderTableDraft, draft: WorkOrderTableDraft,
users: WorkOrderUser[], users: WorkOrderUser[],
sites: LocationOption[], sites: LocationOption[],
): WorkOrderWizardDraft { ): WorkOrderWizardDraft {
const assignee = users.find((u) => String(u.id) === draft.dispatcherId); const assignee = users.find((u) => String(u.id) === draft.dispatcherId);
const location = sites.find((s) => String(s.id) === String(draft.locationId)); const location = resolveInlineDraftLocation(draft, sites);
const locationName = draft.location || draft.site || location?.name || ""; const schedule = resolveInlineDraftSchedule(draft);
const hasWeekTarget = Boolean(draft.targetWeek?.trim());
const scheduleWeekOnly = draft.scheduleWeekOnly && (hasWeekTarget || Boolean(draft.scheduledOn));
return { return {
locationId: String(draft.locationId || ""), locationId: String(draft.locationId || ""),
locationName, locationName: location.locationName,
siteCode: location ? resolveSiteCode(location) : resolveSiteCode({ name: locationName }), siteCode: location.siteCode,
woNumber: draft.woNumber, woNumber: draft.woNumber,
type: draft.type || "PM", type: draft.type || "PM",
severity: draft.severity, severity: draft.severity,
@ -39,16 +58,12 @@ export function buildInlineDraftCreateInput(
vendorNotes: "", vendorNotes: "",
apptStart: draft.apptStart, apptStart: draft.apptStart,
apptEnd: draft.apptEnd, apptEnd: draft.apptEnd,
status: resolveCreationStatus({ status: schedule.status,
status: draft.status || "Incomplete",
scheduledOn: draft.scheduledOn,
scheduleWeekOnly,
}),
pocName: draft.pocName, pocName: draft.pocName,
pocPhone: draft.pocPhone, pocPhone: draft.pocPhone,
pocNotes: draft.pocNotes, pocNotes: draft.pocNotes,
additionalContacts: draft.additionalContacts ?? [], additionalContacts: draft.additionalContacts ?? [],
scheduleWeekOnly, scheduleWeekOnly: schedule.scheduleWeekOnly,
targetWeek: draft.targetWeek || "", targetWeek: draft.targetWeek || "",
scheduleEnd: draft.scheduleEnd || "", scheduleEnd: draft.scheduleEnd || "",
schedMode: draft.schedMode, schedMode: draft.schedMode,

View file

@ -36,183 +36,277 @@ type PMTypeCellProps = {
serviceQuery?: ServiceRegistryQueryState; serviceQuery?: ServiceRegistryQueryState;
}; };
export function PMTypeCell({ type PMTypeCellModel = {
value, assignableServices?: Service[];
pmTypes, displayValue: string;
q, filtered: string[];
onSave, registryManaged: boolean;
serviceNotes = "", choicesUnavailable: boolean;
extraServices = [], catalogEmpty: boolean;
avetaRequired = false, hasDetails: boolean;
avetaAttached = false, notePreview: string;
woNumber, };
onSaveDetails,
viewOnly = false,
services,
serviceId,
workOrderType = "",
serviceQuery,
}: PMTypeCellProps) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [search, setSearch] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const assignableServices = function getAssignableServices(props: PMTypeCellProps) {
services == null ? undefined : servicesForWorkOrderType(services, workOrderType); return props.services == null
const serviceNames = assignableServices?.map((service) => service.name) ?? pmTypes; ? undefined
const filtered = search : servicesForWorkOrderType(props.services, props.workOrderType ?? "");
? 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;
const extras = extraServices ?? []; function getCellDisplayValue(props: PMTypeCellProps) {
const hasDetails = hasServiceNotesDetails(serviceNotes, extras, avetaRequired); const linkedService = props.services?.find(
const notePreview = serviceNotes?.trim() ? truncateServiceNotesPreview(serviceNotes) : ""; (service) => String(service.id) === String(props.serviceId),
);
return !props.viewOnly && linkedService?.isActive ? linkedService.name : props.value;
}
const closePopover = () => { function getServiceChoicesUnavailable(props: PMTypeCellProps, registryManaged: boolean) {
setAnchorEl(null); const query = props.serviceQuery;
setSearch(""); 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 = function PMTypeCellDialog({
onSaveDetails != null || viewOnly ? ( props,
<ServiceNotesDialog model,
open={modalOpen} open,
onOpenChange={setModalOpen} onOpenChange,
pm={displayValue} }: {
extraServices={extras} props: PMTypeCellProps;
serviceNotes={serviceNotes ?? ""} model: PMTypeCellModel;
avetaRequired={avetaRequired} open: boolean;
woNumber={woNumber} onOpenChange: (open: boolean) => void;
pmTypes={pmTypes} }) {
services={assignableServices} if (!props.viewOnly && props.onSaveDetails == null) return null;
serviceId={serviceId}
viewOnly={viewOnly} return (
onSave={onSaveDetails ?? (() => undefined)} <ServiceNotesDialog
open={open}
onOpenChange={onOpenChange}
pm={model.displayValue}
extraServices={props.extraServices ?? []}
serviceNotes={props.serviceNotes ?? ""}
avetaRequired={props.avetaRequired ?? false}
woNumber={props.woNumber}
pmTypes={props.pmTypes}
services={model.assignableServices}
serviceId={props.serviceId}
viewOnly={props.viewOnly ?? false}
onSave={props.onSaveDetails ?? (() => undefined)}
/>
);
}
function PMTypeCellReadOnly({
props,
model,
onOpenDetails,
}: {
props: PMTypeCellProps;
model: PMTypeCellModel;
onOpenDetails: () => void;
}) {
return (
<button
type="button"
onClick={(event) => {
event.stopPropagation();
onOpenDetails();
}}
className="group/cell flex w-full items-center gap-1.5 transition-opacity hover:opacity-80"
>
<PmTypeIcon
pmType={model.displayValue}
size={12}
className="shrink-0"
style={{ color: "var(--color-text-muted)" }}
/> />
) : null; {model.displayValue ? (
<span
if (viewOnly) { className="min-w-0 truncate"
return ( style={{ fontSize: 13, color: "var(--color-text-primary)" }}
<>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setModalOpen(true);
}}
className="group/cell flex w-full items-center gap-1.5 transition-opacity hover:opacity-80"
> >
<PmTypeIcon <HighlightText text={model.displayValue} q={props.q} />
pmType={displayValue} </span>
size={12} ) : (
className="shrink-0" <span style={{ fontSize: 13, color: "var(--color-text-muted)" }}>—</span>
style={{ color: "var(--color-text-muted)" }} )}
/> {Boolean(props.avetaRequired) && <AvetaBadge size={11} attached={props.avetaAttached} />}
{displayValue ? ( </button>
<span );
className="min-w-0 truncate" }
style={{ fontSize: 13, color: "var(--color-text-primary)" }}
>
<HighlightText text={displayValue} q={q} />
</span>
) : (
<span style={{ fontSize: 13, color: "var(--color-text-muted)" }}>—</span>
)}
{Boolean(avetaRequired) && <AvetaBadge size={11} attached={avetaAttached} />}
</button>
{modal}
</>
);
}
if (hasDetails && onSaveDetails) { function PMTypeCellDetails({
return ( props,
<> model,
<PmTypeDetailsTrigger onOpenDetails,
value={displayValue} }: {
q={q} props: PMTypeCellProps;
notePreview={notePreview} model: PMTypeCellModel;
extrasCount={extras.length} onOpenDetails: () => void;
avetaRequired={avetaRequired} }) {
avetaAttached={avetaAttached} return (
onOpenDetails={() => setModalOpen(true)} <PmTypeDetailsTrigger
/> value={model.displayValue}
{modal} q={props.q}
</> notePreview={model.notePreview}
); extrasCount={(props.extraServices ?? []).length}
} avetaRequired={props.avetaRequired ?? false}
avetaAttached={props.avetaAttached ?? false}
onOpenDetails={onOpenDetails}
/>
);
}
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 ( return (
<> <>
<button <button
type="button" type="button"
onClick={(e) => { onClick={(event) => {
e.stopPropagation(); event.stopPropagation();
setAnchorEl(e.currentTarget); onOpenPicker(event.currentTarget);
}} }}
className="group/cell flex w-full items-center gap-1.5 transition-opacity hover:opacity-80" className="group/cell flex w-full items-center gap-1.5 transition-opacity hover:opacity-80"
> >
<PmTypeIcon <PmTypeIcon
pmType={displayValue} pmType={model.displayValue}
size={12} size={12}
className="shrink-0" className="shrink-0"
style={{ color: "var(--color-text-muted)" }} style={{ color: "var(--color-text-muted)" }}
/> />
{displayValue ? ( {model.displayValue ? (
<span <span
className="min-w-0 truncate" className="min-w-0 truncate"
style={{ style={{ fontSize: 13, color: "var(--color-text-primary)" }}
fontSize: 13,
color: "var(--color-text-primary)",
}}
> >
<HighlightText text={displayValue} q={q} /> <HighlightText text={model.displayValue} q={props.q} />
</span> </span>
) : ( ) : (
<span style={{ fontSize: 13, color: "var(--color-text-muted)" }}>Select …</span> <span style={{ fontSize: 13, color: "var(--color-text-muted)" }}>Select …</span>
)} )}
<HoverChevron /> <HoverChevron />
</button> </button>
<PmTypePickerPopover <PmTypePickerPopover
anchorEl={anchorEl} anchorEl={anchorEl}
search={search} search={search}
filtered={filtered} filtered={model.filtered}
value={displayValue} value={model.displayValue}
showAddDetails={Boolean(onSaveDetails)} showAddDetails={Boolean(props.onSaveDetails)}
onSearchChange={setSearch} onSearchChange={onSearchChange}
onClose={closePopover} onClose={onClose}
registryManaged={serviceRegistryManaged} registryManaged={model.registryManaged}
loading={serviceQuery?.isLoading === true && !serviceQuery.hasData} loading={props.serviceQuery?.isLoading === true && !props.serviceQuery.hasData}
fetching={serviceQuery?.isFetching === true && serviceQuery.hasData} fetching={props.serviceQuery?.isFetching === true && props.serviceQuery.hasData}
error={serviceQuery?.isError === true} error={props.serviceQuery?.isError === true}
empty={serviceCatalogEmpty} empty={model.catalogEmpty}
selectionDisabled={serviceChoicesUnavailable} selectionDisabled={model.choicesUnavailable}
onRetry={serviceQuery?.retry} onRetry={props.serviceQuery?.retry}
onPick={(pm) => { onPick={onPick}
const picked = assignableServices?.find((service) => service.name === pm);
onSave(pm, picked?.id);
closePopover();
}}
onAddDetails={() => { onAddDetails={() => {
closePopover(); onClose();
setModalOpen(true); onOpenDetails();
}} }}
/> />
{modal} </>
);
}
export function PMTypeCell(props: PMTypeCellProps) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(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 ? (
<PMTypeCellReadOnly props={props} model={model} onOpenDetails={openDetails} />
) : model.hasDetails && props.onSaveDetails ? (
<PMTypeCellDetails props={props} model={model} onOpenDetails={openDetails} />
) : (
<PMTypeCellPicker
props={props}
model={model}
anchorEl={anchorEl}
search={search}
onOpenPicker={setAnchorEl}
onSearchChange={setSearch}
onClose={closePicker}
onOpenDetails={openDetails}
/>
);
return (
<>
{content}
<PMTypeCellDialog props={props} model={model} open={modalOpen} onOpenChange={setModalOpen} />
</> </>
); );
} }

View file

@ -18,14 +18,23 @@ type PersistPatchFn = (
options?: { onSuccess?: () => void }, options?: { onSuccess?: () => void },
) => void; ) => void;
export function saveExistingRowType( type SaveExistingRowTypeOptions = {
onPatch: PersistPatchFn, onPatch: PersistPatchFn;
row: Pick<WorkOrderTableRow, "id" | "woNumber" | "severity" | "serviceId">, row: Pick<WorkOrderTableRow, "id" | "woNumber" | "severity" | "serviceId">;
type: WOType, type: WOType;
severity: WorkOrderSeverity | null, severity: WorkOrderSeverity | null;
serviceHistory: Service[] = [], serviceHistory?: Service[];
serviceHistoryReady?: boolean;
};
export function saveExistingRowType({
onPatch,
row,
type,
severity,
serviceHistory = [],
serviceHistoryReady = true, serviceHistoryReady = true,
) { }: SaveExistingRowTypeOptions) {
const selectedService = serviceHistoryReady const selectedService = serviceHistoryReady
? serviceHistory.find((service) => String(service.id) === String(row.serviceId)) ? serviceHistory.find((service) => String(service.id) === String(row.serviceId))
: undefined; : undefined;

View file

@ -127,14 +127,14 @@ export function WoTableRowCells({
q={search} q={search}
viewOnly={isWorkOrderCompletionSurfaceReadOnly(row.status)} viewOnly={isWorkOrderCompletionSurfaceReadOnly(row.status)}
onSave={(t, sev) => onSave={(t, sev) =>
saveExistingRowType( saveExistingRowType({
handlers.onPatch, onPatch: handlers.onPatch,
row, row,
t, type: t,
sev, severity: sev,
serviceHistory ?? services, serviceHistory: serviceHistory ?? services,
serviceHistoryReady, serviceHistoryReady,
) })
} }
/> />
</td> </td>

View file

@ -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<typeof useWoTableRowHandlers>;
};
function WoTableRenderedRow(props: WoTableRowRenderProps) {
return <WoTableRow {...props} />;
}
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 (
<WoTableInlineRow
draft={tableData.draft}
draftDayKey={tableData.draftDayKey}
users={users}
sites={sites}
sitePlace={sitePlace}
vendors={vendors}
services={services}
serviceQuery={serviceQuery}
pmTypes={pmTypes}
allRows={tableData.rows}
saving={saving}
onChange={tableData.setDraft}
onSave={handleInlineSave}
onCancel={tableData.cancelDraft}
onDuplicateFound={onDuplicateFound}
/>
);
}
function WoTableEmptyResult({
tableData,
onOpenAdvancedFilters,
onClearSearch,
}: {
tableData: ReturnTypeOfUseWorkOrdersTableData;
onOpenAdvancedFilters?: () => void;
onClearSearch?: () => void;
}) {
if (tableData.advActive && tableData.scopeTotalCount === 0) {
return (
<WoTableEmptyShell
message="No work orders match these filters"
actionLabel="Adjust filters"
onAction={onOpenAdvancedFilters}
/>
);
}
if (!tableData.advActive && tableData.noSearchResults) {
return (
<WoTableEmptyShell
message="No work orders match your search"
actionLabel="Clear search"
onAction={onClearSearch}
/>
);
}
return null;
}
export function WoTable({ export function WoTable({
tableData, tableData,
users, users,
@ -141,7 +247,7 @@ export function WoTable({
}); });
const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => ( const renderRow = (row: WorkOrderTableRow, forceScheduleHighlight = false) => (
<WoTableRow <WoTableRenderedRow
key={String(row.id)} key={String(row.id)}
row={row} row={row}
allRows={tableData.rows} allRows={tableData.rows}
@ -166,30 +272,26 @@ export function WoTable({
/> />
); );
const renderInlineRow = (dayKey: string) => const renderInlineRow = (dayKey: string) => (
tableData.draftDayKey === dayKey && ( <WoTableRenderedInlineRow
<WoTableInlineRow tableData={tableData}
draft={tableData.draft} dayKey={dayKey}
draftDayKey={tableData.draftDayKey} users={users}
users={users} sites={sites}
sites={sites} sitePlace={resolveSitePlace(
sitePlace={resolveSitePlace( tableData.draft.locationId,
tableData.draft.locationId, tableData.draft.site,
tableData.draft.site, sitePlaceLookups,
sitePlaceLookups, )}
)} vendors={vendors}
vendors={vendors} services={services}
services={services} serviceQuery={serviceQuery}
serviceQuery={serviceQuery} pmTypes={pmTypes}
pmTypes={pmTypes} saving={saving}
allRows={tableData.rows} handleInlineSave={handleInlineSave}
saving={saving} onDuplicateFound={setDuplicateRow}
onChange={tableData.setDraft} />
onSave={handleInlineSave} );
onCancel={tableData.cancelDraft}
onDuplicateFound={setDuplicateRow}
/>
);
if (tableData.isLoading) { if (tableData.isLoading) {
return ( return (
@ -197,29 +299,14 @@ export function WoTable({
); );
} }
const emptyAdv = tableData.advActive && tableData.scopeTotalCount === 0; const emptyResult = (
<WoTableEmptyResult
if (emptyAdv) { tableData={tableData}
return ( onOpenAdvancedFilters={onOpenAdvancedFilters}
<WoTableEmptyShell onClearSearch={onClearSearch}
message="No work orders match these filters" />
actionLabel="Adjust filters" );
onAction={onOpenAdvancedFilters} if (emptyResult != null) return emptyResult;
/>
);
}
const showSearchEmpty = !tableData.advActive && tableData.noSearchResults;
if (showSearchEmpty) {
return (
<WoTableEmptyShell
message="No work orders match your search"
actionLabel="Clear search"
onAction={onClearSearch}
/>
);
}
return ( return (
<> <>

View file

@ -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<Service[], Error>,
): 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,
};
}

View file

@ -9,11 +9,6 @@ import {
} from "@/domain/work-orders/use-cases/use-work-order-detail"; } from "@/domain/work-orders/use-cases/use-work-order-detail";
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list"; 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 { 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 { shouldCompleteAfterCompletionDoc } from "@/domain/work-orders/utils/completion-doc-auto-complete";
import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link"; import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link";
import { import {
@ -23,6 +18,7 @@ import {
import { guardDialogClose } from "@/lib/dialog-guard"; import { guardDialogClose } from "@/lib/dialog-guard";
import { useWorkOrdersListFilters } from "./use-work-orders-list-filters"; import { useWorkOrdersListFilters } from "./use-work-orders-list-filters";
import { useWorkOrdersTableData } from "./use-work-orders-table-data"; import { useWorkOrdersTableData } from "./use-work-orders-table-data";
import { useWorkOrderServiceRegistry } from "./use-work-order-service-registry";
function useSlideOverOpenState() { function useSlideOverOpenState() {
const [slideOverRow, setSlideOverRow] = useState<WorkOrderTableRow | null>(null); const [slideOverRow, setSlideOverRow] = useState<WorkOrderTableRow | null>(null);
@ -96,29 +92,7 @@ export function useWorkOrdersListPage() {
const { data: locations = [] } = useWorkOrderLocations(); const { data: locations = [] } = useWorkOrderLocations();
const { data: vendors = [] } = useWorkOrderVendors(); const { data: vendors = [] } = useWorkOrderVendors();
const { data: problemOptions = [] } = useDropdownOptionsByCategory("Problem"); const { data: problemOptions = [] } = useDropdownOptionsByCategory("Problem");
const activeServicesQuery = useActiveServices(); const serviceRegistry = useWorkOrderServiceRegistry();
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 pmTypes = useMemo( const pmTypes = useMemo(
() => problemOptions.map((option) => option.value).filter(Boolean), () => problemOptions.map((option) => option.value).filter(Boolean),
@ -223,11 +197,7 @@ export function useWorkOrdersListPage() {
locations, locations,
vendors, vendors,
pmTypes, pmTypes,
services, ...serviceRegistry,
serviceHistory,
servicePickerState,
serviceHistoryState,
serviceHistoryReadyForTypeChange,
vendorTechs, vendorTechs,
activeSlideOverRow, activeSlideOverRow,
activeDocRow, activeDocRow,

View file

@ -19,7 +19,7 @@ describe("saveExistingRowType", () => {
it("warns when severity newly enters SEV 1 only after persist onSuccess", () => { it("warns when severity newly enters SEV 1 only after persist onSuccess", () => {
const onPatch = vi.fn(); const onPatch = vi.fn();
saveExistingRowType(onPatch, row, "Emergency", 1); saveExistingRowType({ onPatch, row, type: "Emergency", severity: 1 });
expect(toastWarn).not.toHaveBeenCalled(); expect(toastWarn).not.toHaveBeenCalled();
expect(onPatch).toHaveBeenCalledWith( expect(onPatch).toHaveBeenCalledWith(
@ -36,7 +36,7 @@ describe("saveExistingRowType", () => {
it("does not warn when staying on a critical severity", () => { it("does not warn when staying on a critical severity", () => {
const onPatch = vi.fn(); 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(toastWarn).not.toHaveBeenCalled();
expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", severity: 1 }); expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", severity: 1 });
@ -46,7 +46,7 @@ describe("saveExistingRowType", () => {
it("does not warn when moving between SEV 3–5", () => { it("does not warn when moving between SEV 3–5", () => {
const onPatch = vi.fn(); 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(toastWarn).not.toHaveBeenCalled();
expect(onPatch).toHaveBeenCalledWith(7, { type: "Reactive", severity: 3 }); 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", () => { it("does not warn when clearing severity on a PM correction", () => {
const onPatch = vi.fn(); 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(toastWarn).not.toHaveBeenCalled();
expect(onPatch).toHaveBeenCalledWith(7, { type: "PM", severity: null }); 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, { expect(onPatch).toHaveBeenCalledWith(7, {
type: "Emergency", 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 }); 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, { expect(onPatch).toHaveBeenCalledWith(7, {
type: "Emergency", type: "Emergency",
@ -146,14 +164,14 @@ describe("saveExistingRowType", () => {
supportedWorkOrderTypes: ["PM"], supportedWorkOrderTypes: ["PM"],
}; };
saveExistingRowType( saveExistingRowType({
onPatch, onPatch,
{ ...row, serviceId: 701 }, row: { ...row, serviceId: 701 },
"Emergency", type: "Emergency",
null, severity: null,
[inactiveService], serviceHistory: [inactiveService],
false, serviceHistoryReady: false,
); });
expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", severity: null }); expect(onPatch).toHaveBeenCalledWith(7, { type: "Emergency", severity: null });
}); });