/** * AdminFeatureRolesPage * * Admin page for managing FEATURE TEMPLATE ROLES. * These are roles that are copied to new feature instances. * * According to admin_ui_concept.md: * - Filter: featureCode!=null AND mandateId=null AND featureInstanceId=null * - View: Per feature (dropdown selection) * - Actions: Create feature role, edit description, manage AccessRules, delete role */ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { AccessRulesEditor } from '../../components/AccessRules'; import { FaPlus, FaSync, FaUserShield, FaCube, FaShieldAlt } from 'react-icons/fa'; import api from '../../api'; import styles from './Admin.module.css'; interface Feature { id?: string; code: string; // Backend uses 'code' not 'featureCode' featureCode?: string; // Alias for backward compatibility label: string | { [key: string]: string }; // Backend uses 'label' not 'name' name?: string | { [key: string]: string }; // Alias for backward compatibility description?: string | { [key: string]: string }; icon?: string; } interface FeatureRole { id: string; roleLabel: string; description?: { [key: string]: string }; featureCode: string; mandateId?: string | null; featureInstanceId?: string | null; isSystemRole?: boolean; } export const AdminFeatureRolesPage: React.FC = () => { // State const [features, setFeatures] = useState([]); const [selectedFeatureCode, setSelectedFeatureCode] = useState(''); const [roles, setRoles] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); const [editingRole, setEditingRole] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [permissionsRole, setPermissionsRole] = useState(null); // Load features on mount useEffect(() => { const loadFeatures = async () => { try { const response = await api.get('/api/features/'); const featureList = response.data?.items || response.data || []; setFeatures(Array.isArray(featureList) ? featureList : []); // Auto-select first feature if available if (featureList.length > 0 && !selectedFeatureCode) { setSelectedFeatureCode(featureList[0].code || featureList[0].featureCode || ''); } } catch (err: any) { console.error('Error loading features:', err); setError('Fehler beim Laden der Features'); } }; loadFeatures(); }, []); // Load roles when feature changes const fetchRoles = useCallback(async () => { if (!selectedFeatureCode) { setRoles([]); return; } setLoading(true); setError(null); try { const response = await api.get(`/api/features/templates/roles`, { params: { featureCode: selectedFeatureCode } }); const roleList = response.data || []; setRoles(Array.isArray(roleList) ? roleList : []); } catch (err: any) { console.error('Error loading feature roles:', err); setError('Fehler beim Laden der Feature-Rollen'); setRoles([]); } finally { setLoading(false); } }, [selectedFeatureCode]); useEffect(() => { fetchRoles(); }, [fetchRoles]); // Get text from multilingual object const getTextValue = (value: string | { [key: string]: string } | undefined): string => { if (!value) return '-'; if (typeof value === 'string') return value; return value.de || value.en || Object.values(value)[0] || '-'; }; // Table columns const columns = useMemo(() => [ { key: 'roleLabel', label: 'Rollen-Label', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 180 }, { key: 'description', label: 'Beschreibung', type: 'string' as const, sortable: false, width: 300, formatter: (value: string | { [key: string]: string }) => getTextValue(value) }, { key: 'featureCode', label: 'Feature', type: 'string' as const, sortable: true, filterable: true, width: 120, formatter: (value: string) => ( {value} ) }, ], []); // Form attributes for create const createFields: AttributeDefinition[] = useMemo(() => { const fields: AttributeDefinition[] = [ { name: 'roleLabel', label: 'Rollen-Label', type: 'string', required: true, description: 'Eindeutiger Bezeichner der Rolle (z.B. trustee-admin)' }, { name: 'description', label: 'Beschreibung', type: 'multilingual', required: false, description: 'Mehrsprachige Beschreibung der Rolle' } ]; return fields; }, []); // Form attributes for edit const editFields: AttributeDefinition[] = useMemo(() => { return [ { name: 'roleLabel', label: 'Rollen-Label', type: 'string', required: true, readonly: true, // Label should not be changed after creation description: 'Eindeutiger Bezeichner der Rolle' }, { name: 'description', label: 'Beschreibung', type: 'multilingual', required: false, description: 'Mehrsprachige Beschreibung der Rolle' } ]; }, []); // Handle create role const handleCreateRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => { if (!selectedFeatureCode) return; setIsSubmitting(true); try { const params = new URLSearchParams(); params.append('roleLabel', data.roleLabel); params.append('featureCode', selectedFeatureCode); await api.post(`/api/features/templates/roles?${params.toString()}`, data.description || {}); setShowCreateModal(false); await fetchRoles(); } catch (err: any) { console.error('Error creating role:', err); alert(err.response?.data?.detail || 'Fehler beim Erstellen der Rolle'); } finally { setIsSubmitting(false); } }; // Handle edit role const handleEditRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => { if (!editingRole) return; setIsSubmitting(true); try { await api.put(`/api/rbac/roles/${editingRole.id}`, { description: data.description }); setEditingRole(null); await fetchRoles(); } catch (err: any) { console.error('Error updating role:', err); alert(err.response?.data?.detail || 'Fehler beim Aktualisieren der Rolle'); } finally { setIsSubmitting(false); } }; // Handle delete role const handleDeleteRole = async (role: FeatureRole) => { if (window.confirm(`Möchten Sie die Rolle "${role.roleLabel}" wirklich löschen?`)) { try { await api.delete(`/api/rbac/roles/${role.id}`); await fetchRoles(); } catch (err: any) { console.error('Error deleting role:', err); alert(err.response?.data?.detail || 'Fehler beim Löschen der Rolle'); } } }; // Handle edit click const handleEditClick = (role: FeatureRole) => { setEditingRole(role); }; // Get feature name - Backend uses 'label' field const getFeatureName = (feature: Feature) => getTextValue(feature.label || feature.name); if (error && !selectedFeatureCode) { return (
⚠️

{error}

); } return (

Feature-Rollen

Template-Rollen für Feature-Instanzen verwalten

{/* Feature Selector */}
{selectedFeatureCode && (
)}
{/* Info Box */} {selectedFeatureCode && (
Feature-Template-Rollen werden bei der Erstellung neuer Feature-Instanzen automatisch kopiert. Änderungen an Template-Rollen wirken sich nicht auf bestehende Instanzen aus.
)} {/* Content */} {!selectedFeatureCode ? (

Kein Feature ausgewählt

Wählen Sie ein Feature aus, um dessen Template-Rollen zu verwalten.

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

Keine Rollen

Es gibt noch keine Template-Rollen für dieses Feature.

) : (
, onClick: (role: FeatureRole) => setPermissionsRole(role), title: 'Berechtigungen verwalten', } ]} onDelete={handleDeleteRole} hookData={{ refetch: fetchRoles, handleDelete: handleDeleteRole, }} emptyMessage="Keine Feature-Rollen gefunden" />
)} {/* Create Role Modal */} {showCreateModal && (
setShowCreateModal(false)}>
e.stopPropagation()}>

Neue Feature-Rolle erstellen

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

Feature-Rolle bearbeiten

Feature: {editingRole.featureCode}
setEditingRole(null)} submitButtonText={isSubmitting ? 'Speichern...' : 'Speichern'} cancelButtonText="Abbrechen" />
)} {/* Permissions Modal */} {permissionsRole && (
setPermissionsRole(null)}>
e.stopPropagation()}>

Berechtigungen: {permissionsRole.roleLabel}

Feature: {permissionsRole.featureCode} Template-Rolle (global)
setPermissionsRole(null)} />
)}
); }; export default AdminFeatureRolesPage;