shoc-frontend-new/src/app/(protected)/vendors/_components/use-vendor-roster-selection.ts
Arthur Bassi 3c5941b895 chore(work-orders): resolve SH-388 uplift conflicts onto current main
Keep the slide-over uplift tab and the evidence upload while taking the
current board, dashboard, and vendor changes from main.
2026-09-24 15:17:34 -03:00

204 lines
7.2 KiB
TypeScript

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<void>;
clearSelectedCompany: (nextName?: string) => void;
reload: () => void;
retryLoad: () => void;
resetSelection: () => void;
}
interface UseVendorRosterSelectionOptions {
mode: "create" | "update";
reset: UseFormReset<VendorCompanyRosterFormValues>;
query: UseQueryResult<VendorCompanyRoster, Error>;
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<VendorCompanyRoster | null>(null);
const [loadErrorMessage, setLoadErrorMessage] = useState<string | null>(null);
const retryTargetRef = useRef<
{ kind: "select"; company: VendorFacetCompany } | { kind: "reload" } | null
>(null);
const requestIdRef = useRef(0);
const fetchRosterByCompany = useCallback(
async (id: string | number): Promise<VendorCompanyRoster> =>
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,
};
}