diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx index 1a99666c..631d75f4 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx @@ -106,20 +106,22 @@ function SlideOverTypeField({ const linkedService = typeServices.find( (service) => String(service.id) === String(draft.serviceId), ); - const registryUnavailable = draft.serviceId != null && (!serviceHistoryReady || !linkedService); + const registryUnavailable = draft.serviceId != null && !serviceHistoryReady; + const linkedServiceMissing = + draft.serviceId != null && serviceHistoryReady && linkedService == null; return ( { if (registryUnavailable) return; onDraftChange( draft.serviceId != null && - linkedService != null && - !serviceSupportsWorkOrderType(linkedService, type) + (linkedService == null || !serviceSupportsWorkOrderType(linkedService, type)) ? { type, severity, serviceId: null, pm: "" } : { type, severity }, ); diff --git a/src/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog.tsx index 0705ac47..edd1cf79 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/service-notes-dialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type SetStateAction } from "react"; import { Wrench } from "lucide-react"; import { Dialog, DialogContent, DialogTitle } from "@mui/material"; import { WizardServiceNotesFields } from "@/app/(protected)/workorders/_components/wizard/wizard-service-notes-fields"; @@ -16,6 +16,75 @@ export type ServiceNotesDetails = { serviceId?: Service["id"] | null; }; +type ServiceNotesSelectionFieldsProps = { + pm: string; + extraServices: string[]; + pmTypes: string[]; + services?: Service[]; + serviceQuery?: ServiceRegistryQueryState; + serviceOptionsUnavailable: boolean; + serviceOptionsEmpty: boolean; + sel: string[]; + onToggle: (value: string, optionId?: string | number) => void; + note: string; + setNote: (value: string) => void; + search: string; + setSearch: (value: string) => void; + aveta: boolean; + setAveta: (value: boolean) => void; + viewOnly: boolean; +}; + +function ServiceNotesSelectionFields({ + pm, + extraServices, + pmTypes, + services, + serviceQuery, + serviceOptionsUnavailable, + serviceOptionsEmpty, + sel, + onToggle, + note, + setNote, + search, + setSearch, + aveta, + setAveta, + viewOnly, +}: ServiceNotesSelectionFieldsProps) { + 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 })); + + return ( +
+ + +
+ ); +} + type ServiceNotesDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; @@ -53,6 +122,96 @@ function selectedServices(pm: string, extraServices: string[]) { ); } +function createServiceSelectionToggle({ + services, + selected, + primaryServiceId, + serviceIdsByName, + setSelected, + setPrimaryServiceId, + setServiceIdsByName, +}: { + services?: Service[]; + selected: string[]; + primaryServiceId?: string | number | null; + serviceIdsByName: Record; + setSelected: (value: string[]) => void; + setPrimaryServiceId: (value: string | number | null | undefined) => void; + setServiceIdsByName: (value: SetStateAction>) => void; +}) { + return (value: string, optionId?: string | number) => { + const option = services?.find((service) => String(service.id) === String(optionId)); + const optionServiceId = option?.id; + const replacesSameLabel = + selected[0] === value && + optionServiceId != null && + String(optionServiceId) !== String(primaryServiceId); + if (replacesSameLabel) { + setPrimaryServiceId(optionServiceId); + setServiceIdsByName((current) => ({ ...current, [value]: optionServiceId })); + return; + } + if (selected.includes(value)) { + const next = selected.filter((item) => item !== value); + setSelected(next); + if (selected[0] === value) { + const nextName = next[0]; + const nextService = services?.find((service) => service.name === nextName); + setPrimaryServiceId( + nextName == null ? undefined : (serviceIdsByName[nextName] ?? nextService?.id), + ); + } + return; + } + setSelected([...selected, value]); + if (optionServiceId != null) { + setServiceIdsByName((current) => ({ ...current, [value]: optionServiceId })); + if (selected.length === 0) setPrimaryServiceId(optionServiceId); + } + }; +} + +function resolveSelectedServiceId({ + unavailable, + selected, + currentPm, + savedServiceId, + selectedServiceId, +}: { + unavailable: boolean; + selected: string[]; + currentPm: string; + savedServiceId?: string | number | null; + selectedServiceId?: string | number | null; +}) { + if (unavailable) return savedServiceId ?? undefined; + if ( + selected[0] === currentPm && + String(savedServiceId ?? "") === String(selectedServiceId ?? "") + ) { + return savedServiceId ?? undefined; + } + if (selected[0] != null && selectedServiceId != null) return selectedServiceId; + if (selected[0] === currentPm && savedServiceId != null) return savedServiceId; + return savedServiceId == null ? undefined : null; +} + +function getServiceOptionState( + services: Service[] | undefined, + query: ServiceRegistryQueryState | undefined, +) { + const managed = services !== undefined; + return { + unavailable: managed && query != null && !query.hasData, + empty: + managed && + query?.hasData === true && + !query.isLoading && + !query.isError && + services.length === 0, + }; +} + function useServiceNotesDraft({ open, target, @@ -61,6 +220,7 @@ function useServiceNotesDraft({ extraServices, serviceNotes, avetaRequired, + services, }: { open: boolean; target: DialogTarget; @@ -69,10 +229,13 @@ function useServiceNotesDraft({ extraServices: string[]; serviceNotes: string; avetaRequired: boolean; + services?: Service[]; }) { const [sel, setSel] = useState([]); const [note, setNote] = useState(""); const [aveta, setAveta] = useState(false); + const [primaryServiceId, setPrimaryServiceId] = useState(); + const [serviceIdsByName, setServiceIdsByName] = useState>({}); const [search, setSearch] = useState(""); const lastTarget = useRef(null); const { key: targetKey, pm: targetPm, serviceId: targetServiceId } = target; @@ -85,6 +248,16 @@ function useServiceNotesDraft({ const previous = lastTarget.current; if (previous == null || previous.key !== targetKey) { setSel(selectedServices(pm, extraServices)); + setPrimaryServiceId(serviceId); + setServiceIdsByName({ + ...(serviceId != null ? { [pm]: serviceId } : {}), + ...Object.fromEntries( + extraServices.flatMap((name) => { + const service = services?.find((item) => item.name === name); + return service == null ? [] : [[name, service.id]]; + }), + ), + }); setNote(serviceNotes ?? ""); setAveta(Boolean(avetaRequired)); setSearch(""); @@ -94,6 +267,7 @@ function useServiceNotesDraft({ previous.pm !== pm ) { setSel((current) => (current[0] === previous.pm ? [pm, ...current.slice(1)] : current)); + setServiceIdsByName((current) => ({ ...current, [pm]: serviceId! })); } lastTarget.current = { key: targetKey, pm: targetPm, serviceId: targetServiceId }; }, [ @@ -104,11 +278,25 @@ function useServiceNotesDraft({ pm, serviceId, extraServices, + services, serviceNotes, avetaRequired, ]); - return { sel, setSel, note, setNote, aveta, setAveta, search, setSearch }; + return { + sel, + setSel, + note, + setNote, + aveta, + setAveta, + search, + setSearch, + primaryServiceId, + setPrimaryServiceId, + serviceIdsByName, + setServiceIdsByName, + }; } export function ServiceNotesDialog({ @@ -127,7 +315,20 @@ export function ServiceNotesDialog({ onSave, }: ServiceNotesDialogProps) { const target = getDialogTarget(woNumber, serviceId, pm); - const { sel, setSel, note, setNote, aveta, setAveta, search, setSearch } = useServiceNotesDraft({ + const { + sel, + setSel, + note, + setNote, + aveta, + setAveta, + search, + setSearch, + primaryServiceId, + setPrimaryServiceId, + serviceIdsByName, + setServiceIdsByName, + } = useServiceNotesDraft({ open, target, pm, @@ -135,37 +336,27 @@ export function ServiceNotesDialog({ extraServices, serviceNotes, avetaRequired, + services, }); - const toggle = (value: string) => { - setSel((cur) => (cur.includes(value) ? cur.filter((x) => x !== value) : [...cur, 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 = () => { - if (serviceOptionsUnavailable) return serviceId ?? undefined; - const selectedId = services?.find((service) => service.name === sel[0])?.id; - if (selectedId != null) return selectedId; - if (sel[0] === pm) return serviceId ?? undefined; - return serviceId == null ? undefined : null; - }; - const registryManaged = services !== undefined; - const serviceOptionsUnavailable = - registryManaged && serviceQuery != null && !serviceQuery.hasData; - const serviceOptionsEmpty = - registryManaged && - serviceQuery?.hasData === true && - !serviceQuery.isLoading && - !serviceQuery.isError && - services.length === 0; - + const toggle = createServiceSelectionToggle({ + services, + selected: sel, + primaryServiceId, + serviceIdsByName, + setSelected: setSel, + setPrimaryServiceId, + setServiceIdsByName, + }); + const optionState = getServiceOptionState(services, serviceQuery); + const saveServiceId = () => + resolveSelectedServiceId({ + unavailable: optionState.unavailable, + selected: sel, + currentPm: pm, + savedServiceId: serviceId, + selectedServiceId: primaryServiceId, + }); return ( e.stopPropagation()} style={{ paddingTop: 8 }} > -
- - -
+
{viewOnly ? (