/** * useTrusteeOptions Hook * * Zentraler Hook für Trustee-Options (Dropdowns, Label-Auflösung). * Lädt Options von den entsprechenden /options Endpoints und cached sie. * Unterstützt dynamische Filterung (z.B. Contracts nach Organisation). */ import { useState, useCallback, useEffect, useMemo } from 'react'; import api from '../api'; import { useInstanceId } from './useCurrentInstance'; // ============================================================================ // TYPES // ============================================================================ export interface TrusteeOption { value: string; label: string; } export interface TrusteeOptionsMap { users: TrusteeOption[]; organisations: TrusteeOption[]; roles: TrusteeOption[]; contracts: TrusteeOption[]; documents: TrusteeOption[]; positions: TrusteeOption[]; } export type TrusteeOptionEntity = keyof TrusteeOptionsMap; interface LoadOptionsParams { organisationId?: string; contractId?: string; } // ============================================================================ // HOOK // ============================================================================ /** * Hook für Trustee-Options. * * @param autoLoad - Array von Entity-Namen, die automatisch beim Mount geladen werden sollen * @returns Options-Map, Lade-Funktion, Label-Getter * * @example * ```tsx * // Auto-load users, organisations und roles * const { options, getLabel, loading } = useTrusteeOptions(['users', 'organisations', 'roles']); * * // Label für eine userId auflösen * const userName = getLabel('users', access.userId); * * // Contracts für spezifische Organisation nachladen * await loadOptions(['contracts'], { organisationId: 'org-123' }); * ``` */ export function useTrusteeOptions(autoLoad: TrusteeOptionEntity[] = []) { const instanceId = useInstanceId(); const [options, setOptions] = useState>({ users: [], organisations: [], roles: [], contracts: [], documents: [], positions: [], }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [loadedEntities, setLoadedEntities] = useState>(new Set()); /** * Lädt Options für angegebene Entities. * * @param entities - Array von Entity-Namen * @param filters - Optionale Filter (z.B. organisationId für Contracts) */ const loadOptions = useCallback(async ( entities: TrusteeOptionEntity[], filters?: LoadOptionsParams ): Promise => { if (!instanceId && entities.some(e => e !== 'users')) { console.warn('useTrusteeOptions: No instanceId available, skipping load for trustee entities'); return; } setLoading(true); setError(null); try { const promises = entities.map(async (entity) => { let url: string; if (entity === 'users') { // Users kommen aus dem globalen API-Endpoint url = '/api/users/options'; } else { // Trustee-Entities kommen aus dem Feature-API mit instanceId url = `/api/trustee/${instanceId}/${entity}/options`; // Dynamische Filterung für Contracts nach Organisation if (filters?.organisationId && entity === 'contracts') { url += `?organisationId=${encodeURIComponent(filters.organisationId)}`; } // Dynamische Filterung für Documents/Positions nach Contract if (filters?.contractId && (entity === 'documents' || entity === 'positions')) { url += `?contractId=${encodeURIComponent(filters.contractId)}`; } } const response = await api.get(url); return { entity, data: response.data as TrusteeOption[] }; }); const results = await Promise.all(promises); const newOptions: Partial = {}; results.forEach(({ entity, data }) => { newOptions[entity] = Array.isArray(data) ? data : []; }); setOptions(prev => ({ ...prev, ...newOptions })); // Merke geladene Entities (nur ohne Filter) if (!filters) { setLoadedEntities(prev => { const newSet = new Set(prev); entities.forEach(e => newSet.add(e)); return newSet; }); } } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to load options'; setError(errorMessage); console.error('useTrusteeOptions: Error loading options:', err); } finally { setLoading(false); } }, [instanceId]); /** * Gibt das Label für einen Wert zurück. * Falls nicht gefunden, wird der Wert selbst zurückgegeben. */ const getLabel = useCallback((entity: TrusteeOptionEntity, value: string | null | undefined): string => { if (value === null || value === undefined || value === '') { return '-'; } const entityOptions = options[entity]; if (!entityOptions || entityOptions.length === 0) { return value; } const found = entityOptions.find(o => o.value === value); return found?.label || value; }, [options]); /** * Gibt Options für eine Entity zurück. */ const getOptions = useCallback((entity: TrusteeOptionEntity): TrusteeOption[] => { return options[entity] || []; }, [options]); /** * Prüft ob Options für eine Entity geladen wurden. */ const isLoaded = useCallback((entity: TrusteeOptionEntity): boolean => { return loadedEntities.has(entity); }, [loadedEntities]); /** * Lädt Options für Contracts einer spezifischen Organisation. * Nützlich für abhängige Dropdowns. */ const loadContractsForOrganisation = useCallback(async (organisationId: string): Promise => { if (!instanceId || !organisationId) { return []; } try { const url = `/api/trustee/${instanceId}/contracts/options?organisationId=${encodeURIComponent(organisationId)}`; const response = await api.get(url); const contractOptions = Array.isArray(response.data) ? response.data : []; // Update Options-State setOptions(prev => ({ ...prev, contracts: contractOptions })); return contractOptions; } catch (err) { console.error('useTrusteeOptions: Error loading contracts for organisation:', err); return []; } }, [instanceId]); /** * Erstellt eine Lookup-Map für schnelle Label-Auflösung. */ const createLookupMap = useCallback((entity: TrusteeOptionEntity): Map => { const map = new Map(); const entityOptions = options[entity] || []; entityOptions.forEach(opt => { map.set(opt.value, opt.label); }); return map; }, [options]); // Memoized Lookup-Maps für Performance const lookupMaps = useMemo(() => ({ users: createLookupMap('users'), organisations: createLookupMap('organisations'), roles: createLookupMap('roles'), contracts: createLookupMap('contracts'), documents: createLookupMap('documents'), positions: createLookupMap('positions'), }), [createLookupMap]); /** * Schnelle Label-Auflösung via Lookup-Map. */ const getLabelFast = useCallback((entity: TrusteeOptionEntity, value: string | null | undefined): string => { if (value === null || value === undefined || value === '') { return '-'; } return lookupMaps[entity].get(value) || value; }, [lookupMaps]); // Auto-Load beim Mount useEffect(() => { if (autoLoad.length > 0) { // Nur laden wenn instanceId verfügbar (oder nur 'users' geladen werden soll) const needsInstance = autoLoad.some(e => e !== 'users'); if (!needsInstance || instanceId) { loadOptions(autoLoad); } } }, [instanceId, autoLoad.join(',')]); // autoLoad als String-Join für Dependency-Vergleich return { // State options, loading, error, // Actions loadOptions, loadContractsForOrganisation, // Getters getLabel, getLabelFast, getOptions, isLoaded, createLookupMap, // Context instanceId, }; } // ============================================================================ // CONVENIENCE EXPORTS // ============================================================================ /** * Hook speziell für TrusteeAccessView. * Lädt automatisch users, organisations und roles. */ export function useTrusteeAccessOptions() { return useTrusteeOptions(['users', 'organisations', 'roles']); } /** * Hook speziell für Views mit Organisation+Contract Dropdowns. * Lädt automatisch organisations und contracts. */ export function useTrusteeOrgContractOptions() { return useTrusteeOptions(['organisations', 'contracts']); } export default useTrusteeOptions;