/** * AdminMandateRolePermissionsPage * * Admin page for managing access rules (permissions) for mandate-level roles. * Similar to TrusteeInstanceRolesView but for mandate/global roles. * * Shows: * - System roles (admin, user, viewer) - read-only permissions * - Global roles (mandateId=null) - editable permissions * - Mandate-specific roles (mandateId=xyz) - editable permissions * * Each role can be expanded to show/edit its AccessRules via AccessRulesEditor. * * Includes a "Cleanup Duplicates" tool to find and remove duplicate AccessRules. */ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useMandateRoles, type Role } from '../../hooks/useMandateRoles'; import { useUserMandates, type Mandate } from '../../hooks/useUserMandates'; import { AccessRulesEditor } from '../../components/AccessRules'; import api from '../../api'; import { FaUserShield, FaShieldAlt, FaSync, FaChevronDown, FaChevronRight, FaGlobe, FaBuilding, FaFilter, FaBroom, FaTimes, FaExclamationTriangle, FaCheckCircle } from 'react-icons/fa'; import styles from './Admin.module.css'; // Types for cleanup result interface DuplicateGroup { roleId: string; context: string; item: string; totalCount: number; keepId: string; deleteCount: number; deleteIds: string[]; } interface CleanupResult { dryRun: boolean; totalRules: number; uniqueSignatures: number; duplicateGroups: number; duplicateRulesToDelete: number; deletedCount: number; details: DuplicateGroup[]; } export const AdminMandateRolePermissionsPage: React.FC = () => { const { roles, loading, error, fetchRoles, } = useMandateRoles(); const { fetchMandates } = useUserMandates(); // State const [mandates, setMandates] = useState([]); const [selectedMandateId, setSelectedMandateId] = useState(''); const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('mandate'); const [expandedRoleId, setExpandedRoleId] = useState(null); // Cleanup state const [showCleanupModal, setShowCleanupModal] = useState(false); const [cleanupLoading, setCleanupLoading] = useState(false); const [cleanupResult, setCleanupResult] = useState(null); const [cleanupError, setCleanupError] = useState(null); const [cleanupPhase, setCleanupPhase] = useState<'idle' | 'preview' | 'done'>('idle'); // Load mandates on mount useEffect(() => { const loadMandates = async () => { const data = await fetchMandates(); setMandates(data); if (data.length > 0 && !selectedMandateId) { setSelectedMandateId(data[0].id); } }; loadMandates(); }, [fetchMandates]); // Load roles when mandate or scopeFilter changes useEffect(() => { if (selectedMandateId) { fetchRoles(selectedMandateId, { scopeFilter }); } }, [selectedMandateId, scopeFilter, fetchRoles]); // Refetch roles const handleRefresh = useCallback(async () => { if (selectedMandateId) { await fetchRoles(selectedMandateId, { scopeFilter }); } }, [selectedMandateId, scopeFilter, fetchRoles]); // Get description 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] || ''; }; // Toggle role expansion const toggleRole = (roleId: string) => { setExpandedRoleId(prev => prev === roleId ? null : roleId); }; // Check if a role is a template (not bound to a specific mandate) const _isTemplateRole = (role: Role): boolean => { return !!role.isSystemRole || !role.mandateId; }; // Get scope badge const getScopeBadge = (role: Role) => { if (role.isSystemRole) { return ( System-Template ); } if (!role.mandateId) { return ( Template ); } return ( Mandant ); }; // --- Cleanup functions --- const _openCleanupModal = useCallback(async () => { setShowCleanupModal(true); setCleanupError(null); setCleanupResult(null); setCleanupPhase('idle'); setCleanupLoading(true); try { const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=true'); setCleanupResult(response.data); setCleanupPhase('preview'); } catch (err: any) { setCleanupError(err?.response?.data?.detail || err?.message || 'Fehler beim Laden der Duplikate'); } finally { setCleanupLoading(false); } }, []); const _executeCleanup = useCallback(async () => { setCleanupLoading(true); setCleanupError(null); try { const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=false'); setCleanupResult(response.data); setCleanupPhase('done'); // Refresh roles after cleanup if (selectedMandateId) { fetchRoles(selectedMandateId, { scopeFilter }); } } catch (err: any) { setCleanupError(err?.response?.data?.detail || err?.message || 'Fehler beim Bereinigen'); } finally { setCleanupLoading(false); } }, [selectedMandateId, scopeFilter, fetchRoles]); const _closeCleanupModal = useCallback(() => { setShowCleanupModal(false); setCleanupResult(null); setCleanupError(null); setCleanupPhase('idle'); }, []); // Filter options for scope const scopeOptions = useMemo(() => [ { value: 'mandate', label: 'Mandanten-Rollen' }, { value: 'all', label: 'Alle (inkl. Templates)' }, { value: 'global', label: 'Nur Templates' }, ], []); if (error) { return (
⚠️

Fehler beim Laden: {error}

); } return (
{/* Header */}

Rollen-Berechtigungen

Verwalten Sie die Zugriffsrechte für Mandanten- und globale Rollen

{/* Filters */}
{/* Info Box */}
Klicken Sie auf eine Rolle, um deren Berechtigungen (AccessRules) zu bearbeiten. Template-Rollen sind schreibgeschützt - Änderungen an Templates wirken sich nur auf neu erstellte Mandanten aus. Mandanten-Rollen sind direkt bearbeitbar.
{/* Loading State */} {loading && (
Lade Rollen...
)} {/* Empty State */} {!loading && roles.length === 0 && (

Keine Rollen gefunden

{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.'}

)} {/* Roles List */} {!loading && roles.length > 0 && (
{roles.map(role => (
{/* Role Header - Clickable to expand */}
toggleRole(role.id)} >
{expandedRoleId === role.id ? : } {role.roleLabel} {getTextValue(role.description)}
{getScopeBadge(role)}
{/* Expanded Content - AccessRulesEditor */} {expandedRoleId === role.id && (
{_isTemplateRole(role) && (
Dies ist eine Template-Rolle. Änderungen an den Berechtigungen wirken sich nur auf neu erstellte Mandanten aus. Bestehende Mandanten-Instanzen werden nicht aktualisiert.
)}
)}
))}
)} {/* Cleanup Duplicates Modal */} {showCleanupModal && (
e.stopPropagation()}>

Doppelte Regeln bereinigen

{/* Loading */} {cleanupLoading && (
{cleanupPhase === 'idle' ? 'Analysiere Duplikate...' : 'Bereinige Duplikate...'}
)} {/* Error */} {cleanupError && (
{cleanupError}
)} {/* Results */} {cleanupResult && !cleanupLoading && ( <> {/* Summary Cards */}
{cleanupResult.totalRules}
Regeln total
{cleanupResult.uniqueSignatures}
Eindeutige Regeln
0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}>
0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}
Duplikat-Gruppen
0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateRulesToDelete > 0 ? '#fc8181' : '#9ae6b4'}` }}>
0 ? '#c53030' : '#2f855a' }}> {cleanupPhase === 'done' ? cleanupResult.deletedCount : cleanupResult.duplicateRulesToDelete}
{cleanupPhase === 'done' ? 'Geloescht' : 'Zu loeschen'}
{/* Status Message */} {cleanupPhase === 'done' && (
{cleanupResult.deletedCount} doppelte Regeln wurden erfolgreich entfernt.
)} {cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && (
Keine Duplikate gefunden. Alles sauber!
)} {/* Details Table */} {cleanupResult.details.length > 0 && (

Duplikat-Details {cleanupResult.details.length < cleanupResult.duplicateGroups && `(${cleanupResult.details.length} von ${cleanupResult.duplicateGroups})`}

{cleanupResult.details.map((group, idx) => ( ))}
Kontext Item Total Duplikate
{group.context} {group.item} {group.totalCount} {group.deleteCount}
)} )}
{cleanupPhase === 'preview' && cleanupResult && cleanupResult.duplicateRulesToDelete > 0 && ( )}
)}
); }; export default AdminMandateRolePermissionsPage;