287 lines
8.7 KiB
TypeScript
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;
|