/** * TrusteeDataTab * * Generic tab body that mounts a `FormGeneratorTable` for one Trustee data * model. The actual data hook (created via the `_createTrusteeEntityHook` * factory in `useTrustee.ts`) is provided by the parent * `TrusteeDataTablesView` so this component stays purely presentational. * * Modes: * - `readOnly: true` (default for sync tables, TrusteeData*, TrusteeAccounting*) * – no edit/delete/select UI. * - `readOnly: false` + `operationsHook` supplied – wires up edit/delete with * a `FormGeneratorForm` modal and respects backend RBAC permissions * (`permissions.update`, `permissions.delete`) returned by the entity hook. * * Layout chain: see `wiki/b-reference/frontend-nyla/formgenerator.md` * ("Page Layout Chain"). The parent provides `tableContainer`; this component * propagates `flex:1; min-height:0; flex-direction:column; width:100%`. */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FaSync } from 'react-icons/fa'; import { FormGeneratorTable } from '../../../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm } from '../../../../components/FormGenerator/FormGeneratorForm'; import { useLanguage } from '../../../../providers/language/LanguageContext'; import { useInstanceId } from '../../../../hooks/useCurrentInstance'; import adminStyles from '../../../admin/Admin.module.css'; export interface TrusteeDataTabProps { /** Result of the entity hook factory call (see `useTrustee.ts`). */ hookResult: any; /** Optional result of the matching operations hook (handleDelete/Update/Create). */ operationsHook?: any; /** Backend endpoint that backs the table (Unified Filter API enabled). */ apiEndpoint: string; /** Read-only mode hides edit/delete/select UI. */ readOnly?: boolean; /** Extra column keys to hide on top of the default system fields. */ hiddenColumns?: string[]; /** Optional initial sort applied on first load. */ initialSort?: Array<{ key: string; direction: 'asc' | 'desc' }>; /** Default page size for this tab (Sync-Tabellen können > 25 wollen). */ pageSize?: number; /** Empty-state message override. */ emptyMessage?: string; /** Human label for the entity (used in modal title, e.g. "Organisation"). */ entityLabel?: string; } const _DEFAULT_HIDDEN_COLUMNS = [ 'mandateId', 'featureInstanceId', '_hideDelete', '_permissions', ]; const _SYSTEM_FORM_FIELDS = [ 'id', 'mandateId', 'instanceId', 'featureInstanceId', 'sysCreatedAt', 'sysCreatedBy', 'sysModifiedAt', 'sysModifiedBy', ]; export const TrusteeDataTab: React.FC = ({ hookResult, operationsHook, apiEndpoint, readOnly = true, hiddenColumns, initialSort, pageSize = 25, emptyMessage, entityLabel, }) => { const { t } = useLanguage(); const instanceId = useInstanceId(); const { items, attributes, permissions, pagination, loading, error, refetch, fetchById, updateOptimistically, removeOptimistically, } = hookResult; const handleDelete = operationsHook?.handleDelete; const handleUpdate = operationsHook?.handleUpdate; const deletingItems: Set = operationsHook?.deletingItems ?? new Set(); // Permission gating (RBAC enforced by backend; we only avoid leaking buttons) const canUpdate = !readOnly && !!handleUpdate && permissions?.update !== 'n'; const canDelete = !readOnly && !!handleDelete && permissions?.delete !== 'n'; // Edit modal state const [editingRow, setEditingRow] = useState(null); const _tableRefetch = useCallback(async (params?: any) => { await refetch(params); }, [refetch]); const _refresh = useCallback(async () => { await _tableRefetch({ page: 1, pageSize, sort: initialSort }); }, [_tableRefetch, pageSize, initialSort]); useEffect(() => { _tableRefetch({ page: 1, pageSize, sort: initialSort }); }, [_tableRefetch, pageSize, initialSort]); const columns = useMemo(() => { const hidden = new Set([..._DEFAULT_HIDDEN_COLUMNS, ...(hiddenColumns || [])]); return (attributes || []) .filter((attr: any) => !hidden.has(attr.name)) .map((attr: any) => ({ key: attr.name, label: attr.label || attr.name, type: (attr.type as any) || 'text', sortable: attr.sortable !== false, filterable: attr.filterable !== false, searchable: attr.searchable !== false, width: attr.width || 150, minWidth: attr.minWidth || 100, maxWidth: attr.maxWidth || 400, fkSource: attr.fkSource, fkDisplayField: attr.fkDisplayField, frontendFormat: attr.frontendFormat, frontendFormatLabels: attr.frontendFormatLabels, })); }, [attributes, hiddenColumns]); const formAttributes = useMemo(() => { return (attributes || []).filter((attr: any) => !_SYSTEM_FORM_FIELDS.includes(attr.name)); }, [attributes]); const _handleEditClick = useCallback(async (row: any) => { if (!fetchById) { setEditingRow(row); return; } const full = await fetchById(row.id); setEditingRow(full || row); }, [fetchById]); const _handleDeleteRow = useCallback(async (row: any) => { if (!handleDelete) return; if (removeOptimistically) removeOptimistically(row.id); const ok = await handleDelete(row.id); if (!ok) await _tableRefetch(); }, [handleDelete, removeOptimistically, _tableRefetch]); const _handleFormSubmit = useCallback(async (data: any) => { if (!editingRow || !handleUpdate) return; const result = await handleUpdate(editingRow.id, data); if (result?.success) { setEditingRow(null); await _tableRefetch(); } }, [editingRow, handleUpdate, _tableRefetch]); const _handleInlineUpdate = useCallback(async (itemId: string, updateData: any, row: any) => { if (!handleUpdate) return; if (updateOptimistically) updateOptimistically(itemId, updateData); const result = await handleUpdate(itemId, { ...row, ...updateData }); if (!result?.success) await _tableRefetch(); }, [handleUpdate, updateOptimistically, _tableRefetch]); // Layout: bounded height chain inside parent's `.tableContainer` const _rootStyle: React.CSSProperties = { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, width: '100%', }; const _tableWrapStyle: React.CSSProperties = { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%', }; if (error) { return (
⚠️

{t('Fehler beim Laden: {detail}', { detail: String(error) })}

); } const actionButtons: any[] = []; if (canUpdate) { actionButtons.push({ type: 'edit' as const, onAction: _handleEditClick, title: t('Bearbeiten'), }); } if (canDelete) { actionButtons.push({ type: 'delete' as const, title: t('Löschen'), loading: (row: any) => deletingItems.has(row.id), }); } return (
{editingRow && canUpdate && (

{entityLabel ? t('{label} bearbeiten', { label: entityLabel }) : t('Bearbeiten')}

{formAttributes.length === 0 ? (
{t('Lade Formular')}
) : ( setEditingRow(null)} submitButtonText={t('Speichern')} cancelButtonText={t('Abbrechen')} instanceId={instanceId || undefined} /> )}
)}
); }; export default TrusteeDataTab;