mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
refactor(work-orders): keep technician assignment paths maintainable
This commit is contained in:
parent
262c93b41c
commit
55b5441d71
4 changed files with 307 additions and 99 deletions
|
|
@ -31,6 +31,14 @@ type UseCompDocDialogArgs = {
|
|||
onDateCompletedChange?: (date: string) => void | Promise<void>;
|
||||
};
|
||||
|
||||
function completionDocTechnician(
|
||||
assigned: boolean | null | undefined,
|
||||
frozenAssigned: boolean | null | undefined,
|
||||
contactName: string,
|
||||
): string {
|
||||
return assigned === false || frozenAssigned === false ? "" : contactName || "";
|
||||
}
|
||||
|
||||
function notifyDraftSaved() {
|
||||
toast(
|
||||
<div
|
||||
|
|
@ -116,10 +124,11 @@ export function useCompDocDialog({
|
|||
amazonRep,
|
||||
seaRep,
|
||||
siteCode: site.code,
|
||||
tech:
|
||||
info.technicianAssigned === false || company.technicianAssigned === false
|
||||
? ""
|
||||
: company.contactName || "",
|
||||
tech: completionDocTechnician(
|
||||
info.technicianAssigned,
|
||||
company.technicianAssigned,
|
||||
company.contactName,
|
||||
),
|
||||
company: company.name || "",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema";
|
||||
import { useAddTechniciansToCompany } from "@/domain/vendors/use-cases/use-add-technicians-to-company";
|
||||
|
|
@ -31,6 +31,69 @@ type UseVendorDialogFormArgs = {
|
|||
onSave: (patch: VendorPatch) => 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<typeof buildTechnicianOptions>;
|
||||
setSelectedCompany: (company: string) => void;
|
||||
setAddedVendors: Dispatch<SetStateAction<VendorDropdownItem[]>>;
|
||||
setPendingCreates: Dispatch<SetStateAction<Record<string, VendorFormValues>>>;
|
||||
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<typeof buildTechnicianOptions>;
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<string, VendorFormValues>;
|
||||
phone: string;
|
||||
addTechnicians: ReturnType<typeof useAddTechniciansToCompany>;
|
||||
setSaveError: (error: string | null) => void;
|
||||
}): Promise<VendorDropdownItem | null> {
|
||||
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<string, VendorFormValues>;
|
||||
phone: string;
|
||||
addTechnicians: ReturnType<typeof useAddTechniciansToCompany>;
|
||||
setSaveError: (error: string | null) => void;
|
||||
}): VendorDropdownItem | undefined | Promise<VendorDropdownItem | null> {
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>,
|
||||
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<string, unknown>, ...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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue