/** * Real Estate Hooks * * Hooks für das Real Estate/PEK-Feature mit Instanz-Kontext. * Die instanceId wird automatisch aus der URL gelesen (Feature-Instanz-Route). * Analog zu useTrustee.ts für backend-driven FormGeneratorTable. */ 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 { type RealEstateProject, type RealEstateParcel, type PaginationParams, fetchProjects as fetchProjectsApi, fetchProjectById as fetchProjectByIdApi, createProject as createProjectApi, updateProject as updateProjectApi, deleteProject as deleteProjectApi, fetchParcels as fetchParcelsApi, fetchParcelById as fetchParcelByIdApi, createParcel as createParcelApi, updateParcel as updateParcelApi, deleteParcel as deleteParcelApi, } from '../api/realEstateApi'; export type { RealEstateProject, RealEstateParcel, 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 REAL ESTATE ENTITY HOOK FACTORY // ============================================================================ interface RealEstateEntityConfig { 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 _createRealEstateEntityHook(config: RealEstateEntityConfig) { return function useRealEstateEntity() { 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/realestate/${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 (err: any) { console.error(`Error fetching ${config.entityName} attributes:`, err); setAttributes([]); return []; } }, [instanceId]); const fetchPermissions = useCallback(async () => { try { const objectKey = `data.feature.realestate.${config.entityName}`; const perms = await checkPermission('DATA', objectKey); setPermissions(perms); return perms; } catch (err: any) { console.error(`Error fetching ${config.entityName} permissions:`, err); 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 { 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 nonEditable = ['sysCreatedBy', 'sysCreatedAt', 'sysModifiedBy', 'sysModifiedAt']; return !nonEditable.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; let optionsReference: string | 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 as any[]).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 as any[]).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 => !['sysCreatedBy', 'sysCreatedAt', 'sysModifiedBy', 'sysModifiedAt', 'mandateId', 'featureInstanceId'].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; let optionsReference: string | 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 as any[]).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 as any[]).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]); 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, }; }; } function _createRealEstateOperationsHook(config: RealEstateEntityConfig) { return function useRealEstateEntityOperations() { const instanceId = useInstanceId(); const [deletingItems, setDeletingItems] = useState>(new Set()); const [creatingItem, setCreatingItem] = useState(false); const { request } = 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 (err: any) { setDeleteError(err.message); return false; } finally { setDeletingItems(prev => { const next = new Set(prev); next.delete(itemId); return next; }); } }, [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); try { const newItem = await config.create(request, instanceId, itemData); return { success: true, data: newItem }; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.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 (err: any) { const errorMessage = err.response?.data?.message || err.message || 'Failed to update'; setUpdateError(errorMessage); return { success: false, error: errorMessage, statusCode: err.response?.status, isPermissionError: err.response?.status === 403, isValidationError: err.response?.status === 400, }; } }, [request, instanceId]); return { deletingItems, creatingItem, deleteError, createError, updateError, handleDelete, handleCreate, handleUpdate, instanceId, }; }; } // ============================================================================ // PROJECT HOOKS // ============================================================================ const projectConfig: RealEstateEntityConfig = { entityName: 'Projekt', fetchAll: fetchProjectsApi, fetchById: fetchProjectByIdApi, create: createProjectApi, update: updateProjectApi, deleteItem: deleteProjectApi, }; export const useRealEstateProjects = _createRealEstateEntityHook(projectConfig); export const useRealEstateProjectOperations = _createRealEstateOperationsHook(projectConfig); // ============================================================================ // PARCEL HOOKS // ============================================================================ const parcelConfig: RealEstateEntityConfig = { entityName: 'Parzelle', fetchAll: fetchParcelsApi, fetchById: fetchParcelByIdApi, create: createParcelApi, update: updateParcelApi, deleteItem: deleteParcelApi, }; export const useRealEstateParcels = _createRealEstateEntityHook(parcelConfig); export const useRealEstateParcelOperations = _createRealEstateOperationsHook(parcelConfig);