frontend_nyla/src/hooks/useTrusteeOptions.ts
2026-01-23 21:05:36 +01:00

287 lines
8.7 KiB
TypeScript

/**
* 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<Partial<TrusteeOptionsMap>>({
users: [],
organisations: [],
roles: [],
contracts: [],
documents: [],
positions: [],
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [loadedEntities, setLoadedEntities] = useState<Set<string>>(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<void> => {
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<TrusteeOptionsMap> = {};
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<TrusteeOption[]> => {
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<string, string> => {
const map = new Map<string, string>();
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;