mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 13:42:09 +00:00
refactor(workorders): meet maintainability gate
This commit is contained in:
parent
ffbf60d16a
commit
d40d4eaffb
11 changed files with 821 additions and 439 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue