mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 00:23:13 +00:00
fix(workorders): use service registry in list controls
This commit is contained in:
parent
29d235e133
commit
315a26c07d
33 changed files with 499 additions and 49 deletions
|
|
@ -12,6 +12,7 @@ import type {
|
|||
} from "@/domain/work-orders/types/work-order";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { formatApptRange, parseApptTime } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||
import {
|
||||
effectiveCompany,
|
||||
|
|
@ -31,6 +32,7 @@ type SlideOverInfoTabDisplayViewProps = {
|
|||
sites: LocationOption[];
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
};
|
||||
|
||||
type FrozenPoc = ReturnType<typeof effectivePoc>;
|
||||
|
|
@ -115,14 +117,22 @@ function SlideOverSiteField({
|
|||
return <SlideOverCompletedSite source={source} poc={poc} sites={sites} />;
|
||||
}
|
||||
|
||||
function SlideOverOpenService({ source }: { source: WorkOrderTableRow }) {
|
||||
function SlideOverOpenService({
|
||||
source,
|
||||
services,
|
||||
}: {
|
||||
source: WorkOrderTableRow;
|
||||
services: Service[];
|
||||
}) {
|
||||
if (!source.pm) {
|
||||
return <span style={{ color: "var(--color-text-muted)" }}>—</span>;
|
||||
}
|
||||
const name =
|
||||
services.find((service) => String(service.id) === String(source.serviceId))?.name ?? source.pm;
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 text-[12.5px]">
|
||||
<PmTypeIcon pmType={source.pm} size={14} />
|
||||
{source.pm}
|
||||
<PmTypeIcon pmType={name} size={14} />
|
||||
{name}
|
||||
{Boolean(source.avetaRequired) && (
|
||||
<AvetaBadge size={11} attached={Boolean(source.hasAvetaDocument)} />
|
||||
)}
|
||||
|
|
@ -134,16 +144,21 @@ function SlideOverServiceField({
|
|||
source,
|
||||
isCompleted,
|
||||
pmTypes,
|
||||
services,
|
||||
}: {
|
||||
source: WorkOrderTableRow;
|
||||
isCompleted: boolean;
|
||||
pmTypes: string[];
|
||||
services: Service[];
|
||||
}) {
|
||||
if (!isCompleted) return <SlideOverOpenService source={source} />;
|
||||
if (!isCompleted) return <SlideOverOpenService source={source} services={services} />;
|
||||
return (
|
||||
<PMTypeCell
|
||||
value={source.pm}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
serviceId={source.serviceId}
|
||||
workOrderType={source.type}
|
||||
serviceNotes={source.serviceNotes}
|
||||
extraServices={source.extraServices}
|
||||
avetaRequired={source.avetaRequired}
|
||||
|
|
@ -189,6 +204,7 @@ export function SlideOverInfoTabDisplayView({
|
|||
sites,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
}: SlideOverInfoTabDisplayViewProps) {
|
||||
const poc = effectivePoc(source);
|
||||
|
||||
|
|
@ -219,7 +235,12 @@ export function SlideOverInfoTabDisplayView({
|
|||
<span className="text-[12.5px]">{formatApptDisplay(source.apptTime)}</span>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Service">
|
||||
<SlideOverServiceField source={source} isCompleted={isCompleted} pmTypes={pmTypes} />
|
||||
<SlideOverServiceField
|
||||
source={source}
|
||||
isCompleted={isCompleted}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
/>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Vendor">
|
||||
<SlideOverVendorField source={source} isCompleted={isCompleted} vendors={vendors} />
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import type {
|
|||
} from "@/domain/work-orders/types/work-order";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { WizardWOStatus } from "@/domain/work-orders/types/work-order-wizard";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { isServiceEligibleForWorkOrderType } from "@/domain/services/utils/services-for-work-order-type";
|
||||
import { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch";
|
||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||
import { DocBadge } from "./slide-over-doc-badge";
|
||||
|
|
@ -30,6 +33,7 @@ type SlideOverInfoTabEditViewProps = {
|
|||
sites: LocationOption[];
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
allOrders: WorkOrderTableRow[];
|
||||
onDraftChange: (patch: Partial<WorkOrderTableRow>) => void;
|
||||
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
|
||||
|
|
@ -114,6 +118,7 @@ export function SlideOverInfoTabEditView({
|
|||
sites,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
allOrders,
|
||||
onDraftChange,
|
||||
onDuplicateFound,
|
||||
|
|
@ -165,7 +170,18 @@ export function SlideOverInfoTabEditView({
|
|||
<TypeCell
|
||||
value={draft.type}
|
||||
severity={draft.severity}
|
||||
onSave={(type, severity) => onDraftChange({ type, severity })}
|
||||
onSave={(type, severity) => {
|
||||
const selectedService = services.find(
|
||||
(service) => String(service.id) === String(draft.serviceId),
|
||||
);
|
||||
onDraftChange(
|
||||
draft.serviceId != null &&
|
||||
selectedService != null &&
|
||||
!isServiceEligibleForWorkOrderType(selectedService, type)
|
||||
? { type, severity, serviceId: null, pm: "" }
|
||||
: { type, severity },
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Assigned To">
|
||||
|
|
@ -196,14 +212,17 @@ export function SlideOverInfoTabEditView({
|
|||
<PMTypeCell
|
||||
value={draft.pm}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
serviceId={draft.serviceId}
|
||||
workOrderType={draft.type}
|
||||
serviceNotes={draft.serviceNotes}
|
||||
extraServices={draft.extraServices}
|
||||
avetaRequired={draft.avetaRequired}
|
||||
woNumber={draft.woNumber}
|
||||
onSave={(pm) => onDraftChange({ pm })}
|
||||
onSave={(pm, id) => onDraftChange(id != null ? { pm, serviceId: id } : { pm })}
|
||||
onSaveDetails={(details) =>
|
||||
onDraftChange({
|
||||
pm: details.pm,
|
||||
...serviceIdentityPatch(draft.serviceId, draft.pm, details.pm, details.serviceId),
|
||||
serviceNotes: details.serviceNotes,
|
||||
extraServices: details.extraServices,
|
||||
avetaRequired: details.avetaRequired,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
WorkOrderUser,
|
||||
} from "@/domain/work-orders/types/work-order";
|
||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { SlideOverInfoTabDisplayView } from "./slide-over-info-tab-display-view";
|
||||
import { SlideOverInfoTabEditView } from "./slide-over-info-tab-edit-view";
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ type SlideOverInfoTabProps = {
|
|||
sites: LocationOption[];
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
allOrders: WorkOrderTableRow[];
|
||||
onDraftChange: (patch: Partial<WorkOrderTableRow>) => void;
|
||||
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
|
||||
|
|
@ -33,6 +35,7 @@ export function SlideOverInfoTab({
|
|||
sites,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
allOrders,
|
||||
onDraftChange,
|
||||
onDuplicateFound,
|
||||
|
|
@ -50,6 +53,7 @@ export function SlideOverInfoTab({
|
|||
sites={sites}
|
||||
vendors={vendors}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
allOrders={allOrders}
|
||||
onDraftChange={onDraftChange}
|
||||
onDuplicateFound={onDuplicateFound}
|
||||
|
|
@ -67,6 +71,7 @@ export function SlideOverInfoTab({
|
|||
sites={sites}
|
||||
vendors={vendors}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||
import { resolveSlideOverAvetaRequired } from "@/domain/work-orders/utils/resolve-slide-over-aveta-required";
|
||||
import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import type { useWorkOrderSlideOver, SlideOverTab } from "./use-work-order-slide-over";
|
||||
import { WorkOrderSlideOverTabPanel } from "./work-order-slide-over-tab-panel";
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ type WorkOrderSlideOverTabsProps = {
|
|||
sites: LocationOption[];
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
allOrders: WorkOrderTableRow[];
|
||||
commentHighlight?: CommentHighlight | null;
|
||||
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
|
||||
|
|
@ -40,6 +42,7 @@ export function WorkOrderSlideOverTabs({
|
|||
sites,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
allOrders,
|
||||
commentHighlight,
|
||||
onDuplicateFound,
|
||||
|
|
@ -63,6 +66,7 @@ export function WorkOrderSlideOverTabs({
|
|||
sites={sites}
|
||||
vendors={vendors}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
allOrders={allOrders}
|
||||
onDraftChange={state.handleDraftChange}
|
||||
onDuplicateFound={onDuplicateFound}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
WorkOrderTableRow,
|
||||
} from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { useWorkOrderSlideOver, type SlideOverTab } from "./use-work-order-slide-over";
|
||||
import { WorkOrderSlideOverTabNav } from "./work-order-slide-over-tab-nav";
|
||||
import { WorkOrderSlideOverTabs } from "./work-order-slide-over-tabs";
|
||||
|
|
@ -28,6 +29,7 @@ type WorkOrderSlideOverProps = {
|
|||
sites: LocationOption[];
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
allOrders: WorkOrderTableRow[];
|
||||
onSave: (id: string | number, patch: WorkOrderTablePatch, options?: SlideOverSaveOptions) => void;
|
||||
onOpenCompletionDoc: (row: WorkOrderTableRow) => void;
|
||||
|
|
@ -50,6 +52,7 @@ export function WorkOrderSlideOver({
|
|||
sites,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
allOrders,
|
||||
onSave,
|
||||
onOpenCompletionDoc,
|
||||
|
|
@ -101,6 +104,7 @@ export function WorkOrderSlideOver({
|
|||
sites={sites}
|
||||
vendors={vendors}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
allOrders={allOrders}
|
||||
commentHighlight={commentHighlight}
|
||||
onDuplicateFound={onDuplicateFound}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
type WorkOrderAdvancedFilters,
|
||||
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { WIZARD_STATUS_DOTS } from "@/domain/work-orders/types/work-order-wizard";
|
||||
|
||||
type AdvancedSearchFilterSectionsProps = {
|
||||
|
|
@ -37,7 +38,8 @@ type AdvancedSearchFilterSectionsProps = {
|
|||
setMany: (key: AdvancedFilterSetKey, values: string[], select: boolean) => void;
|
||||
sites: { id: string | number; name: string }[];
|
||||
users: WorkOrderUser[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
pmTypes?: string[];
|
||||
vendorTechs: string[];
|
||||
};
|
||||
|
||||
|
|
@ -48,7 +50,8 @@ export function AdvancedSearchFilterSections({
|
|||
setMany,
|
||||
sites,
|
||||
users,
|
||||
pmTypes,
|
||||
services = [],
|
||||
pmTypes = [],
|
||||
vendorTechs,
|
||||
}: AdvancedSearchFilterSectionsProps) {
|
||||
const selectAllRow = (key: AdvancedFilterSetKey, all: string[], selectedSize: number) => (
|
||||
|
|
@ -141,13 +144,17 @@ export function AdvancedSearchFilterSections({
|
|||
|
||||
<AdvSection icon={Flame} label="Service" count={draft.pmTypes.size}>
|
||||
<AdvMultiSelect
|
||||
options={pmTypes.map((pmType) => ({ value: pmType, label: pmType }))}
|
||||
options={
|
||||
services.length > 0
|
||||
? services.map((service) => ({ value: service.name, label: service.name }))
|
||||
: pmTypes.map((name) => ({ value: name, label: name }))
|
||||
}
|
||||
selected={draft.pmTypes}
|
||||
onToggle={(value) => toggleIn("pmTypes", value)}
|
||||
onBulkToggle={(values, select) => setMany("pmTypes", values, select)}
|
||||
placeholder="Search service…"
|
||||
renderLeft={(pmType) => (
|
||||
<PmTypeIcon pmType={pmType} size={14} className="shrink-0 text-muted-foreground" />
|
||||
renderLeft={(name) => (
|
||||
<PmTypeIcon pmType={name} size={14} className="shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
/>
|
||||
</AdvSection>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { IconCloseButton } from "@/components/ui/icon-close-button";
|
|||
import { SideSheet } from "@/components/ui/side-sheet";
|
||||
import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
|
||||
type WorkOrdersAdvancedSearchSheetProps = {
|
||||
open: boolean;
|
||||
|
|
@ -15,7 +16,7 @@ type WorkOrdersAdvancedSearchSheetProps = {
|
|||
onClearAll: () => void;
|
||||
sites: { id: string | number; name: string }[];
|
||||
users: WorkOrderUser[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
vendorTechs: string[];
|
||||
};
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ export function WorkOrdersAdvancedSearchSheet({
|
|||
onClearAll,
|
||||
sites,
|
||||
users,
|
||||
pmTypes,
|
||||
services = [],
|
||||
vendorTechs,
|
||||
}: WorkOrdersAdvancedSearchSheetProps) {
|
||||
const { draft, setDraft, activeCount, toggleIn, setMany, handleClearAll, handleApply } =
|
||||
|
|
@ -59,7 +60,7 @@ export function WorkOrdersAdvancedSearchSheet({
|
|||
setMany={setMany}
|
||||
sites={sites}
|
||||
users={users}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
vendorTechs={vendorTechs}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export function buildInlineDraftCreateInput(
|
|||
scheduledOn: draft.scheduledOn,
|
||||
dueDate: draft.dueDate,
|
||||
pm: draft.pm,
|
||||
serviceId: null,
|
||||
serviceId: draft.serviceId ?? null,
|
||||
extraServices: draft.extraServices ?? [],
|
||||
avetaRequired: Boolean(draft.avetaRequired),
|
||||
pmNote: draft.serviceNotes ?? "",
|
||||
|
|
|
|||
|
|
@ -13,12 +13,15 @@ import {
|
|||
hasServiceNotesDetails,
|
||||
truncateServiceNotesPreview,
|
||||
} from "@/domain/work-orders/utils/service-notes-ui";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { servicesForWorkOrderType } from "@/domain/services/utils/services-for-work-order-type";
|
||||
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||
|
||||
type PMTypeCellProps = {
|
||||
value: string;
|
||||
pmTypes: string[];
|
||||
q?: string;
|
||||
onSave: (v: string) => void;
|
||||
onSave: (v: string, serviceId?: Service["id"]) => void;
|
||||
serviceNotes?: string;
|
||||
extraServices?: string[];
|
||||
avetaRequired?: boolean;
|
||||
|
|
@ -26,6 +29,9 @@ type PMTypeCellProps = {
|
|||
woNumber?: string;
|
||||
onSaveDetails?: (details: ServiceNotesDetails) => void;
|
||||
viewOnly?: boolean;
|
||||
services?: Service[];
|
||||
serviceId?: string | number | null;
|
||||
workOrderType?: WOType | "";
|
||||
};
|
||||
|
||||
export function PMTypeCell({
|
||||
|
|
@ -40,14 +46,22 @@ export function PMTypeCell({
|
|||
woNumber,
|
||||
onSaveDetails,
|
||||
viewOnly = false,
|
||||
services,
|
||||
serviceId,
|
||||
workOrderType = "",
|
||||
}: PMTypeCellProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const assignableServices =
|
||||
services == null ? undefined : servicesForWorkOrderType(services, workOrderType);
|
||||
const serviceNames = assignableServices?.map((service) => service.name) ?? pmTypes;
|
||||
const filtered = search
|
||||
? pmTypes.filter((p) => p.toLowerCase().includes(search.toLowerCase()))
|
||||
: pmTypes;
|
||||
? serviceNames.filter((p) => p.toLowerCase().includes(search.toLowerCase()))
|
||||
: serviceNames;
|
||||
const displayValue =
|
||||
services?.find((service) => String(service.id) === String(serviceId))?.name ?? value;
|
||||
|
||||
const extras = extraServices ?? [];
|
||||
const hasDetails = hasServiceNotesDetails(serviceNotes, extras, avetaRequired);
|
||||
|
|
@ -63,12 +77,14 @@ export function PMTypeCell({
|
|||
<ServiceNotesDialog
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
pm={value}
|
||||
pm={displayValue}
|
||||
extraServices={extras}
|
||||
serviceNotes={serviceNotes ?? ""}
|
||||
avetaRequired={avetaRequired}
|
||||
woNumber={woNumber}
|
||||
pmTypes={pmTypes}
|
||||
services={assignableServices}
|
||||
serviceId={serviceId}
|
||||
viewOnly={viewOnly}
|
||||
onSave={onSaveDetails ?? (() => undefined)}
|
||||
/>
|
||||
|
|
@ -86,17 +102,17 @@ export function PMTypeCell({
|
|||
className="group/cell flex w-full items-center gap-1.5 transition-opacity hover:opacity-80"
|
||||
>
|
||||
<PmTypeIcon
|
||||
pmType={value}
|
||||
pmType={displayValue}
|
||||
size={12}
|
||||
className="shrink-0"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
/>
|
||||
{value ? (
|
||||
{displayValue ? (
|
||||
<span
|
||||
className="min-w-0 truncate"
|
||||
style={{ fontSize: 13, color: "var(--color-text-primary)" }}
|
||||
>
|
||||
<HighlightText text={value} q={q} />
|
||||
<HighlightText text={displayValue} q={q} />
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 13, color: "var(--color-text-muted)" }}>—</span>
|
||||
|
|
@ -112,7 +128,7 @@ export function PMTypeCell({
|
|||
return (
|
||||
<>
|
||||
<PmTypeDetailsTrigger
|
||||
value={value}
|
||||
value={displayValue}
|
||||
q={q}
|
||||
notePreview={notePreview}
|
||||
extrasCount={extras.length}
|
||||
|
|
@ -136,12 +152,12 @@ export function PMTypeCell({
|
|||
className="group/cell flex w-full items-center gap-1.5 transition-opacity hover:opacity-80"
|
||||
>
|
||||
<PmTypeIcon
|
||||
pmType={value}
|
||||
pmType={displayValue}
|
||||
size={12}
|
||||
className="shrink-0"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
/>
|
||||
{value ? (
|
||||
{displayValue ? (
|
||||
<span
|
||||
className="min-w-0 truncate"
|
||||
style={{
|
||||
|
|
@ -149,7 +165,7 @@ export function PMTypeCell({
|
|||
color: "var(--color-text-primary)",
|
||||
}}
|
||||
>
|
||||
<HighlightText text={value} q={q} />
|
||||
<HighlightText text={displayValue} q={q} />
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontSize: 13, color: "var(--color-text-muted)" }}>Select …</span>
|
||||
|
|
@ -161,12 +177,13 @@ export function PMTypeCell({
|
|||
anchorEl={anchorEl}
|
||||
search={search}
|
||||
filtered={filtered}
|
||||
value={value}
|
||||
value={displayValue}
|
||||
showAddDetails={Boolean(onSaveDetails)}
|
||||
onSearchChange={setSearch}
|
||||
onClose={closePopover}
|
||||
onPick={(pm) => {
|
||||
onSave(pm);
|
||||
const picked = assignableServices?.find((service) => service.name === pm);
|
||||
onSave(pm, picked?.id);
|
||||
closePopover();
|
||||
}}
|
||||
onAddDetails={() => {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import { Dialog, DialogContent, DialogTitle } from "@mui/material";
|
|||
import { WizardServiceNotesFields } from "@/app/(protected)/workorders/_components/wizard/wizard-service-notes-fields";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
|
||||
export type ServiceNotesDetails = {
|
||||
pm: string;
|
||||
extraServices: string[];
|
||||
serviceNotes: string;
|
||||
avetaRequired: boolean;
|
||||
serviceId?: Service["id"] | null;
|
||||
};
|
||||
|
||||
type ServiceNotesDialogProps = {
|
||||
|
|
@ -21,6 +23,8 @@ type ServiceNotesDialogProps = {
|
|||
avetaRequired?: boolean;
|
||||
woNumber?: string;
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
serviceId?: string | number | null;
|
||||
viewOnly?: boolean;
|
||||
onSave: (details: ServiceNotesDetails) => void;
|
||||
};
|
||||
|
|
@ -34,6 +38,8 @@ export function ServiceNotesDialog({
|
|||
avetaRequired = false,
|
||||
woNumber,
|
||||
pmTypes,
|
||||
services,
|
||||
serviceId,
|
||||
viewOnly = false,
|
||||
onSave,
|
||||
}: ServiceNotesDialogProps) {
|
||||
|
|
@ -56,7 +62,20 @@ export function ServiceNotesDialog({
|
|||
setSel((cur) => (cur.includes(value) ? cur.filter((x) => x !== value) : [...cur, value]));
|
||||
};
|
||||
|
||||
const services = pmTypes.map((value) => ({ id: value, value }));
|
||||
const serviceOptions = services
|
||||
? [
|
||||
...services.map((service) => ({ id: service.id, value: service.name })),
|
||||
...[pm, ...extraServices]
|
||||
.filter((value) => value && !services.some((service) => service.name === value))
|
||||
.map((value) => ({ id: `historical:${value}`, value })),
|
||||
]
|
||||
: pmTypes.map((value) => ({ id: value, value }));
|
||||
const saveServiceId = () => {
|
||||
const selectedId = services?.find((service) => service.name === sel[0])?.id;
|
||||
if (selectedId != null) return selectedId;
|
||||
if (sel[0] === pm) return serviceId ?? undefined;
|
||||
return serviceId == null ? undefined : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
|
@ -85,7 +104,7 @@ export function ServiceNotesDialog({
|
|||
>
|
||||
<fieldset disabled={viewOnly} className="min-w-0 space-y-4 border-0 p-0">
|
||||
<WizardServiceNotesFields
|
||||
services={services}
|
||||
services={serviceOptions}
|
||||
sel={sel}
|
||||
onToggle={toggle}
|
||||
note={note}
|
||||
|
|
@ -121,6 +140,7 @@ export function ServiceNotesDialog({
|
|||
onClick={() => {
|
||||
onSave({
|
||||
pm: sel[0] ?? "",
|
||||
serviceId: saveServiceId(),
|
||||
extraServices: sel.slice(1),
|
||||
serviceNotes: note,
|
||||
avetaRequired: aveta,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type {
|
|||
WorkOrderTableRow,
|
||||
} from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { isServiceEligibleForWorkOrderType } from "@/domain/services/utils/services-for-work-order-type";
|
||||
import {
|
||||
formatSeverityEscalationToast,
|
||||
shouldAnnounceSeverityEscalation,
|
||||
|
|
@ -18,11 +20,18 @@ type PersistPatchFn = (
|
|||
|
||||
export function saveExistingRowType(
|
||||
onPatch: PersistPatchFn,
|
||||
row: Pick<WorkOrderTableRow, "id" | "woNumber" | "severity">,
|
||||
row: Pick<WorkOrderTableRow, "id" | "woNumber" | "severity" | "serviceId">,
|
||||
type: WOType,
|
||||
severity: WorkOrderSeverity | null,
|
||||
services: Service[] = [],
|
||||
) {
|
||||
const patch = { type, severity };
|
||||
const selectedService = services.find((service) => String(service.id) === String(row.serviceId));
|
||||
const clearService =
|
||||
row.serviceId != null &&
|
||||
selectedService != null &&
|
||||
selectedService.isActive &&
|
||||
!isServiceEligibleForWorkOrderType(selectedService, type);
|
||||
const patch = clearService ? { type, severity, serviceId: null, pm: "" } : { type, severity };
|
||||
if (!shouldAnnounceSeverityEscalation(row.severity, severity)) {
|
||||
onPatch(row.id, patch);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export function toInlineDraftRow(draft: WorkOrderTableDraft) {
|
|||
completedDate: "",
|
||||
dueDate: draft.dueDate,
|
||||
pm: draft.pm,
|
||||
serviceId: draft.serviceId,
|
||||
serviceNotes: draft.serviceNotes,
|
||||
extraServices: draft.extraServices,
|
||||
company: draft.company,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import type {
|
|||
WorkOrderTableDraft,
|
||||
WorkOrderTableRow,
|
||||
} from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { isServiceEligibleForWorkOrderType } from "@/domain/services/utils/services-for-work-order-type";
|
||||
import { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch";
|
||||
import { AssignedToCell } from "./cells/assigned-to-cell";
|
||||
import { ApptTimeCell } from "./cells/appt-time-cell";
|
||||
import { DateCell } from "./cells/date-cell";
|
||||
|
|
@ -37,6 +40,7 @@ type WoTableInlineRowCellsProps = {
|
|||
sitePlace?: { city: string; state: string };
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
allRows: { id: string | number; woNumber: string }[];
|
||||
siteCellRef: RefObject<HTMLTableCellElement | null>;
|
||||
woCellRef: RefObject<HTMLTableCellElement | null>;
|
||||
|
|
@ -61,17 +65,30 @@ function InlineFieldCell({ children }: { children: ReactNode }) {
|
|||
|
||||
function InlineTypeSeverityCell({
|
||||
draft,
|
||||
services = [],
|
||||
onPatch,
|
||||
}: {
|
||||
draft: WorkOrderTableDraft;
|
||||
services: Service[];
|
||||
onPatch: (patch: Partial<WorkOrderTableDraft>) => void;
|
||||
}) {
|
||||
const selectedService = services.find(
|
||||
(service) => String(service.id) === String(draft.serviceId),
|
||||
);
|
||||
return (
|
||||
<InlineFieldCell>
|
||||
<TypeCell
|
||||
value={draft.type}
|
||||
severity={draft.severity}
|
||||
onSave={(t, sev) => onPatch({ type: t, severity: sev })}
|
||||
onSave={(t, sev) =>
|
||||
onPatch(
|
||||
draft.serviceId != null &&
|
||||
selectedService != null &&
|
||||
!isServiceEligibleForWorkOrderType(selectedService, t)
|
||||
? { type: t, severity: sev, serviceId: null, pm: "" }
|
||||
: { type: t, severity: sev },
|
||||
)
|
||||
}
|
||||
/>
|
||||
</InlineFieldCell>
|
||||
);
|
||||
|
|
@ -174,6 +191,7 @@ export function WoTableInlineRowCells({
|
|||
sitePlace,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
allRows,
|
||||
siteCellRef,
|
||||
woCellRef,
|
||||
|
|
@ -202,7 +220,7 @@ export function WoTableInlineRowCells({
|
|||
onDuplicateFound={onDuplicateFound}
|
||||
/>
|
||||
|
||||
<InlineTypeSeverityCell draft={draft} onPatch={onPatch} />
|
||||
<InlineTypeSeverityCell draft={draft} services={services} onPatch={onPatch} />
|
||||
|
||||
<InlineFieldCell>
|
||||
<AssignedToCell
|
||||
|
|
@ -227,14 +245,17 @@ export function WoTableInlineRowCells({
|
|||
<PMTypeCell
|
||||
value={draft.pm}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
serviceId={draft.serviceId}
|
||||
workOrderType={draft.type}
|
||||
serviceNotes={draft.serviceNotes}
|
||||
extraServices={draft.extraServices}
|
||||
avetaRequired={draft.avetaRequired}
|
||||
woNumber={draft.woNumber}
|
||||
onSave={(pm) => onPatch({ pm })}
|
||||
onSave={(pm, id) => onPatch(id != null ? { pm, serviceId: id } : { pm })}
|
||||
onSaveDetails={(d) =>
|
||||
onPatch({
|
||||
pm: d.pm,
|
||||
...serviceIdentityPatch(draft.serviceId, draft.pm, d.pm, d.serviceId),
|
||||
extraServices: d.extraServices,
|
||||
serviceNotes: d.serviceNotes,
|
||||
avetaRequired: d.avetaRequired,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
WorkOrderTableDraft,
|
||||
WorkOrderTableRow,
|
||||
} from "@/domain/work-orders/types/work-order-table-row";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import {
|
||||
UNASSIGNED_DAY_KEY,
|
||||
UNSCHEDULED_DAY_KEY,
|
||||
|
|
@ -33,6 +34,7 @@ type WoTableInlineRowProps = {
|
|||
sitePlace?: { city: string; state: string };
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
allRows: { id: string | number; woNumber: string }[];
|
||||
saving?: boolean;
|
||||
onChange: (draft: WorkOrderTableDraft) => void;
|
||||
|
|
@ -62,6 +64,7 @@ export function WoTableInlineRow({
|
|||
sitePlace,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
allRows,
|
||||
saving = false,
|
||||
onChange,
|
||||
|
|
@ -136,6 +139,7 @@ export function WoTableInlineRow({
|
|||
sitePlace={sitePlace}
|
||||
vendors={vendors}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
allRows={allRows}
|
||||
siteCellRef={siteCellRef}
|
||||
woCellRef={woCellRef}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@ import { SiteCell } from "./cells/site-cell";
|
|||
import { TypeCell } from "./cells/type-cell";
|
||||
import type { WoTableRowHandlers } from "./wo-table-row";
|
||||
import { resolveDueDateFloor } from "@/domain/work-orders/utils/due-date-floor";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
|
||||
type WoTableRowCellsProps = {
|
||||
row: WorkOrderTableRow;
|
||||
allRows: WorkOrderTableRow[];
|
||||
users: WorkOrderUser[];
|
||||
sites: LocationOption[];
|
||||
services?: Service[];
|
||||
sitePlace?: { city: string; state: string };
|
||||
search?: string;
|
||||
forceScheduleHighlight?: boolean;
|
||||
|
|
@ -39,6 +41,7 @@ export function WoTableRowCells({
|
|||
allRows,
|
||||
users,
|
||||
sites,
|
||||
services = [],
|
||||
sitePlace,
|
||||
search,
|
||||
forceScheduleHighlight,
|
||||
|
|
@ -119,7 +122,7 @@ export function WoTableRowCells({
|
|||
severity={row.severity}
|
||||
q={search}
|
||||
viewOnly={isWorkOrderCompletionSurfaceReadOnly(row.status)}
|
||||
onSave={(t, sev) => saveExistingRowType(handlers.onPatch, row, t, sev)}
|
||||
onSave={(t, sev) => saveExistingRowType(handlers.onPatch, row, t, sev, services)}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
|
|
|
|||
|
|
@ -13,11 +13,14 @@ import { isWorkOrderUpliftsReadOnly } from "@/domain/work-orders/utils/uplift-di
|
|||
import { applyVendorTableSave } from "@/domain/work-orders/utils/work-order-feedback-toasts";
|
||||
import { effectiveCompany } from "@/domain/work-orders/utils/effective-completion-snapshot";
|
||||
import type { WoTableRowHandlers } from "./wo-table-row";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
import { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch";
|
||||
|
||||
type WoTableRowServiceCellsProps = {
|
||||
row: WorkOrderTableRow;
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
search?: string;
|
||||
mutationCellStyle?: CSSProperties;
|
||||
viewOnly?: boolean;
|
||||
|
|
@ -30,6 +33,7 @@ export function WoTableRowServiceCells({
|
|||
row,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
search,
|
||||
mutationCellStyle,
|
||||
viewOnly = false,
|
||||
|
|
@ -49,6 +53,9 @@ export function WoTableRowServiceCells({
|
|||
<PMTypeCell
|
||||
value={row.pm}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
serviceId={row.serviceId}
|
||||
workOrderType={row.type}
|
||||
q={search}
|
||||
serviceNotes={row.serviceNotes}
|
||||
extraServices={row.extraServices}
|
||||
|
|
@ -56,10 +63,10 @@ export function WoTableRowServiceCells({
|
|||
avetaAttached={row.hasAvetaDocument}
|
||||
woNumber={row.woNumber}
|
||||
viewOnly={viewOnly}
|
||||
onSave={(pm) => onPatchRow({ pm })}
|
||||
onSave={(pm, id) => onPatchRow(id != null ? { pm, serviceId: id } : { pm })}
|
||||
onSaveDetails={(d) =>
|
||||
onPatchRow({
|
||||
pm: d.pm,
|
||||
...serviceIdentityPatch(row.serviceId, row.pm, d.pm, d.serviceId),
|
||||
extraServices: d.extraServices,
|
||||
serviceNotes: d.serviceNotes,
|
||||
avetaRequired: d.avetaRequired,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
resolveFirstCellShadow,
|
||||
resolveWoTableRowStyle,
|
||||
} from "./wo-table-row-style";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
|
||||
export type WoTableRowHandlers = {
|
||||
onRowClick: (row: WorkOrderTableRow) => void;
|
||||
|
|
@ -59,6 +60,7 @@ type WoTableRowProps = {
|
|||
sitePlace?: { city: string; state: string };
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
search?: string;
|
||||
forceScheduleHighlight?: boolean;
|
||||
forceDueDateHighlight?: boolean;
|
||||
|
|
@ -101,6 +103,7 @@ export function WoTableRow({
|
|||
sitePlace,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
search,
|
||||
forceScheduleHighlight,
|
||||
forceDueDateHighlight,
|
||||
|
|
@ -197,6 +200,7 @@ export function WoTableRow({
|
|||
allRows={allRows}
|
||||
users={users}
|
||||
sites={sites}
|
||||
services={services}
|
||||
sitePlace={sitePlace}
|
||||
search={search}
|
||||
forceScheduleHighlight={forceScheduleHighlight}
|
||||
|
|
@ -210,6 +214,7 @@ export function WoTableRow({
|
|||
<WoTableRowServiceCells
|
||||
row={row}
|
||||
vendors={vendors}
|
||||
services={services}
|
||||
pmTypes={pmTypes}
|
||||
search={search}
|
||||
mutationCellStyle={coreLockStyle}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { WoTableInlineRow } from "./wo-table-inline-row";
|
|||
import { WoTableRow } from "./wo-table-row";
|
||||
import { WoTableEmptyState, WoTableShell } from "./wo-table-shell";
|
||||
import { WO_TABLE_MIN_WIDTH } from "./wo-table-cols";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
|
||||
type WoTableProps = {
|
||||
tableData: ReturnTypeOfUseWorkOrdersTableData;
|
||||
|
|
@ -28,6 +29,7 @@ type WoTableProps = {
|
|||
sites: LocationOption[];
|
||||
vendors: VendorDropdownItem[];
|
||||
pmTypes: string[];
|
||||
services?: Service[];
|
||||
weekMonday: string;
|
||||
onNavigateToDay: (dateIso: string) => void;
|
||||
onOpenSlideOver: (row: WorkOrderTableRow, edit?: boolean) => void;
|
||||
|
|
@ -93,6 +95,7 @@ export function WoTable({
|
|||
sites,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services = [],
|
||||
weekMonday,
|
||||
onNavigateToDay,
|
||||
onOpenSlideOver,
|
||||
|
|
@ -141,6 +144,7 @@ export function WoTable({
|
|||
effectiveSitePlace(row) ?? resolveSitePlace(row.locationId, row.site, sitePlaceLookups)
|
||||
}
|
||||
vendors={vendors}
|
||||
services={services}
|
||||
pmTypes={pmTypes}
|
||||
search={tableData.search}
|
||||
forceScheduleHighlight={forceScheduleHighlight}
|
||||
|
|
@ -165,6 +169,7 @@ export function WoTable({
|
|||
sitePlaceLookups,
|
||||
)}
|
||||
vendors={vendors}
|
||||
services={services}
|
||||
pmTypes={pmTypes}
|
||||
allRows={tableData.rows}
|
||||
saving={saving}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,18 @@ type WorkOrdersListPagePanelsProps = {
|
|||
};
|
||||
|
||||
export function WorkOrdersListPagePanels({ page }: WorkOrdersListPagePanelsProps) {
|
||||
const { filters, tableData, activeDocRow, users, locations, vendors, pmTypes, vendorTechs } =
|
||||
page;
|
||||
const {
|
||||
filters,
|
||||
tableData,
|
||||
activeDocRow,
|
||||
users,
|
||||
locations,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services,
|
||||
serviceHistory,
|
||||
vendorTechs,
|
||||
} = page;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -44,7 +54,7 @@ export function WorkOrdersListPagePanels({ page }: WorkOrdersListPagePanelsProps
|
|||
onClearAll={filters.clearAdvancedFilters}
|
||||
sites={locations}
|
||||
users={users}
|
||||
pmTypes={pmTypes}
|
||||
services={serviceHistory}
|
||||
vendorTechs={vendorTechs}
|
||||
/>
|
||||
|
||||
|
|
@ -58,6 +68,7 @@ export function WorkOrdersListPagePanels({ page }: WorkOrdersListPagePanelsProps
|
|||
sites={locations}
|
||||
vendors={vendors}
|
||||
pmTypes={pmTypes}
|
||||
services={services}
|
||||
allOrders={tableData.rows}
|
||||
onSave={(id, patch, options) => page.patchField(id, patch, options)}
|
||||
onOpenCompletionDoc={(row) => openCompletionDoc(row, page.setDocRow)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import {
|
|||
} from "@/domain/work-orders/use-cases/use-work-order-detail";
|
||||
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
|
||||
import { useWorkOrderTableMutations } from "@/domain/work-orders/use-cases/use-work-order-table-mutations";
|
||||
import {
|
||||
useActiveServices,
|
||||
useAllServices,
|
||||
} from "@/domain/services/use-cases/use-services-for-work-order-type";
|
||||
import { shouldCompleteAfterCompletionDoc } from "@/domain/work-orders/utils/completion-doc-auto-complete";
|
||||
import type { CommentHighlight } from "@/domain/work-orders/utils/work-order-deep-link";
|
||||
import {
|
||||
|
|
@ -91,6 +95,8 @@ export function useWorkOrdersListPage() {
|
|||
const { data: locations = [] } = useWorkOrderLocations();
|
||||
const { data: vendors = [] } = useWorkOrderVendors();
|
||||
const { data: problemOptions = [] } = useDropdownOptionsByCategory("Problem");
|
||||
const { data: services = [] } = useActiveServices();
|
||||
const { data: serviceHistory = [] } = useAllServices();
|
||||
|
||||
const pmTypes = useMemo(
|
||||
() => problemOptions.map((option) => option.value).filter(Boolean),
|
||||
|
|
@ -195,6 +201,8 @@ export function useWorkOrdersListPage() {
|
|||
locations,
|
||||
vendors,
|
||||
pmTypes,
|
||||
services,
|
||||
serviceHistory,
|
||||
vendorTechs,
|
||||
activeSlideOverRow,
|
||||
activeDocRow,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ export default function WorkOrdersListPage() {
|
|||
sites={page.locations}
|
||||
vendors={page.vendors}
|
||||
pmTypes={page.pmTypes}
|
||||
services={page.services}
|
||||
weekMonday={filters.weekMonday}
|
||||
onNavigateToDay={filters.navigateToDay}
|
||||
onOpenSlideOver={page.handleOpenSlideOver}
|
||||
|
|
|
|||
|
|
@ -22,3 +22,12 @@ export function useActiveServices(): UseQueryResult<Service[], Error> {
|
|||
staleTime: 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Registry history is available to filters so inactive historical WOs remain findable. */
|
||||
export function useAllServices(): UseQueryResult<Service[], Error> {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.services.list(),
|
||||
queryFn: () => servicesApi.getAll(),
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
14
src/domain/services/utils/services-for-work-order-type.ts
Normal file
14
src/domain/services/utils/services-for-work-order-type.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { Service, ServiceWorkOrderType } from "@/domain/services/types/service";
|
||||
import type { WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||
|
||||
export function serviceWorkOrderType(type: WOType | ""): ServiceWorkOrderType {
|
||||
return type === "Reactive" || type === "Emergency" ? type : "PM";
|
||||
}
|
||||
|
||||
export function servicesForWorkOrderType(services: Service[], type: WOType | ""): Service[] {
|
||||
return services.filter((service) => isServiceEligibleForWorkOrderType(service, type));
|
||||
}
|
||||
|
||||
export function isServiceEligibleForWorkOrderType(service: Service, type: WOType | ""): boolean {
|
||||
return service.isActive && service.supportedWorkOrderTypes.includes(serviceWorkOrderType(type));
|
||||
}
|
||||
|
|
@ -27,6 +27,11 @@ const SCALAR_BOARD_OP_RULES: ScalarBoardOpRule[] = [
|
|||
field: "severity",
|
||||
toValue: (value) => (value == null ? "" : String(value)),
|
||||
},
|
||||
{
|
||||
key: "serviceId",
|
||||
field: "serviceId",
|
||||
toValue: (value) => (value == null ? "" : String(value)),
|
||||
},
|
||||
{ key: "type", field: "workOrderType", toValue: (value) => value as string },
|
||||
{ key: "site", field: "siteCode", toValue: (value) => value as string },
|
||||
{ key: "status", field: "lifecycleStatus", toValue: (value) => value as string },
|
||||
|
|
@ -58,6 +63,7 @@ export function expandScalarBoardOps(patch: WorkOrderTablePatch): BoardFieldOp[]
|
|||
for (const rule of SCALAR_BOARD_OP_RULES) {
|
||||
const value = patch[rule.key];
|
||||
if (value === undefined) continue;
|
||||
if (rule.key === "pm" && patch.serviceId !== undefined) continue;
|
||||
ops.push({
|
||||
field: rule.field,
|
||||
value: rule.toValue(value),
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export type WorkOrderBoardEditableField =
|
|||
| "apptTime"
|
||||
| "docStatus"
|
||||
| "completedDate"
|
||||
| "serviceId"
|
||||
| "pm"
|
||||
| "serviceNotes"
|
||||
| "avetaRequired"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export interface WorkOrderTableDraft {
|
|||
location: string;
|
||||
locationId: string | number;
|
||||
pm: string;
|
||||
serviceId?: string | number | null;
|
||||
serviceNotes: string;
|
||||
extraServices: string[];
|
||||
avetaRequired?: boolean;
|
||||
|
|
@ -164,6 +165,7 @@ export const EMPTY_TABLE_DRAFT: WorkOrderTableDraft = {
|
|||
location: "",
|
||||
locationId: "",
|
||||
pm: "",
|
||||
serviceId: null,
|
||||
serviceNotes: "",
|
||||
extraServices: [],
|
||||
avetaRequired: false,
|
||||
|
|
|
|||
19
src/domain/work-orders/utils/service-identity-patch.ts
Normal file
19
src/domain/work-orders/utils/service-identity-patch.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { WorkOrderTablePatch } from "@/domain/work-orders/types/work-order-table-row";
|
||||
|
||||
export function serviceIdentityPatch(
|
||||
currentServiceId: string | number | null | undefined,
|
||||
currentName: string,
|
||||
nextName: string,
|
||||
nextServiceId: string | number | null | undefined,
|
||||
): Pick<WorkOrderTablePatch, "pm" | "serviceId"> {
|
||||
const sameIdentity =
|
||||
currentServiceId != null &&
|
||||
nextServiceId != null &&
|
||||
String(currentServiceId) === String(nextServiceId);
|
||||
if (sameIdentity) return {};
|
||||
|
||||
if (nextServiceId != null) return { pm: nextName, serviceId: nextServiceId };
|
||||
if (currentServiceId != null) return { pm: nextName, serviceId: null };
|
||||
|
||||
return nextName === currentName ? {} : { pm: nextName };
|
||||
}
|
||||
|
|
@ -5,6 +5,50 @@ import { ServiceNotesDialog } from "@/app/(protected)/workorders/_components/lis
|
|||
import { renderWithProviders } from "@/test/test-utils";
|
||||
|
||||
describe("Aveta Service & Notes", () => {
|
||||
it("shows eligible registry services and saves the selected service identity", () => {
|
||||
const onSave = vi.fn();
|
||||
renderWithProviders(
|
||||
<PMTypeCell
|
||||
value="Old snapshot"
|
||||
serviceId={null}
|
||||
pmTypes={["Leak", "No Power", "Not Cooling", "Other"]}
|
||||
services={[
|
||||
{
|
||||
id: 701,
|
||||
name: "QA Test Service",
|
||||
trade: "Electrical",
|
||||
iconKey: "wrench",
|
||||
requiresCompletionDocument: false,
|
||||
completionDocTemplate: null,
|
||||
isActive: true,
|
||||
supportedWorkOrderTypes: ["PM"],
|
||||
},
|
||||
{
|
||||
id: 702,
|
||||
name: "Reactive Only",
|
||||
trade: "Electrical",
|
||||
iconKey: "wrench",
|
||||
requiresCompletionDocument: false,
|
||||
completionDocTemplate: null,
|
||||
isActive: true,
|
||||
supportedWorkOrderTypes: ["Reactive"],
|
||||
},
|
||||
]}
|
||||
workOrderType="PM"
|
||||
onSave={onSave}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Old snapshot/i }));
|
||||
|
||||
expect(screen.getByRole("button", { name: "QA Test Service" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Leak" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Reactive Only" })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "QA Test Service" }));
|
||||
expect(onSave).toHaveBeenCalledWith("QA Test Service", 701);
|
||||
});
|
||||
|
||||
it("shows the Aveta shield on the Service cell when required", () => {
|
||||
renderWithProviders(
|
||||
<PMTypeCell
|
||||
|
|
|
|||
|
|
@ -4,6 +4,27 @@ import { mapWizardDraftToBoardCreatePayload } from "@/domain/work-orders/mappers
|
|||
import { EMPTY_TABLE_DRAFT } from "@/domain/work-orders/types/work-order-table-row";
|
||||
|
||||
describe("buildInlineDraftCreateInput", () => {
|
||||
it("preserves the selected registry service identity", () => {
|
||||
const draft = Object.assign(
|
||||
{
|
||||
...EMPTY_TABLE_DRAFT,
|
||||
site: "BK5",
|
||||
locationId: "9",
|
||||
pm: "QA Test Service",
|
||||
},
|
||||
{ serviceId: 701 },
|
||||
);
|
||||
|
||||
const input = buildInlineDraftCreateInput(draft, [], [{ id: 9, name: "BK5 · Dallas, TX" }]);
|
||||
|
||||
expect(input.pm).toBe("QA Test Service");
|
||||
expect(input.serviceId).toBe(701);
|
||||
|
||||
const payload = mapWizardDraftToBoardCreatePayload(input);
|
||||
expect(payload.primaryService).toBe("QA Test Service");
|
||||
expect(payload.serviceId).toBe(701);
|
||||
});
|
||||
|
||||
it("defaults type to PM and omits schedule flags when undated", () => {
|
||||
const draft = {
|
||||
...EMPTY_TABLE_DRAFT,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ vi.mock("react-toastify", () => ({
|
|||
}));
|
||||
|
||||
import { saveExistingRowType } from "@/app/(protected)/workorders/_components/list/table/save-existing-row-type";
|
||||
import type { Service } from "@/domain/services/types/service";
|
||||
|
||||
const row = { id: 7, woNumber: "00000024965", severity: 3 as const };
|
||||
|
||||
|
|
@ -61,4 +62,49 @@ describe("saveExistingRowType", () => {
|
|||
expect(onPatch).toHaveBeenCalledWith(7, { type: "PM", severity: null });
|
||||
expect(onPatch.mock.calls[0]![2]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears an active service before changing to an unsupported work-order type", () => {
|
||||
const onPatch = vi.fn();
|
||||
const services: Service[] = [
|
||||
{
|
||||
id: 701,
|
||||
name: "PM Only Service",
|
||||
trade: "Electrical",
|
||||
iconKey: "wrench",
|
||||
requiresCompletionDocument: false,
|
||||
completionDocTemplate: null,
|
||||
isActive: true,
|
||||
supportedWorkOrderTypes: ["PM"],
|
||||
},
|
||||
];
|
||||
|
||||
saveExistingRowType(onPatch, { ...row, serviceId: 701 }, "Reactive", null, services);
|
||||
|
||||
expect(onPatch).toHaveBeenCalledWith(7, {
|
||||
type: "Reactive",
|
||||
severity: null,
|
||||
serviceId: null,
|
||||
pm: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an inactive historical service on type change", () => {
|
||||
const onPatch = vi.fn();
|
||||
const services: Service[] = [
|
||||
{
|
||||
id: 701,
|
||||
name: "Retired Service",
|
||||
trade: "Electrical",
|
||||
iconKey: "wrench",
|
||||
requiresCompletionDocument: false,
|
||||
completionDocTemplate: null,
|
||||
isActive: false,
|
||||
supportedWorkOrderTypes: ["PM"],
|
||||
},
|
||||
];
|
||||
|
||||
saveExistingRowType(onPatch, { ...row, serviceId: 701 }, "Reactive", null, services);
|
||||
|
||||
expect(onPatch).toHaveBeenCalledWith(7, { type: "Reactive", severity: null });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -102,9 +102,65 @@ vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category", () => ({
|
||||
useDropdownOptionsByCategory: () => ({ data: [{ value: "HVAC" }] }),
|
||||
useDropdownOptionsByCategory: () => ({
|
||||
data: [
|
||||
{ value: "HVAC" },
|
||||
{ value: "Leak" },
|
||||
{ value: "No Power" },
|
||||
{ value: "Not Cooling" },
|
||||
{ value: "Other" },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/domain/services/use-cases/use-services-for-work-order-type", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import("@/domain/services/use-cases/use-services-for-work-order-type")
|
||||
>();
|
||||
return {
|
||||
...actual,
|
||||
useActiveServices: () => ({
|
||||
data: [
|
||||
{
|
||||
id: 701,
|
||||
name: "QA Test Service",
|
||||
trade: "Electrical",
|
||||
iconKey: "wrench",
|
||||
requiresCompletionDocument: false,
|
||||
completionDocTemplate: null,
|
||||
isActive: true,
|
||||
supportedWorkOrderTypes: ["PM", "Reactive", "Emergency"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
useAllServices: () => ({
|
||||
data: [
|
||||
{
|
||||
id: 701,
|
||||
name: "QA Test Service",
|
||||
trade: "Electrical",
|
||||
iconKey: "wrench",
|
||||
requiresCompletionDocument: false,
|
||||
completionDocTemplate: null,
|
||||
isActive: true,
|
||||
supportedWorkOrderTypes: ["PM", "Reactive", "Emergency"],
|
||||
},
|
||||
{
|
||||
id: 702,
|
||||
name: "Retired Service",
|
||||
trade: "Electrical",
|
||||
iconKey: "wrench",
|
||||
requiresCompletionDocument: false,
|
||||
completionDocTemplate: null,
|
||||
isActive: false,
|
||||
supportedWorkOrderTypes: ["PM"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/domain/work-orders/mocks/is-wo-table-mock-enabled", () => ({
|
||||
isWoTableMockEnabled: () => false,
|
||||
}));
|
||||
|
|
@ -126,6 +182,24 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
|
|||
clearAdvancedFilters.mockClear();
|
||||
});
|
||||
|
||||
it("offers active registry services instead of legacy Problem dropdown values", () => {
|
||||
const view = renderWithProviders(<WorkOrdersListPage />, {
|
||||
route: "/workorders",
|
||||
withAuth: false,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Advanced Filters" }));
|
||||
advOpen = true;
|
||||
view.rerender(<WorkOrdersListPage />);
|
||||
|
||||
const sheet = screen.getByRole("dialog", { name: "Advanced Filters" });
|
||||
fireEvent.click(within(sheet).getByRole("button", { name: /^Service/i }));
|
||||
|
||||
expect(within(sheet).getByText("QA Test Service")).toBeInTheDocument();
|
||||
expect(within(sheet).getByText("Retired Service")).toBeInTheDocument();
|
||||
expect(within(sheet).queryByText("Leak")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the sheet from Advanced Filters, applies, then clears", () => {
|
||||
const view = renderWithProviders(<WorkOrdersListPage />, {
|
||||
route: "/workorders",
|
||||
|
|
@ -149,7 +223,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
|
|||
}
|
||||
expect(within(sheet).getByText("Seattle")).toBeInTheDocument();
|
||||
expect(within(sheet).getByText("Dispatcher One")).toBeInTheDocument();
|
||||
expect(within(sheet).getByText("HVAC")).toBeInTheDocument();
|
||||
expect(within(sheet).getByText("QA Test Service")).toBeInTheDocument();
|
||||
expect(within(sheet).getByText("Vendor Co")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(sheet).getByRole("button", { name: /Aveta Compliance/i }));
|
||||
|
|
@ -218,7 +292,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
|
|||
|
||||
const sheet = screen.getByRole("dialog", { name: "Advanced Filters" });
|
||||
fireEvent.click(within(sheet).getByRole("button", { name: /^Service/i }));
|
||||
fireEvent.click(within(sheet).getByText("HVAC"));
|
||||
fireEvent.click(within(sheet).getByText("QA Test Service"));
|
||||
|
||||
fireEvent.click(within(sheet).getByRole("button", { name: /Completion doc/i }));
|
||||
fireEvent.click(within(sheet).getByRole("button", { name: /^Pending$/i }));
|
||||
|
|
@ -230,13 +304,13 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
|
|||
expect(applyAdvancedFilters).toHaveBeenCalled();
|
||||
const appliedCalls = applyAdvancedFilters.mock.calls;
|
||||
const applied = appliedCalls[appliedCalls.length - 1]?.[0] as typeof EMPTY_ADVANCED_FILTERS;
|
||||
expect(applied.pmTypes.has("HVAC")).toBe(true);
|
||||
expect(applied.pmTypes.has("QA Test Service")).toBe(true);
|
||||
expect(applied.docs.has("No")).toBe(true);
|
||||
expect(applied.docs.has("Yes")).toBe(true);
|
||||
expect(applied.docs.has("NN")).toBe(true);
|
||||
|
||||
const params = toBoardSearchQueryParams({ filters: applied });
|
||||
expect(params.pmTypes).toEqual(["HVAC"]);
|
||||
expect(params.pmTypes).toEqual(["QA Test Service"]);
|
||||
expect(params.docStatuses).toEqual([2, 1, 3]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,19 @@ describe("expandTablePatchToBoardFieldOps", () => {
|
|||
expect(expandTablePatchToBoardFieldOps({ pm: "HVAC" })).toEqual([
|
||||
{ field: "pm", value: "HVAC" },
|
||||
]);
|
||||
expect(expandTablePatchToBoardFieldOps({ pm: "QA Test Service", serviceId: 701 })).toEqual([
|
||||
{ field: "serviceId", value: "701" },
|
||||
]);
|
||||
expect(expandTablePatchToBoardFieldOps({ serviceId: null, pm: "" })).toEqual([
|
||||
{ field: "serviceId", value: "" },
|
||||
]);
|
||||
expect(
|
||||
expandTablePatchToBoardFieldOps({ severity: 2, serviceId: null, type: "Reactive", pm: "" }),
|
||||
).toEqual([
|
||||
{ field: "severity", value: "2" },
|
||||
{ field: "serviceId", value: "" },
|
||||
{ field: "workOrderType", value: "Reactive" },
|
||||
]);
|
||||
expect(expandTablePatchToBoardFieldOps({ docStatus: "NN" })).toEqual([
|
||||
{ field: "docStatus", value: "NotNeeded" },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { serviceIdentityPatch } from "@/domain/work-orders/utils/service-identity-patch";
|
||||
|
||||
describe("serviceIdentityPatch", () => {
|
||||
it("keeps an unchanged registry identity out of unrelated detail saves", () => {
|
||||
expect(serviceIdentityPatch(701, "Old snapshot", "QA Test Service", 701)).toEqual({});
|
||||
});
|
||||
|
||||
it("emits the registry identity when selecting a registered service", () => {
|
||||
expect(serviceIdentityPatch(null, "Legacy value", "QA Test Service", 701)).toEqual({
|
||||
pm: "QA Test Service",
|
||||
serviceId: 701,
|
||||
});
|
||||
});
|
||||
|
||||
it("clears a registry identity when the selection is removed", () => {
|
||||
expect(serviceIdentityPatch(701, "QA Test Service", "", null)).toEqual({
|
||||
pm: "",
|
||||
serviceId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps legacy PM edits label-based", () => {
|
||||
expect(serviceIdentityPatch(null, "Legacy value", "Another legacy value", undefined)).toEqual({
|
||||
pm: "Another legacy value",
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue