diff --git a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx index f93a1166..6238d1df 100644 --- a/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx +++ b/src/app/(protected)/workorders/_components/comp-doc/use-comp-doc-dialog.tsx @@ -31,6 +31,14 @@ type UseCompDocDialogArgs = { onDateCompletedChange?: (date: string) => void | Promise; }; +function completionDocTechnician( + assigned: boolean | null | undefined, + frozenAssigned: boolean | null | undefined, + contactName: string, +): string { + return assigned === false || frozenAssigned === false ? "" : contactName || ""; +} + function notifyDraftSaved() { toast(
void; }; +type TechnicianKeyArgs = Pick< + UseVendorDialogFormArgs, + "technicianAssigned" | "vendorId" | "tech" | "company" | "vendors" +>; + +function resolveInitialTechnicianKey(args: TechnicianKeyArgs): string { + if (args.technicianAssigned === false) return NO_TECHNICIAN_KEY; + return resolveTechnicianKey(args.vendorId, args.tech, args.company, args.vendors); +} + +function buildSubmitBaseline(args: TechnicianKeyArgs & { techPhone: string }) { + const options = buildTechnicianOptions(args.vendors, args.company, args.tech, args.company); + return { + company: args.company, + selectedKey: resolveInitialTechnicianKey(args), + phone: args.techPhone || phoneForOption(args.tech, options), + notes: "", + pendingCreateCount: 0, + }; +} + +function createCompanyPickHandler(args: { + selectedKey: string; + technicianOptions: ReturnType; + setSelectedCompany: (company: string) => void; + setAddedVendors: Dispatch>; + setPendingCreates: Dispatch>>; + setSelectedKey: (key: string) => void; + setT: (value: string) => void; + setPhone: (value: string) => void; +}) { + return (nextCompany: string) => { + const selectedOption = args.technicianOptions.find((option) => option.key === args.selectedKey); + args.setSelectedCompany(nextCompany); + if (selectedOption?.company === nextCompany) return; + args.setAddedVendors([]); + args.setPendingCreates({}); + args.setSelectedKey(""); + args.setT(""); + args.setPhone(""); + }; +} + +function createTechnicianPickHandler(args: { + technicianOptions: ReturnType; + setSelectedKey: (key: string) => void; + setT: (value: string) => void; + setPhone: (value: string) => void; +}) { + return (key: string) => { + if (key === NO_TECHNICIAN_KEY) { + args.setSelectedKey(key); + args.setT(""); + args.setPhone(""); + return; + } + const option = args.technicianOptions.find((item) => item.key === key); + args.setSelectedKey(key); + args.setT(option?.name ?? key); + args.setPhone(option?.phone || phoneForOption(option?.name ?? key, args.technicianOptions)); + }; +} + export function useVendorDialogForm({ open, onOpenChange, @@ -44,10 +107,12 @@ export function useVendorDialogForm({ }: UseVendorDialogFormArgs) { const addTechnicians = useAddTechniciansToCompany(); const [selectedCompany, setSelectedCompany] = useState(company); + const technicianKeyArgs = useMemo( + () => ({ technicianAssigned, vendorId, tech, company, vendors }), + [technicianAssigned, vendorId, tech, company, vendors], + ); const [selectedKey, setSelectedKey] = useState(() => - technicianAssigned === false - ? NO_TECHNICIAN_KEY - : resolveTechnicianKey(vendorId, tech, company, vendors), + resolveInitialTechnicianKey(technicianKeyArgs), ); const [t, setT] = useState(tech); const [phone, setPhone] = useState(techPhone); @@ -73,10 +138,7 @@ export function useVendorDialogForm({ useEffect(() => { if (open) { - const key = - technicianAssigned === false - ? NO_TECHNICIAN_KEY - : resolveTechnicianKey(vendorId, tech, company, vendors); + const key = resolveInitialTechnicianKey(technicianKeyArgs); const initialOptions = buildTechnicianOptions(vendors, company, tech, company); setSelectedCompany(company); setSelectedKey(key); @@ -88,7 +150,7 @@ export function useVendorDialogForm({ setSaveError(null); } // invariant: rebuilding options after Add must not clear staged technician drafts. - }, [open, tech, techPhone, vendors, company, vendorId, technicianAssigned]); + }, [open, tech, techPhone, vendors, company, vendorId, technicianAssigned, technicianKeyArgs]); const selected = technicianOptions.find((option) => option.key === selectedKey); const resolvedCompany = selectedCompany; @@ -112,14 +174,7 @@ export function useVendorDialogForm({ }, baseline: { company, - selectedKey: - technicianAssigned === false - ? NO_TECHNICIAN_KEY - : resolveTechnicianKey(vendorId, tech, company, vendors), - phone: - techPhone || phoneForOption(tech, buildTechnicianOptions(vendors, company, tech, company)), - notes: "", - pendingCreateCount: 0, + ...buildSubmitBaseline({ ...technicianKeyArgs, techPhone }), }, }); @@ -141,6 +196,23 @@ export function useVendorDialogForm({ canSubmit, }); + const handleCompanyPick = createCompanyPickHandler({ + selectedKey, + technicianOptions, + setSelectedCompany, + setAddedVendors, + setPendingCreates, + setSelectedKey, + setT, + setPhone, + }); + const handlePick = createTechnicianPickHandler({ + technicianOptions, + setSelectedKey, + setT, + setPhone, + }); + return { companyOptions, technicianOptions, @@ -158,29 +230,8 @@ export function useVendorDialogForm({ canSubmit, isPending: addTechnicians.isPending, handleSave, - handleCompanyPick: (nextCompany: string) => { - const selectedOption = technicianOptions.find((option) => option.key === selectedKey); - setSelectedCompany(nextCompany); - if (selectedOption?.company !== nextCompany) { - setAddedVendors([]); - setPendingCreates({}); - setSelectedKey(""); - setT(""); - setPhone(""); - } - }, - handlePick: (key: string) => { - if (key === NO_TECHNICIAN_KEY) { - setSelectedKey(key); - setT(""); - setPhone(""); - return; - } - const option = technicianOptions.find((o) => o.key === key); - setSelectedKey(key); - setT(option?.name ?? key); - setPhone(option?.phone || phoneForOption(option?.name ?? key, technicianOptions)); - }, + handleCompanyPick, + handlePick, handleTechnicianCreated: ( created: VendorDropdownItem, createdPhone: string, diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-save.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-save.ts index c8959380..f02ea2c3 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-save.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-save.ts @@ -22,6 +22,138 @@ type UseVendorDialogSaveArgs = { canSubmit: boolean; }; +type VendorResolutionArgs = Pick< + UseVendorDialogSaveArgs, + "allVendors" | "selectedKey" | "selectedVendor" | "resolvedCompany" | "anchorCompanyName" | "tech" +>; + +function resolveSaveVendor(args: VendorResolutionArgs): VendorDropdownItem | undefined { + return ( + (args.selectedKey + ? args.allVendors.find((vendor) => String(vendor.id) === args.selectedKey) + : undefined) ?? + args.allVendors.find( + (vendor) => + vendor.contactName === args.tech && + vendor.companyName === (args.resolvedCompany || args.anchorCompanyName), + ) ?? + args.selectedVendor + ); +} + +async function resolvePersistedVendor(args: { + vendor: VendorDropdownItem | undefined; + allVendors: VendorDropdownItem[]; + pendingCreates: Record; + phone: string; + addTechnicians: ReturnType; + setSaveError: (error: string | null) => void; +}): Promise { + const payload = args.vendor ? args.pendingCreates[String(args.vendor.id)] : undefined; + if (!args.vendor || !isDraftVendorId(args.vendor.id) || !payload) return args.vendor; + try { + return await persistDraftTechnician({ + allVendors: args.allVendors, + pendingPayload: payload, + phone: args.phone, + addTechnicians: args.addTechnicians, + }); + } catch (error) { + args.setSaveError(error instanceof Error ? error.message : "Failed to create technician"); + return null; + } +} + +function resolveVendorForSave(args: { + vendor: VendorDropdownItem | undefined; + allVendors: VendorDropdownItem[]; + pendingCreates: Record; + phone: string; + addTechnicians: ReturnType; + setSaveError: (error: string | null) => void; +}): VendorDropdownItem | undefined | Promise { + const payload = args.vendor ? args.pendingCreates[String(args.vendor.id)] : undefined; + if (!args.vendor || !isDraftVendorId(args.vendor.id) || !payload) return args.vendor; + return resolvePersistedVendor(args); +} + +function isNoTechnician(key: string): boolean { + return key === "" || key === "__no_technician__"; +} + +function resolveAssignmentVendorId(args: { + noTechnician: boolean; + saveVendor: VendorDropdownItem | undefined; + vendorId: string | number; + resolvedCompany: string; + originalCompany: string; +}): string { + if (!args.noTechnician && args.saveVendor) return String(args.saveVendor.id); + if ( + args.noTechnician && + args.resolvedCompany === args.originalCompany && + args.vendorId !== "" && + args.vendorId != null + ) { + return String(args.vendorId); + } + return args.noTechnician && args.saveVendor ? String(args.saveVendor.id) : ""; +} + +function buildVendorPatch(args: { + selectedKey: string; + saveVendor: VendorDropdownItem | undefined; + resolvedCompany: string; + anchorCompanyName: string; + tech: string; + phone: string; + vendorId: string | number; + originalCompany: string; +}): VendorPatch { + const assigned = args.selectedKey !== "" && !isNoTechnician(args.selectedKey); + const noTechnician = isNoTechnician(args.selectedKey); + return { + vendorId: resolveAssignmentVendorId({ + noTechnician, + saveVendor: args.saveVendor, + vendorId: args.vendorId, + resolvedCompany: args.resolvedCompany, + originalCompany: args.originalCompany, + }), + company: args.resolvedCompany || args.anchorCompanyName, + tech: assigned ? (args.saveVendor?.contactName ?? args.tech) : "", + techPhone: assigned ? args.phone : "", + technicianAssigned: assigned, + }; +} + +function commitVendorPatch(args: { + saveVendor: VendorDropdownItem | undefined; + selectedKey: string; + resolvedCompany: string; + anchorCompanyName: string; + tech: string; + phone: string; + vendorId: string | number; + originalCompany: string; + onSave: (patch: VendorPatch) => void; + onOpenChange: (open: boolean) => void; +}): void { + args.onSave( + buildVendorPatch({ + saveVendor: args.saveVendor, + selectedKey: args.selectedKey, + resolvedCompany: args.resolvedCompany, + anchorCompanyName: args.anchorCompanyName, + tech: args.tech, + phone: args.phone, + vendorId: args.vendorId, + originalCompany: args.originalCompany, + }), + ); + args.onOpenChange(false); +} + /** Persists a staged draft technician under its existing company (additive roster PATCH). */ async function persistDraftTechnician({ allVendors, @@ -71,54 +203,54 @@ export function useVendorDialogSave({ setSaveError, canSubmit, }: UseVendorDialogSaveArgs) { - const resolveSaveVendor = () => - (selectedKey ? allVendors.find((vendor) => String(vendor.id) === selectedKey) : undefined) ?? - allVendors.find( - (vendor) => - vendor.contactName === tech && - vendor.companyName === (resolvedCompany || anchorCompanyName), - ) ?? - selectedVendor; - const handleSave = async () => { if (!canSubmit) return; setSaveError(null); - let saveVendor = resolveSaveVendor(); - const pendingPayload = saveVendor ? pendingCreates[String(saveVendor.id)] : undefined; - - if (saveVendor != null && isDraftVendorId(saveVendor.id) && pendingPayload) { - try { - saveVendor = await persistDraftTechnician({ - allVendors, - pendingPayload, - phone, - addTechnicians, - }); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to create technician"; - setSaveError(message); - return; - } - } - - const noTechnician = selectedKey === "" || selectedKey === "__no_technician__"; - const preservedVendorId = - noTechnician && resolvedCompany === originalCompany && vendorId !== "" && vendorId != null - ? String(vendorId) - : ""; - onSave({ - vendorId: - selectedKey && !noTechnician - ? saveVendor - ? String(saveVendor.id) - : "" - : preservedVendorId || (noTechnician && saveVendor ? String(saveVendor.id) : ""), - company: resolvedCompany || anchorCompanyName, - tech: selectedKey && !noTechnician ? (saveVendor?.contactName ?? tech) : "", - techPhone: selectedKey && !noTechnician ? phone : "", - technicianAssigned: Boolean(selectedKey && !noTechnician), + const vendor = resolveSaveVendor({ + allVendors, + selectedKey, + selectedVendor, + resolvedCompany, + anchorCompanyName, + tech, + }); + const resolvedVendor = resolveVendorForSave({ + vendor, + allVendors, + pendingCreates, + phone, + addTechnicians, + setSaveError, + }); + if (resolvedVendor instanceof Promise) { + const saveVendor = await resolvedVendor; + if (saveVendor === null) return; + commitVendorPatch({ + saveVendor, + selectedKey, + resolvedCompany, + anchorCompanyName, + tech, + phone, + vendorId, + originalCompany, + onSave, + onOpenChange, + }); + return; + } + commitVendorPatch({ + saveVendor: resolvedVendor, + selectedKey, + resolvedCompany, + anchorCompanyName, + tech, + phone, + vendorId, + originalCompany, + onSave, + onOpenChange, }); - onOpenChange(false); }; return { handleSave }; diff --git a/src/domain/work-orders/mappers/work-order-table-row-mapper.ts b/src/domain/work-orders/mappers/work-order-table-row-mapper.ts index b84a0a8b..b325abe3 100644 --- a/src/domain/work-orders/mappers/work-order-table-row-mapper.ts +++ b/src/domain/work-orders/mappers/work-order-table-row-mapper.ts @@ -84,6 +84,26 @@ function readNullableBool( return undefined; } +function resolveTechnicianAssignment( + rowAssigned: boolean | null | undefined, + frozenAssigned: boolean | null | undefined, +): boolean | null | undefined { + if (rowAssigned === false || frozenAssigned === false) return false; + if (rowAssigned !== undefined) return rowAssigned; + return frozenAssigned; +} + +function readEffectiveTechnicianFields( + item: Record, + assigned: boolean | null | undefined, +): { tech: string; techPhone: string } { + if (assigned === false) return { tech: "", techPhone: "" }; + return { + tech: readString(item, "techName", "TechName", "tech", "Tech", "technician", "Technician"), + techPhone: readString(item, "techPhone", "TechPhone"), + }; +} + function readStringArray(record: Record, ...keys: string[]): string[] { for (const key of keys) { const value = record[key]; @@ -396,12 +416,11 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow { const upliftSummary = boardUpliftSummary == null ? undefined : mapUpliftSummary(item); const frozenCompany = readFrozenCompany(item); const rowTechnicianAssigned = readNullableBool(item, "technicianAssigned", "TechnicianAssigned"); - const technicianAssigned = - rowTechnicianAssigned === false || frozenCompany?.technicianAssigned === false - ? false - : rowTechnicianAssigned !== undefined - ? rowTechnicianAssigned - : frozenCompany?.technicianAssigned; + const technicianAssigned = resolveTechnicianAssignment( + rowTechnicianAssigned, + frozenCompany?.technicianAssigned, + ); + const technician = readEffectiveTechnicianFields(item, technicianAssigned); const row: WorkOrderTableRow = { id: readId(item), @@ -428,11 +447,8 @@ export function mapWorkOrderTableRow(raw: unknown): WorkOrderTableRow { vendorId: vendorIdNum != null ? String(vendorIdNum) : "", company: readString(item, "company", "Company", "vendorName", "VendorName"), primaryDispatchStatus: readPrimaryDispatchStatus(item), - tech: - technicianAssigned === false - ? "" - : readString(item, "techName", "TechName", "tech", "Tech", "technician", "Technician"), - techPhone: technicianAssigned === false ? "" : readString(item, "techPhone", "TechPhone"), + tech: technician.tech, + techPhone: technician.techPhone, ...(technicianAssigned !== undefined ? { technicianAssigned } : {}), apptStart, apptEnd,