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 { fetchPositions as fetchPositionsApi, fetchPositionById as fetchPositionByIdApi, createPosition as createPositionApi, updatePosition as updatePositionApi, deletePosition as deletePositionApi, type TrusteePosition, type PaginationParams } from '../api/trusteeApi'; 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; } // Re-export types export type { TrusteePosition, PaginationParams }; // Positions list hook export function useTrusteePositions() { const instanceId = useInstanceId(); const [positions, setPositions] = 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(); // Fetch attributes from backend const fetchAttributes = useCallback(async () => { if (!instanceId) return []; try { const response = await api.get(`/api/trustee/${instanceId}/attributes/TrusteePosition`); 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; } else if (response.data && typeof response.data === 'object') { const keys = Object.keys(response.data); for (const key of keys) { if (Array.isArray(response.data[key])) { attrs = response.data[key]; break; } } } setAttributes(attrs); return attrs; } catch (error: any) { console.error('Error fetching attributes:', error); setAttributes([]); return []; } }, [instanceId]); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const objectKey = 'data.feature.trustee.TrusteePosition'; const perms = await checkPermission('DATA', objectKey); setPermissions(perms); return perms; } catch (error: any) { console.error('Error fetching permissions:', error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); const fetchPositions = useCallback(async (params?: PaginationParams) => { if (!instanceId) { setPositions([]); return; } try { const data = await fetchPositionsApi(request, instanceId, params); if (data && typeof data === 'object' && 'items' in data) { const items = Array.isArray(data.items) ? data.items : []; setPositions(items); if (data.pagination) { setPagination(data.pagination); } } else { const items = Array.isArray(data) ? data : []; setPositions(items); setPagination(null); } } catch (error: any) { setPositions([]); setPagination(null); } }, [request, instanceId]); // Optimistically remove a position const removeOptimistically = (positionId: string) => { setPositions(prevPositions => prevPositions.filter(pos => pos.id !== positionId)); }; // Optimistically update a position const updateOptimistically = (positionId: string, updateData: Partial) => { setPositions(prevPositions => prevPositions.map(pos => pos.id === positionId ? { ...pos, ...updateData } : pos ) ); }; // Fetch a single position by ID const fetchPositionById = useCallback(async (positionId: string): Promise => { if (!instanceId) return null; return await fetchPositionByIdApi(request, instanceId, positionId); }, [request, instanceId]); // Generate edit fields from attributes dynamically with MwSt calculation logic const generateEditFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' | 'number'; editable?: boolean; required?: boolean; validator?: (value: any, formData?: any) => string | null; onChange?: (value: any, formData: any) => Partial; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; dependsOn?: string; minRows?: number; maxRows?: number; }> => { if (!attributes || attributes.length === 0) { return []; } const editableFields = attributes .filter(attr => { if (attr.readonly === true || attr.editable === false) { return false; } const nonEditableFields = ['id', 'mandate', 'sysCreatedBy', 'sysModifiedBy', 'sysCreatedAt', 'sysModifiedAt']; return !nonEditableFields.includes(attr.name); }) .map(attr => { const isDescField = attr.name === 'desc' || attr.name.toLowerCase().includes('description'); 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; let dependsOn: string | undefined = undefined; let minRows: number | undefined = undefined; let maxRows: number | undefined = undefined; let onChange: ((value: any, formData: any) => Partial) | undefined = undefined; if (isDescField) { fieldType = 'textarea'; minRows = 3; maxRows = 8; } else 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 === 'textarea') { fieldType = 'textarea'; minRows = minRows || 3; maxRows = maxRows || 8; } // contractId depends on organisationId if (attr.name === 'contractId') { dependsOn = 'organisationId'; } // CUSTOM LOGIC: MwSt-Berechnung // When bookingAmount or vatPercentage changes, auto-calculate vatAmount if (attr.name === 'bookingAmount') { onChange = (value: number, formData: any) => { const amount = parseFloat(String(value)) || 0; const percentage = parseFloat(String(formData.vatPercentage)) || 0; const calculatedVat = amount * (percentage / 100); return { vatAmount: calculatedVat }; }; } else if (attr.name === 'vatPercentage') { onChange = (value: number, formData: any) => { const percentage = parseFloat(String(value)) || 0; const amount = parseFloat(String(formData.bookingAmount)) || 0; const calculatedVat = amount * (percentage / 100); return { vatAmount: calculatedVat }; }; } let required = attr.required === true; let validator: ((value: any, formData?: any) => string | null) | undefined = undefined; // CUSTOM LOGIC: vatAmount validator - warn if manually overridden if (attr.name === 'vatAmount') { validator = (value: any, formData?: any) => { if (!formData) return null; const vatAmount = parseFloat(String(value)) || 0; const bookingAmount = parseFloat(String(formData.bookingAmount)) || 0; const vatPercentage = parseFloat(String(formData.vatPercentage)) || 0; const calculatedVat = bookingAmount * (vatPercentage / 100); if (Math.abs(vatAmount - calculatedVat) > 0.01) { return 'MwSt-Betrag weicht von Berechnung ab (manuell überschrieben)'; } return null; }; } // Standard validators if (attr.name === 'organisationId' || attr.name === 'contractId') { required = true; validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return `${attr.label || attr.name} is required`; } return null; }; } else if (attr.name === 'valuta' || attr.name === 'transactionDateTime') { required = true; } else if (attr.name === 'bookingCurrency' || attr.name === 'originalCurrency') { required = true; } else if (attr.name === 'bookingAmount' || attr.name === 'originalAmount') { required = true; validator = (value: any) => { const num = parseFloat(String(value)); if (isNaN(num)) { return 'Must be a valid number'; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: attr.editable !== false && attr.readonly !== true, required, validator, onChange, options, optionsReference, dependsOn, minRows, maxRows }; }); return editableFields; }, [attributes]); // Ensure attributes are loaded const ensureAttributesLoaded = useCallback(async () => { if (attributes && attributes.length > 0) { return attributes; } const fetchedAttributes = await fetchAttributes(); return fetchedAttributes; }, [attributes, fetchAttributes]); // Fetch data when instanceId is available useEffect(() => { if (instanceId) { fetchAttributes(); fetchPermissions(); fetchPositions(); } }, [instanceId, fetchAttributes, fetchPermissions, fetchPositions]); return { positions, loading, error, refetch: fetchPositions, removeOptimistically, updateOptimistically, attributes, permissions, pagination, fetchPositionById, generateEditFieldsFromAttributes, ensureAttributesLoaded, instanceId }; } // Position operations hook export function useTrusteePositionOperations() { const instanceId = useInstanceId(); const [deletingPositions, setDeletingPositions] = useState>(new Set()); const [creatingPosition, setCreatingPosition] = useState(false); const { request, isLoading } = useApiRequest(); const [deleteError, setDeleteError] = useState(null); const [createError, setCreateError] = useState(null); const [updateError, setUpdateError] = useState(null); const handlePositionDelete = async (positionId: string) => { if (!instanceId) { setDeleteError('No instance context'); return false; } setDeleteError(null); setDeletingPositions(prev => new Set(prev).add(positionId)); try { await deletePositionApi(request, instanceId, positionId); await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { setDeleteError(error.message); return false; } finally { setDeletingPositions(prev => { const newSet = new Set(prev); newSet.delete(positionId); return newSet; }); } }; const handlePositionCreate = async (positionData: Partial) => { if (!instanceId) { setCreateError('No instance context'); return { success: false, error: 'No instance context' }; } setCreateError(null); setCreatingPosition(true); try { const newPosition = await createPositionApi(request, instanceId, positionData); return { success: true, positionData: newPosition }; } catch (error: any) { setCreateError(error.message); return { success: false, error: error.message }; } finally { setCreatingPosition(false); } }; const handlePositionUpdate = async ( positionId: string, updateData: Partial, _originalData?: any ) => { if (!instanceId) { setUpdateError('No instance context'); return { success: false, error: 'No instance context' }; } setUpdateError(null); try { const updatedPosition = await updatePositionApi(request, instanceId, positionId, updateData); return { success: true, positionData: updatedPosition }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update position'; const statusCode = error.response?.status; setUpdateError(errorMessage); return { success: false, error: errorMessage, statusCode, isPermissionError: statusCode === 403, isValidationError: statusCode === 400 }; } }; return { deletingPositions, creatingPosition, deleteError, createError, updateError, handlePositionDelete, handlePositionCreate, handlePositionUpdate, isLoading, instanceId }; }