diff --git a/src/domain/services/use-cases/use-services-registry-controller.ts b/src/domain/services/use-cases/use-services-registry-controller.ts index 43237a48..14e2a6ba 100644 --- a/src/domain/services/use-cases/use-services-registry-controller.ts +++ b/src/domain/services/use-cases/use-services-registry-controller.ts @@ -87,47 +87,20 @@ function formFromService(service: Service): ServiceForm { }; } -export function useServicesRegistryController() { - const { user } = useAuthContext(); - const canManage = hasUserRole(user?.userRoles, "scheduler") || isAdminUser(user?.userRoles); - const canDeactivate = isAdminUser(user?.userRoles); - const [status, setStatus] = useState("active"); - const [search, setSearch] = useState(""); +// Owns the add/edit form lifecycle so the top-level controller stays within the +// changed-file maintainability budget. Mutations are passed in so `isSaving` and +// `closeEditor` share the exact instances the controller reports on. +function useServiceEditor( + canonicalTrades: string[], + canDeactivate: boolean, + createService: ReturnType, + updateService: ReturnType, +) { const [selected, setSelected] = useState(null); const [editorMode, setEditorMode] = useState<"add" | "edit" | null>(null); const [form, setForm] = useState(emptyForm); const [iconTouched, setIconTouched] = useState(false); const [formError, setFormError] = useState(""); - const [deactivateTarget, setDeactivateTarget] = useState(null); - - const servicesQuery = useServices(); - // The backend rejects any Trade outside the canonical TradeCatalog (400 - // TradeInvalid), so the picker is sourced from the server-owned canonical - // Trade reference exposed on Vendor/facets — the same list the vendor roster - // form uses — never the editable DropdownOptions "Trade" category, whose - // legacy seed values ("Plumbing", "General", ...) would fail on save. - const facetsQuery = useVendorFacets(); - const canonicalTrades = useMemo(() => facetsQuery.data?.trades ?? [], [facetsQuery.data]); - const { data: templates = [] } = useCompletionDocTemplates(); - const createService = useCreateService(); - const updateService = useUpdateService(); - const deactivateService = useDeactivateService(); - - const services = useMemo(() => servicesQuery.data ?? [], [servicesQuery.data]); - const activeCount = services.filter((service) => service.isActive).length; - const inactiveCount = services.length - activeCount; - - const filteredServices = useMemo(() => { - const needle = search.trim().toLowerCase(); - return services - .filter((service) => (status === "active" ? service.isActive : !service.isActive)) - .filter( - (service) => - !needle || - service.name.toLowerCase().includes(needle) || - service.trade.toLowerCase().includes(needle), - ); - }, [search, services, status]); // Keep the trade stored on the service being edited selectable even if it was // later deactivated in the canonical Trade catalog, so editing never silently @@ -210,12 +183,67 @@ export function useServicesRegistryController() { } }; + return { + selected, + setSelected, + editorMode, + form, + formError, + trades, + openCreate, + openEdit, + closeEditor, + updateForm, + toggleType, + save, + }; +} + +export function useServicesRegistryController() { + const { user } = useAuthContext(); + const canManage = hasUserRole(user?.userRoles, "scheduler") || isAdminUser(user?.userRoles); + const canDeactivate = isAdminUser(user?.userRoles); + const [status, setStatus] = useState("active"); + const [search, setSearch] = useState(""); + const [deactivateTarget, setDeactivateTarget] = useState(null); + + const servicesQuery = useServices(); + // The backend rejects any Trade outside the canonical TradeCatalog (400 + // TradeInvalid), so the picker is sourced from the server-owned canonical + // Trade reference exposed on Vendor/facets — the same list the vendor roster + // form uses — never the editable DropdownOptions "Trade" category, whose + // legacy seed values ("Plumbing", "General", ...) would fail on save. + const facetsQuery = useVendorFacets(); + const canonicalTrades = useMemo(() => facetsQuery.data?.trades ?? [], [facetsQuery.data]); + const { data: templates = [] } = useCompletionDocTemplates(); + const createService = useCreateService(); + const updateService = useUpdateService(); + const deactivateService = useDeactivateService(); + + const editor = useServiceEditor(canonicalTrades, canDeactivate, createService, updateService); + + const services = useMemo(() => servicesQuery.data ?? [], [servicesQuery.data]); + const activeCount = services.filter((service) => service.isActive).length; + const inactiveCount = services.length - activeCount; + + const filteredServices = useMemo(() => { + const needle = search.trim().toLowerCase(); + return services + .filter((service) => (status === "active" ? service.isActive : !service.isActive)) + .filter( + (service) => + !needle || + service.name.toLowerCase().includes(needle) || + service.trade.toLowerCase().includes(needle), + ); + }, [search, services, status]); + const confirmDeactivate = () => { if (!deactivateTarget) return; deactivateService.mutate(deactivateTarget.id, { onSuccess: () => { setDeactivateTarget(null); - setSelected(null); + editor.setSelected(null); }, }); }; @@ -227,12 +255,12 @@ export function useServicesRegistryController() { setStatus, search, setSearch, - selected, - setSelected, - editorMode, - form, - formError, - trades, + selected: editor.selected, + setSelected: editor.setSelected, + editorMode: editor.editorMode, + form: editor.form, + formError: editor.formError, + trades: editor.trades, templates, filteredServices, services, @@ -240,12 +268,12 @@ export function useServicesRegistryController() { inactiveCount, isLoading: servicesQuery.isLoading, error: servicesQuery.error, - openCreate, - openEdit, - closeEditor, - updateForm, - toggleType, - save, + openCreate: editor.openCreate, + openEdit: editor.openEdit, + closeEditor: editor.closeEditor, + updateForm: editor.updateForm, + toggleType: editor.toggleType, + save: editor.save, deactivateTarget, setDeactivateTarget, confirmDeactivate,