/** * 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'; import { useLanguage } from '../../providers/language/LanguageContext'; // Types for cleanup result interface DuplicateGroup { roleId: string; context: string; item: string; totalCount: number; keepId: string; deleteCount: number; deleteIds: string[]; } interface CleanupResult { totalRules: number; uniqueSignatures: number; duplicateGroups: number; duplicateRulesToDelete: number; deletedCount: number; details: DuplicateGroup[]; } interface TemplateFixDetail { userMandateRoleId: string; userMandateId: string; mandateId: string; templateRoleId: string; templateRoleLabel: string; instanceRoleId?: string; action: string; } interface TemplateFixResult { totalUserMandateRoles: number; invalidAssignments: number; fixedCount: number; details: TemplateFixDetail[]; } export const AdminMandateRolePermissionsPage: React.FC = () => { const { t } = useLanguage(); 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 [templateFixResult, setTemplateFixResult] = 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]); const getTextValue = (value: string | Record | undefined): string => { if (!value) return ''; if (typeof value === 'string') return value; return Object.values(value).find(v => !!v) || ''; }; // 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 ( {t('System-Template')} ); } if (!role.mandateId) { return ( {t('Template')} ); } return ( {t('Mandant')} ); }; // --- Cleanup functions --- const _openCleanupModal = useCallback(async () => { setShowCleanupModal(true); setCleanupError(null); setCleanupResult(null); setTemplateFixResult(null); setCleanupPhase('idle'); setCleanupLoading(true); try { const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=true'); const data = response.data; setCleanupResult(data.duplicateRules || data); setTemplateFixResult(data.templateRoleAssignments || null); setCleanupPhase('preview'); } catch (err: any) { setCleanupError(err?.response?.data?.detail || err?.message || t('Fehler beim Laden der Duplikate')); } finally { setCleanupLoading(false); } }, [t]); const _executeCleanup = useCallback(async () => { setCleanupLoading(true); setCleanupError(null); try { const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=false'); const data = response.data; setCleanupResult(data.duplicateRules || data); setTemplateFixResult(data.templateRoleAssignments || null); setCleanupPhase('done'); // Refresh roles after cleanup if (selectedMandateId) { fetchRoles(selectedMandateId, { scopeFilter }); } } catch (err: any) { setCleanupError(err?.response?.data?.detail || err?.message || t('Fehler beim Bereinigen')); } finally { setCleanupLoading(false); } }, [selectedMandateId, scopeFilter, fetchRoles, t]); const _closeCleanupModal = useCallback(() => { setShowCleanupModal(false); setCleanupResult(null); setCleanupError(null); setCleanupPhase('idle'); }, []); // Filter options for scope const scopeOptions = useMemo(() => [ { value: 'mandate', label: t('Mandanten-Rollen') }, { value: 'all', label: t('Alle inkl. Templates') }, { value: 'global', label: t('Nur Templates') }, ], [t]); if (error) { return (
⚠️

{t('Fehler beim Laden')}: {error}

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

{t('Rollen-Berechtigungen')}

{t('Verwalten Sie die Zugriffsrechte für Mandanten- und globale Rollen')}

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

{t('Keine Rollen gefunden')}

{scopeFilter === 'mandate' ? t('Es gibt noch keine Mandanten-Rollen. System-Rollen werden bei der Mandant-Erstellung automatisch kopiert.') : scopeFilter === 'global' ? t('Es gibt noch keine Rollentemplates') : t('Es gibt noch keine Rollen')}

)} {/* 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) && (
{t('Dies ist eine')} {t('Template-Rolle')}.{' '} {t('Änderungen an den Berechtigungen wirken sich nur auf neu erstellte Mandanten aus. Bestehende Mandanten-Instanzen werden nicht aktualisiert.')}
)}
)}
))}
)} {/* Cleanup Duplicates Modal */} {showCleanupModal && (
e.stopPropagation()}>

{t('Doppelte Regeln bereinigen')}

{/* Loading */} {cleanupLoading && (
{cleanupPhase === 'idle' ? t('Analysiere Duplikate') : t('Bereinige Duplikate')}
)} {/* Error */} {cleanupError && (
{cleanupError}
)} {/* Results */} {cleanupResult && !cleanupLoading && ( <> {/* Summary Cards */}
{cleanupResult.totalRules}
{t('Regeln total')}
{cleanupResult.uniqueSignatures}
{t('Eindeutige Regeln')}
0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}>
0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}
{t('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' ? t('Gelöscht') : t('Zu löschen')}
{/* Status Message */} {cleanupPhase === 'done' && (
{cleanupResult.deletedCount} {t('Doppelte Regeln wurden erfolgreich entfernt')}
)} {cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && (
{t('Keine Duplikate gefunden, alles sauber')}
)} {/* Details Table */} {cleanupResult.details && cleanupResult.details.length > 0 && (

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

{cleanupResult.details.map((group, idx) => ( ))}
{t('Kontext')} {t('Item')} {t('Total')} {t('Duplikate')}
{group.context} {group.item} {group.totalCount} {group.deleteCount}
)} {/* Template Role Assignments Section */} {templateFixResult && templateFixResult.invalidAssignments > 0 && (

{t('Template-Rollen-Zuweisungen')}

{templateFixResult.totalUserMandateRoles}
{t('Rollenzuweisungen total')}
0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${templateFixResult.invalidAssignments > 0 ? '#fc8181' : '#9ae6b4'}` }}>
0 ? '#c53030' : '#2f855a' }}>{templateFixResult.invalidAssignments}
{t('Template statt Instanz')}
0 ? '#2f855a' : 'var(--text-primary)' }}> {cleanupPhase === 'done' ? templateFixResult.fixedCount : templateFixResult.invalidAssignments}
{cleanupPhase === 'done' ? t('Repariert') : t('Zu reparieren')}
{templateFixResult.details && templateFixResult.details.length > 0 && (
{templateFixResult.details.map((detail, idx) => ( ))}
{t('Rolle')} {t('Mandant')} {t('Aktion')}
{detail.templateRoleLabel} {detail.mandateId.substring(0, 8)}... {detail.action}
)}
)} {templateFixResult && templateFixResult.invalidAssignments === 0 && (
{t('Keine fehlerhaften Templaterollenzuweisungen')}
)} )}
{cleanupPhase === 'preview' && cleanupResult && (cleanupResult.duplicateRulesToDelete > 0 || (templateFixResult && templateFixResult.invalidAssignments > 0)) && ( )}
)}
); }; export default AdminMandateRolePermissionsPage;