import { useCallback, useRef, useState } from "react"; import { useQueryClient, type UseQueryResult } from "@tanstack/react-query"; import { emptyRosterTechnician, emptyVendorCompanyRosterForm, type VendorCompanyRosterFormValues, } from "@/domain/vendors/schemas/vendor-roster-schema"; import { vendorCompanyRosterApi } from "@/domain/vendors/api/vendor-company-roster-api"; import { queryKeys } from "@/infra/query-key/query-key"; import type { VendorCompanyRoster, VendorFacetCompany } from "@/domain/vendors/types/vendor"; import type { UseFormReset } from "react-hook-form"; const SELECT_ERROR_MESSAGE = "Couldn't load that company's roster. Try again."; const RELOAD_ERROR_MESSAGE = "Couldn't reload the latest company. Try again."; export interface VendorRosterSelection { selectedRoster: VendorCompanyRoster | null; loadErrorMessage: string | null; selectedCompanyId: string | number | null; selectCompany: (company: VendorFacetCompany | null) => Promise; clearSelectedCompany: (nextName?: string) => void; reload: () => void; retryLoad: () => void; resetSelection: () => void; } interface UseVendorRosterSelectionOptions { mode: "create" | "update"; reset: UseFormReset; query: UseQueryResult; clearConflict: () => void; } export function toFormValues(roster: VendorCompanyRoster): VendorCompanyRosterFormValues { return { name: roster.name, companyPhone: roster.companyPhone, email: roster.email, address: roster.address, city: roster.city, state: roster.state, zip: roster.zip, // why: The deprecated stored Maps URL is intentionally not seeded: the form has no input for it, and // carrying it would re-emit a legacy value on write. notes: roster.notes, areaId: roster.areaId, technicians: roster.technicians.map((technician) => ({ id: technician.id, contactName: technician.contactName, phone: technician.phone, email: technician.email, preferredContact: technician.preferredContact, tradeSpecialties: technician.tradeSpecialties, isActive: technician.isActive, })), }; } export function toCompanyFormValues(roster: VendorCompanyRoster): VendorCompanyRosterFormValues { return { ...toFormValues(roster), technicians: [] }; } function withCompanyFieldsOnly( roster: VendorCompanyRoster, ): (current: VendorCompanyRosterFormValues) => VendorCompanyRosterFormValues { return (current) => ({ ...toCompanyFormValues(roster), technicians: current.technicians.length > 0 ? current.technicians : [{ ...emptyRosterTechnician }], }); } export function useVendorRosterSelection({ mode, reset, query, clearConflict, }: UseVendorRosterSelectionOptions): VendorRosterSelection { const queryClient = useQueryClient(); const [selectedRoster, setSelectedRoster] = useState(null); const [loadErrorMessage, setLoadErrorMessage] = useState(null); const retryTargetRef = useRef< { kind: "select"; company: VendorFacetCompany } | { kind: "reload" } | null >(null); const requestIdRef = useRef(0); const fetchRosterByCompany = useCallback( async (id: string | number): Promise => queryClient.fetchQuery({ queryKey: queryKeys.vendors.roster("companyId", id), queryFn: () => vendorCompanyRosterApi.get({ companyId: id }), // SH-250: this fetch exists to obtain the freshest rowVersion, so it must not // serve the global 5-minute staleTime cache. A conflict never invalidates this // key (useSaveVendorCompanyRoster invalidates only on success, and its onError // returns early for conflicts), and the Add flow has no mounted observer on it. // Without staleTime: 0 the Reload button re-seeds the same stale rowVersion and // the retry 409s again indefinitely. staleTime: 0, }), [queryClient], ); const selectCompany = useCallback( async (company: VendorFacetCompany | null) => { if (!company || company.companyId == null) { requestIdRef.current += 1; setSelectedRoster(null); setLoadErrorMessage(null); retryTargetRef.current = null; return; } const requestId = (requestIdRef.current += 1); setLoadErrorMessage(null); retryTargetRef.current = { kind: "select", company }; try { const roster = await fetchRosterByCompany(company.companyId); if (requestIdRef.current !== requestId) return; reset(withCompanyFieldsOnly(roster)); setSelectedRoster(roster); clearConflict(); retryTargetRef.current = null; } catch { if (requestIdRef.current !== requestId) return; setLoadErrorMessage(SELECT_ERROR_MESSAGE); } }, [clearConflict, fetchRosterByCompany, reset], ); const clearSelectedCompany = useCallback( (nextName = "") => { requestIdRef.current += 1; if (selectedRoster) { reset((current) => ({ ...emptyVendorCompanyRosterForm, name: nextName, technicians: current.technicians, })); } setSelectedRoster(null); clearConflict(); setLoadErrorMessage(null); retryTargetRef.current = null; }, [clearConflict, reset, selectedRoster], ); const reload = useCallback(() => { const requestId = (requestIdRef.current += 1); clearConflict(); setLoadErrorMessage(null); retryTargetRef.current = { kind: "reload" }; if (mode === "update") { void query.refetch().then((result) => { if (requestIdRef.current !== requestId) return; if (result.isError) setLoadErrorMessage(RELOAD_ERROR_MESSAGE); else retryTargetRef.current = null; }); return; } if (selectedRoster?.companyId != null) { void fetchRosterByCompany(selectedRoster.companyId) .then((roster) => { if (requestIdRef.current !== requestId) return; // SH-250: refresh the visible company fields alongside rowVersion. // Refreshing only selectedRoster left the form on pre-conflict values, // so a retry diffed stale fields against the reloaded roster and could // overwrite the concurrent company update that caused the 409. reset(withCompanyFieldsOnly(roster)); setSelectedRoster(roster); retryTargetRef.current = null; }) .catch(() => { if (requestIdRef.current !== requestId) return; setLoadErrorMessage(RELOAD_ERROR_MESSAGE); }); } }, [clearConflict, fetchRosterByCompany, mode, query, reset, selectedRoster]); const retryLoad = useCallback(() => { const target = retryTargetRef.current; if (!target) return; if (target.kind === "select") void selectCompany(target.company); else void reload(); }, [reload, selectCompany]); const resetSelection = useCallback(() => { requestIdRef.current += 1; setSelectedRoster(null); setLoadErrorMessage(null); retryTargetRef.current = null; }, []); return { selectedRoster, loadErrorMessage, selectedCompanyId: selectedRoster?.companyId ?? null, selectCompany, clearSelectedCompany, reload, retryLoad, resetSelection, }; }