/** * AdminMandateRolesPage * * Admin page for managing ALL ROLES (system + global + mandate-specific). * Consolidated view replacing separate System-Roles page. * * Shows: * - System roles (admin, user, viewer) - read-only, cannot be deleted * - Global roles (mandateId=null) - CRUD available * - Mandate-specific roles (mandateId=xyz) - CRUD available * - Feature-template roles are managed in AdminFeatureRolesPage * * ALL filtering, sorting, and pagination is handled by the backend. */ import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useMandateRoles, type Role, type RoleCreate, type RoleUpdate, type PaginationParams } from '../../hooks/useMandateRoles'; import { useUserMandates, type Mandate } from '../../hooks/useUserMandates'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { FaPlus, FaSync, FaUserShield, FaBuilding, FaGlobe, FaShieldAlt, FaCube } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; import styles from './Admin.module.css'; export const AdminMandateRolesPage: React.FC = () => { const navigate = useNavigate(); const { showError, showWarning } = useToast(); const { roles, loading, error, pagination, fetchRoles, createRole, updateRole, deleteRole, } = useMandateRoles(); const { fetchMandates } = useUserMandates(); // State const [mandates, setMandates] = useState([]); const [selectedMandateId, setSelectedMandateId] = useState(''); const [showCreateModal, setShowCreateModal] = useState(false); const [editingRole, setEditingRole] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('mandate'); const [backendAttributes, setBackendAttributes] = useState([]); // Store current filter state for refetch const currentScopeFilterRef = useRef(scopeFilter); currentScopeFilterRef.current = scopeFilter; // Load mandates and attributes on mount useEffect(() => { const loadMandates = async () => { const data = await fetchMandates(); setMandates(data); if (data.length > 0 && !selectedMandateId) { setSelectedMandateId(data[0].id); } }; loadMandates(); // Fetch Role attributes from backend api.get('/api/attributes/Role').then(response => { const attrs = response.data?.attributes || response.data || []; setBackendAttributes(Array.isArray(attrs) ? attrs : []); }).catch(() => setBackendAttributes([])); }, [fetchMandates]); // Load roles when mandate or scopeFilter changes useEffect(() => { if (selectedMandateId) { fetchRoles(selectedMandateId, { scopeFilter }); } }, [selectedMandateId, scopeFilter, fetchRoles]); // Refetch wrapper that accepts pagination params from FormGeneratorTable // and includes the current mandateId and scopeFilter const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => { if (!selectedMandateId) return; // Merge pagination params with current filter state return fetchRoles(selectedMandateId, { ...paginationParams, scopeFilter: currentScopeFilterRef.current }); }, [selectedMandateId, fetchRoles]); // Get description text const getDescriptionText = (desc: string | { [key: string]: string } | undefined) => { if (!desc) return '-'; if (typeof desc === 'string') return desc; return desc.de || desc.en || Object.values(desc)[0] || '-'; }; // Table columns - scopeType is now a backend-computed field const columns = useMemo(() => [ { key: 'roleLabel', label: 'Bezeichnung', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 150 }, { key: 'description', label: 'Beschreibung', type: 'string' as const, sortable: false, filterable: false, width: 250, formatter: (value: string | { [key: string]: string }) => getDescriptionText(value) }, { key: 'scopeType', label: 'Geltungsbereich', type: 'string' as const, sortable: true, filterable: true, width: 140, formatter: (value: string) => { if (value === 'system') { return ( System-Template ); } if (value === 'global') { return ( Template ); } return ( Mandant ); } }, ], []); // Form attributes from backend - for create form const createFields: AttributeDefinition[] = useMemo(() => { const excludedFields = ['id', 'mandateId', 'featureInstanceId', 'featureCode', 'isSystemRole', 'scopeType']; const fields = backendAttributes .filter(attr => !excludedFields.includes(attr.name)) .map(attr => ({ ...attr })) as AttributeDefinition[]; // Add scope field for mandate/global selection (not a model attribute) if (fields.length > 0) { fields.push({ name: 'scope', label: 'Geltungsbereich', type: 'enum' as any, required: true, default: 'mandate', options: [ { value: 'mandate', label: 'Nur dieser Mandant' }, { value: 'global', label: 'Template (wird bei neuen Mandanten kopiert)' }, ] }); } return fields; }, [backendAttributes]); // Form attributes from backend - for edit form // NOTE: mandateId/featureInstanceId/featureCode are IMMUTABLE - only description can be edited const editFields: AttributeDefinition[] = useMemo(() => { const excludedFields = ['id', 'mandateId', 'featureInstanceId', 'featureCode', 'isSystemRole', 'scopeType']; const fields = backendAttributes .filter(attr => !excludedFields.includes(attr.name)) .map(attr => ({ ...attr, // Mark roleLabel as readonly (cannot change after creation) readonly: attr.name === 'roleLabel' ? true : attr.readonly, })) as AttributeDefinition[]; // No scope field for edit - context is immutable! return fields; }, [backendAttributes]); // Handle create role const handleCreateRole = async (data: { roleLabel: string; description?: string | { [key: string]: string }; scope: 'mandate' | 'global' }) => { if (!selectedMandateId) return; setIsSubmitting(true); try { // Ensure description is always a multilingual object let description: { [key: string]: string } = {}; if (typeof data.description === 'string') { description = { de: data.description, en: data.description }; } else if (data.description && typeof data.description === 'object') { description = data.description; } const roleData: RoleCreate = { roleLabel: data.roleLabel.toLowerCase().replace(/\s+/g, '_'), description: description, mandateId: data.scope === 'mandate' ? selectedMandateId : undefined }; const result = await createRole(roleData, selectedMandateId); if (result.success) { setShowCreateModal(false); await fetchRoles(selectedMandateId, { scopeFilter }); } else { showError('Fehler', result.error || 'Fehler beim Erstellen der Rolle'); } } catch (err: any) { console.error('Create role error:', err); showError('Fehler', err.message || 'Fehler beim Erstellen der Rolle'); } finally { setIsSubmitting(false); } }; // Handle edit role const handleEditRole = async (data: RoleUpdate & { scope?: 'mandate' | 'global' }) => { if (!editingRole) return; setIsSubmitting(true); try { // Ensure description is always a multilingual object let description: { [key: string]: string } | undefined; if (typeof data.description === 'string') { description = { de: data.description, en: data.description }; } else if (data.description && typeof data.description === 'object') { description = data.description as { [key: string]: string }; } // Convert scope to mandateId - NOTE: Context fields are IMMUTABLE per concept! // We should not be changing mandateId after creation const updateData: RoleUpdate = { roleLabel: data.roleLabel, description: description, // mandateId is immutable - don't include in update }; const result = await updateRole(editingRole.id, updateData); if (result.success) { setEditingRole(null); if (selectedMandateId) { await fetchRoles(selectedMandateId, { scopeFilter }); } } else { showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rolle'); } } catch (err: any) { console.error('Update role error:', err); showError('Fehler', err.message || 'Fehler beim Aktualisieren der Rolle'); } finally { setIsSubmitting(false); } }; // Handle delete role (confirmation handled by DeleteActionButton) const handleDeleteRole = async (role: Role) => { if (role.isSystemRole) { showWarning('Nicht erlaubt', 'System-Rollen können nicht gelöscht werden.'); return; } const result = await deleteRole(role.id); if (result.success) { // Refetch to update the list await fetchRoles(selectedMandateId, { scopeFilter }); } else { showError('Fehler', result.error || 'Fehler beim Löschen der Rolle'); } }; // Handle edit click const handleEditClick = (role: Role) => { setEditingRole(role); }; // Get mandate name const getMandateName = (mandate: Mandate) => { if (mandate.label) return mandate.label; if (typeof mandate.name === 'object') { return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id; } return mandate.name || mandate.id; }; if (error && !selectedMandateId) { return (
⚠️

Fehler: {error}

); } return (

Rollen

Verwalten Sie System-, globale und mandantenspezifische Rollen

{/* Mandate Selector and Filters */}
{selectedMandateId && (
)}
{/* Info Box */} {selectedMandateId && (
System-Templates (admin, user, viewer) werden bei der Mandant-Erstellung automatisch als Mandanten-Instanz-Rollen kopiert. Templates selbst können nicht gelöscht werden. Mandanten-Rollen gelten nur für den ausgewählten Mandanten und sind den Benutzern zuweisbar.
)} {/* Content */} {!selectedMandateId ? (

Kein Mandant ausgewählt

Wählen Sie einen Mandanten aus, um dessen Rollen zu verwalten.

) : loading && roles.length === 0 ? (
Lade Rollen...
) : roles.length === 0 ? (

Keine Rollen

{scopeFilter === 'mandate' ? 'Es gibt noch keine Mandanten-Rollen. System-Rollen werden bei der Mandant-Erstellung automatisch kopiert.' : scopeFilter === 'global' ? 'Es gibt noch keine Rollen-Templates.' : 'Es gibt noch keine Rollen für diesen Mandanten.'}

) : (
row.isSystemRole ? { disabled: true, message: 'System-Rollen können nicht bearbeitet werden' } : false }, { type: 'delete' as const, title: 'Rolle löschen', disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: 'System-Rollen können nicht gelöscht werden' } : false } ]} onDelete={handleDeleteRole} hookData={{ refetch: refetchWithParams, pagination: pagination, handleDelete: handleDeleteRole, }} emptyMessage="Keine Rollen gefunden" />
)} {/* Create Role Modal */} {showCreateModal && (
setShowCreateModal(false)}>
e.stopPropagation()}>

Neue Rolle erstellen

{createFields.length === 0 ? (
Lade Formular...
) : ( setShowCreateModal(false)} submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'} cancelButtonText="Abbrechen" /> )}
)} {/* Edit Role Modal */} {editingRole && (
setEditingRole(null)}>
e.stopPropagation()}>

Rolle bearbeiten: {editingRole.roleLabel}

{editFields.length === 0 ? (
Lade Formular...
) : ( <>
Geltungsbereich: {editingRole.mandateId ? 'Mandanten-Instanz' : 'Template (global)'} {' '}(kann nicht geändert werden)
setEditingRole(null)} submitButtonText={isSubmitting ? 'Speichern...' : 'Speichern'} cancelButtonText="Abbrechen" /> )}
)}
); }; export default AdminMandateRolesPage;