/** * Trustee Hooks * * Hooks für das Trustee-Feature mit Instanz-Kontext. * Die instanceId wird automatisch aus der URL gelesen. */ import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import api from '../api'; import { usePermissions, type UserPermissions } from './usePermissions'; import { useInstanceId } from './useCurrentInstance'; import { // Types type TrusteeOrganisation, type TrusteeRole, type TrusteeAccess, type TrusteeContract, type TrusteeDocument, type TrusteePosition, type PaginationParams, // Organisation API fetchOrganisations as fetchOrganisationsApi, fetchOrganisationById as fetchOrganisationByIdApi, createOrganisation as createOrganisationApi, updateOrganisation as updateOrganisationApi, deleteOrganisation as deleteOrganisationApi, // Role API fetchRoles as fetchRolesApi, fetchRoleById as fetchRoleByIdApi, createRole as createRoleApi, updateRole as updateRoleApi, deleteRole as deleteRoleApi, // Access API fetchAccess as fetchAccessApi, fetchAccessById as fetchAccessByIdApi, createAccess as createAccessApi, updateAccess as updateAccessApi, deleteAccess as deleteAccessApi, // Contract API fetchContracts as fetchContractsApi, fetchContractById as fetchContractByIdApi, createContract as createContractApi, updateContract as updateContractApi, deleteContract as deleteContractApi, // Document API fetchDocuments as fetchDocumentsApi, fetchDocumentById as fetchDocumentByIdApi, createDocument as createDocumentApi, updateDocument as updateDocumentApi, deleteDocument as deleteDocumentApi, // Position API fetchPositions as fetchPositionsApi, fetchPositionById as fetchPositionByIdApi, createPosition as createPositionApi, updatePosition as updatePositionApi, deletePosition as deletePositionApi, } from '../api/trusteeApi'; export type { TrusteeOrganisation, TrusteeRole, TrusteeAccess, TrusteeContract, TrusteeDocument, TrusteePosition, PaginationParams }; export interface AttributeDefinition { name: string; type: 'text' | 'email' | 'date' | 'checkbox' | 'select' | 'multiselect' | 'number' | 'textarea' | 'timestamp' | 'file'; label: string; description?: string; required?: boolean; default?: any; options?: any[] | string; readonly?: boolean; editable?: boolean; visible?: boolean; order?: number; sortable?: boolean; filterable?: boolean; searchable?: boolean; width?: number; minWidth?: number; maxWidth?: number; filterOptions?: string[]; dependsOn?: string; } // ============================================================================ // GENERIC TRUSTEE ENTITY HOOK FACTORY // ============================================================================ interface TrusteeEntityConfig { entityName: string; fetchAll: (request: any, instanceId: string, params?: PaginationParams) => Promise; fetchById: (request: any, instanceId: string, id: string) => Promise; create: (request: any, instanceId: string, data: Partial) => Promise; update: (request: any, instanceId: string, id: string, data: Partial) => Promise; deleteItem: (request: any, instanceId: string, id: string) => Promise; } function _createTrusteeEntityHook(config: TrusteeEntityConfig) { return function useTrusteeEntity() { // Hole instanceId aus URL-Kontext const instanceId = useInstanceId(); const [items, setItems] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); const [pagination, setPagination] = useState<{ currentPage: number; pageSize: number; totalItems: number; totalPages: number; } | null>(null); const { request, isLoading: loading, error } = useApiRequest(); const { checkPermission } = usePermissions(); const fetchAttributes = useCallback(async () => { if (!instanceId) return []; try { const response = await api.get(`/api/trustee/${instanceId}/attributes/${config.entityName}`); let attrs: AttributeDefinition[] = []; if (response.data?.attributes && Array.isArray(response.data.attributes)) { attrs = response.data.attributes; } else if (Array.isArray(response.data)) { attrs = response.data; } setAttributes(attrs); return attrs; } catch (error: any) { console.error(`Error fetching ${config.entityName} attributes:`, error); setAttributes([]); return []; } }, [instanceId]); const fetchPermissions = useCallback(async () => { try { // Use fully qualified objectKey for RBAC: data.feature.trustee.EntityName const objectKey = `data.feature.trustee.${config.entityName}`; const perms = await checkPermission('DATA', objectKey); setPermissions(perms); return perms; } catch (error: any) { console.error(`Error fetching ${config.entityName} permissions:`, error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); const fetchItems = useCallback(async (params?: PaginationParams) => { if (!instanceId) { setItems([]); return; } try { const data = await config.fetchAll(request, instanceId, params); if (data && typeof data === 'object' && 'items' in data) { const fetchedItems = Array.isArray(data.items) ? data.items : []; setItems(fetchedItems); if (data.pagination) { setPagination(data.pagination); } } else { const fetchedItems = Array.isArray(data) ? data : []; setItems(fetchedItems); setPagination(null); } } catch (error: any) { setItems([]); setPagination(null); } }, [request, instanceId]); const removeOptimistically = (itemId: string) => { setItems(prev => prev.filter(item => item.id !== itemId)); }; const updateOptimistically = (itemId: string, updateData: Partial) => { setItems(prev => prev.map(item => item.id === itemId ? { ...item, ...updateData } : item ) ); }; const fetchById = useCallback(async (itemId: string): Promise => { if (!instanceId) return null; return await config.fetchById(request, instanceId, itemId); }, [request, instanceId]); const generateEditFieldsFromAttributes = useCallback(() => { if (!attributes || attributes.length === 0) { return []; } return attributes .filter(attr => { if (attr.readonly === true || attr.editable === false) { return false; } if (attr.name === 'id') { return false; } const nonEditableFields = ['sysCreatedBy', 'sysCreatedAt', 'sysModifiedBy', 'sysModifiedAt']; return !nonEditableFields.includes(attr.name); }) .map(attr => { let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' | 'number' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; if (attr.type === 'checkbox') { fieldType = 'boolean'; } else if (attr.type === 'email') { fieldType = 'email'; } else if (attr.type === 'date') { fieldType = 'date'; } else if (attr.type === 'number') { fieldType = 'number'; } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { options = attr.options.map((opt: any) => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; if (Array.isArray(attr.options)) { options = attr.options.map((opt: any) => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'textarea') { fieldType = 'textarea'; } else if (attr.type === 'timestamp') { fieldType = 'readonly'; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: attr.editable !== false && attr.readonly !== true, required: attr.required === true, options, optionsReference, dependsOn: attr.dependsOn }; }); }, [attributes]); const generateCreateFieldsFromAttributes = useCallback(() => { if (!attributes || attributes.length === 0) { return []; } return attributes .filter(attr => { const systemFields = ['sysCreatedBy', 'sysCreatedAt', 'sysModifiedBy', 'sysModifiedAt', 'mandateId']; return !systemFields.includes(attr.name); }) .map(attr => { let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' | 'number' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; if (attr.type === 'checkbox') { fieldType = 'boolean'; } else if (attr.type === 'email') { fieldType = 'email'; } else if (attr.type === 'date') { fieldType = 'date'; } else if (attr.type === 'number') { fieldType = 'number'; } else if (attr.type === 'select') { fieldType = 'enum'; if (Array.isArray(attr.options)) { options = attr.options.map((opt: any) => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; if (Array.isArray(attr.options)) { options = attr.options.map((opt: any) => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { optionsReference = attr.options; } } else if (attr.type === 'textarea') { fieldType = 'textarea'; } else if (attr.type === 'timestamp') { fieldType = 'readonly'; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: true, required: attr.required === true, options, optionsReference, dependsOn: attr.dependsOn }; }); }, [attributes]); const ensureAttributesLoaded = useCallback(async () => { if (attributes && attributes.length > 0) { return attributes; } return await fetchAttributes(); }, [attributes, fetchAttributes]); // Lade Daten wenn instanceId verfügbar useEffect(() => { if (instanceId) { fetchAttributes(); fetchPermissions(); fetchItems(); } }, [instanceId, fetchAttributes, fetchPermissions, fetchItems]); return { items, loading, error, refetch: fetchItems, removeOptimistically, updateOptimistically, attributes, permissions, pagination, fetchById, generateEditFieldsFromAttributes, generateCreateFieldsFromAttributes, ensureAttributesLoaded, instanceId // Auch instanceId zurückgeben für Operations-Hook }; }; } function _createTrusteeOperationsHook(config: TrusteeEntityConfig) { return function useTrusteeEntityOperations() { // Hole instanceId aus URL-Kontext const instanceId = useInstanceId(); const [deletingItems, setDeletingItems] = useState>(new Set()); const [creatingItem, setCreatingItem] = useState(false); const { request, isLoading } = useApiRequest(); const [deleteError, setDeleteError] = useState(null); const [createError, setCreateError] = useState(null); const [updateError, setUpdateError] = useState(null); const handleDelete = useCallback(async (itemId: string) => { if (!instanceId) { setDeleteError('No instance context'); return false; } setDeleteError(null); setDeletingItems(prev => new Set(prev).add(itemId)); try { await config.deleteItem(request, instanceId, itemId); await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { setDeleteError(error.message); return false; } finally { setDeletingItems(prev => { const newSet = new Set(prev); newSet.delete(itemId); return newSet; }); } }, [request, instanceId]); const handleCreate = useCallback(async (itemData: Partial) => { if (!instanceId) { setCreateError('No instance context'); return { success: false, error: 'No instance context' }; } setCreateError(null); setCreatingItem(true); console.warn('🔧 handleCreate called with itemData:', itemData); try { const newItem = await config.create(request, instanceId, itemData); return { success: true, data: newItem }; } catch (error: any) { console.error('🔧 handleCreate error:', { message: error.message, response: error.response?.data, status: error.response?.status }); const errorMessage = error.response?.data?.detail || error.message; setCreateError(errorMessage); return { success: false, error: errorMessage }; } finally { setCreatingItem(false); } }, [request, instanceId]); const handleUpdate = useCallback(async (itemId: string, updateData: Partial) => { if (!instanceId) { setUpdateError('No instance context'); return { success: false, error: 'No instance context' }; } setUpdateError(null); try { const updatedItem = await config.update(request, instanceId, itemId, updateData); return { success: true, data: updatedItem }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update'; setUpdateError(errorMessage); return { success: false, error: errorMessage, statusCode: error.response?.status, isPermissionError: error.response?.status === 403, isValidationError: error.response?.status === 400 }; } }, [request, instanceId]); return { deletingItems, creatingItem, deleteError, createError, updateError, handleDelete, handleCreate, handleUpdate, isLoading, instanceId }; }; } // ============================================================================ // ORGANISATION HOOKS // ============================================================================ const organisationConfig: TrusteeEntityConfig = { entityName: 'TrusteeOrganisation', fetchAll: fetchOrganisationsApi, fetchById: fetchOrganisationByIdApi, create: createOrganisationApi, update: updateOrganisationApi, deleteItem: deleteOrganisationApi }; export const useTrusteeOrganisations = _createTrusteeEntityHook(organisationConfig); export const useTrusteeOrganisationOperations = _createTrusteeOperationsHook(organisationConfig); // ============================================================================ // ROLE HOOKS // ============================================================================ const roleConfig: TrusteeEntityConfig = { entityName: 'TrusteeRole', fetchAll: fetchRolesApi, fetchById: fetchRoleByIdApi, create: createRoleApi, update: updateRoleApi, deleteItem: deleteRoleApi }; export const useTrusteeRoles = _createTrusteeEntityHook(roleConfig); export const useTrusteeRoleOperations = _createTrusteeOperationsHook(roleConfig); // ============================================================================ // ACCESS HOOKS // ============================================================================ const accessConfig: TrusteeEntityConfig = { entityName: 'TrusteeAccess', fetchAll: fetchAccessApi, fetchById: fetchAccessByIdApi, create: createAccessApi, update: updateAccessApi, deleteItem: deleteAccessApi }; export const useTrusteeAccess = _createTrusteeEntityHook(accessConfig); export const useTrusteeAccessOperations = _createTrusteeOperationsHook(accessConfig); // ============================================================================ // CONTRACT HOOKS // ============================================================================ const contractConfig: TrusteeEntityConfig = { entityName: 'TrusteeContract', fetchAll: fetchContractsApi, fetchById: fetchContractByIdApi, create: createContractApi, update: updateContractApi, deleteItem: deleteContractApi }; export const useTrusteeContracts = _createTrusteeEntityHook(contractConfig); export const useTrusteeContractOperations = _createTrusteeOperationsHook(contractConfig); // ============================================================================ // DOCUMENT HOOKS // ============================================================================ const documentConfig: TrusteeEntityConfig = { entityName: 'TrusteeDocument', fetchAll: fetchDocumentsApi, fetchById: fetchDocumentByIdApi, create: createDocumentApi, update: updateDocumentApi, deleteItem: deleteDocumentApi }; export const useTrusteeDocuments = _createTrusteeEntityHook(documentConfig); export const useTrusteeDocumentOperations = _createTrusteeOperationsHook(documentConfig); // ============================================================================ // POSITION HOOKS // ============================================================================ const positionConfig: TrusteeEntityConfig = { entityName: 'TrusteePosition', fetchAll: fetchPositionsApi, fetchById: fetchPositionByIdApi, create: createPositionApi, update: updatePositionApi, deleteItem: deletePositionApi }; export const useTrusteePositions = _createTrusteeEntityHook(positionConfig); export const useTrusteePositionOperations = _createTrusteeOperationsHook(positionConfig); export { useTrusteePositionDocuments, useTrusteePositionDocumentOperations } from './useTrusteePositionDocuments'; export type { TrusteePositionDocument } from '../api/trusteeApi';