/** * 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, // Read-only data table API (Daten-Tabellen page) fetchDataAccounts as fetchDataAccountsApi, fetchDataJournalEntries as fetchDataJournalEntriesApi, fetchDataJournalLines as fetchDataJournalLinesApi, fetchDataContacts as fetchDataContactsApi, fetchDataAccountBalances as fetchDataAccountBalancesApi, fetchAccountingConfigs as fetchAccountingConfigsApi, fetchAccountingSyncs as fetchAccountingSyncsApi, type TrusteeDataAccount, type TrusteeDataJournalEntry, type TrusteeDataJournalLine, type TrusteeDataContact, type TrusteeDataAccountBalance, type TrusteeAccountingConfigRecord, type TrusteeAccountingSyncRecord, } 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; /** Backend: FK label column (e.g. userId -> userIdLabel). */ displayField?: string; frontendFormat?: string; frontendFormatLabels?: 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; /** Optional override: name of the *view* model (e.g. ``TrusteePositionView``) * used purely for the `/attributes/...` lookup so synthetic display columns * resolve via `resolveColumnTypes`. Falls back to `entityName` when absent. * Permissions and CRUD operations always use `entityName`. */ attributesEntityName?: 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 attrEntity = config.attributesEntityName ?? config.entityName; const response = await api.get(`/api/trustee/${instanceId}/attributes/${attrEntity}`); 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) => ({ value: opt.value, label: opt.label || String(opt.value) })); } 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) => ({ value: opt.value, label: opt.label || String(opt.value) })); } 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) => ({ value: opt.value, label: opt.label || String(opt.value) })); } 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) => ({ value: opt.value, label: opt.label || String(opt.value) })); } 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', // Use the view model so the table picks up `syncStatus` / `syncErrorMessage` // attributes (computed at the route layer from `TrusteeAccountingSync`). attributesEntityName: 'TrusteePositionView', 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'; // ============================================================================ // READ-ONLY DATA TABLE HOOKS (Daten-Tabellen page) // ============================================================================ // // These hooks expose synced/operational tables in read-only form. Mutations // would be overwritten by the next accounting sync, so create/update/delete // are intentionally not implemented (`_readOnlyMutator` raises if called). function _readOnlyMutator(): never { throw new Error('Read-only entity: mutations are not supported via this hook.'); } function _buildReadOnlyConfig( entityName: string, fetchAll: TrusteeEntityConfig['fetchAll'] ): TrusteeEntityConfig { return { entityName, fetchAll, fetchById: async () => null, create: _readOnlyMutator as any, update: _readOnlyMutator as any, deleteItem: _readOnlyMutator as any, }; } export const useTrusteeDataAccounts = _createTrusteeEntityHook( _buildReadOnlyConfig('TrusteeDataAccount', fetchDataAccountsApi) ); export const useTrusteeDataJournalEntries = _createTrusteeEntityHook( _buildReadOnlyConfig('TrusteeDataJournalEntry', fetchDataJournalEntriesApi) ); export const useTrusteeDataJournalLines = _createTrusteeEntityHook( _buildReadOnlyConfig('TrusteeDataJournalLine', fetchDataJournalLinesApi) ); export const useTrusteeDataContacts = _createTrusteeEntityHook( _buildReadOnlyConfig('TrusteeDataContact', fetchDataContactsApi) ); export const useTrusteeDataAccountBalances = _createTrusteeEntityHook( _buildReadOnlyConfig('TrusteeDataAccountBalance', fetchDataAccountBalancesApi) ); export const useTrusteeAccountingConfigs = _createTrusteeEntityHook( _buildReadOnlyConfig('TrusteeAccountingConfig', fetchAccountingConfigsApi) ); export const useTrusteeAccountingSyncs = _createTrusteeEntityHook( _buildReadOnlyConfig('TrusteeAccountingSync', fetchAccountingSyncsApi) ); export type { TrusteeDataAccount, TrusteeDataJournalEntry, TrusteeDataJournalLine, TrusteeDataContact, TrusteeDataAccountBalance, TrusteeAccountingConfigRecord, TrusteeAccountingSyncRecord, };