frontend_nyla/src/pages/admin/AdminMandateRolePermissionsPage.tsx
2026-04-13 09:54:11 +02:00

602 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<Mandate[]>([]);
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('mandate');
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
// Cleanup state
const [showCleanupModal, setShowCleanupModal] = useState(false);
const [cleanupLoading, setCleanupLoading] = useState(false);
const [cleanupResult, setCleanupResult] = useState<CleanupResult | null>(null);
const [templateFixResult, setTemplateFixResult] = useState<TemplateFixResult | null>(null);
const [cleanupError, setCleanupError] = useState<string | null>(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<string, string> | 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 (
<span className={styles.badge} style={{ background: 'var(--warning-color, #d69e2e)', color: 'white' }}>
<FaUserShield style={{ marginRight: 4 }} /> {t('System-Template')}
</span>
);
}
if (!role.mandateId) {
return (
<span className={styles.badge} style={{ background: 'var(--info-color, #3182ce)', color: 'white' }}>
<FaGlobe style={{ marginRight: 4 }} /> {t('Template')}
</span>
);
}
return (
<span className={styles.badge} style={{ background: 'var(--success-color, #38a169)', color: 'white' }}>
<FaBuilding style={{ marginRight: 4 }} /> {t('Mandant')}
</span>
);
};
// --- 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 (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>
{t('Fehler beim Laden')}: {error}
</p>
<button className={styles.secondaryButton} onClick={handleRefresh}>
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
);
}
return (
<div className={styles.adminPage}>
{/* Header */}
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>
<FaShieldAlt style={{ marginRight: '0.5rem' }} />
{t('Rollen-Berechtigungen')}
</h1>
<p className={styles.pageSubtitle}>
{t('Verwalten Sie die Zugriffsrechte für Mandanten- und globale Rollen')}
</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={_openCleanupModal}
disabled={loading}
title={t('Doppelte Regeln finden und bereinigen')}
>
<FaBroom /> {t('Duplikate bereinigen')}
</button>
<button
className={styles.secondaryButton}
onClick={handleRefresh}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
</div>
</div>
{/* Filters */}
<div className={styles.filterBar}>
<div className={styles.filterGroup}>
<label className={styles.filterLabel}>{t('Mandant')}</label>
<select
className={styles.filterSelect}
value={selectedMandateId}
onChange={(e) => setSelectedMandateId(e.target.value)}
>
{mandates.map(mandate => (
<option key={mandate.id} value={mandate.id}>
{mandate.label || getTextValue(mandate.name)}
</option>
))}
</select>
</div>
<div className={styles.filterGroup}>
<label className={styles.filterLabel}>
<FaFilter style={{ marginRight: 4 }} /> {t('Bereich')}:
</label>
<select
className={styles.filterSelect}
value={scopeFilter}
onChange={(e) => setScopeFilter(e.target.value as 'all' | 'mandate' | 'global')}
>
{scopeOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
</div>
{/* Info Box */}
<div className={styles.infoBox}>
<FaShieldAlt style={{ marginRight: '0.5rem' }} />
<span>
{t('Klicken Sie auf eine Rolle, um deren Berechtigungen (AccessRules) zu bearbeiten.')}{' '}
<strong>{t('Template-Rollen')}</strong>{' '}
{t('sind schreibgeschützt Änderungen an Templates wirken sich nur auf neu erstellte Mandanten aus.')}{' '}
<strong>{t('Mandanten-Rollen')}</strong> {t('sind direkt bearbeitbar.')}
</span>
</div>
{/* Loading State */}
{loading && (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('Lade Rollen')}</span>
</div>
)}
{/* Empty State */}
{!loading && roles.length === 0 && (
<div className={styles.emptyState}>
<FaUserShield className={styles.emptyIcon} />
<p>{t('Keine Rollen gefunden')}</p>
<p className={styles.emptyHint}>
{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')}
</p>
</div>
)}
{/* Roles List */}
{!loading && roles.length > 0 && (
<div className={styles.rolesList}>
{roles.map(role => (
<div key={role.id} className={styles.roleCard}>
{/* Role Header - Clickable to expand */}
<div
className={styles.roleHeader}
onClick={() => toggleRole(role.id)}
>
<div className={styles.roleInfo}>
<span className={styles.expandIcon}>
{expandedRoleId === role.id ? <FaChevronDown /> : <FaChevronRight />}
</span>
<span className={styles.roleLabel}>{role.roleLabel}</span>
<span className={styles.roleDescription}>
{getTextValue(role.description)}
</span>
</div>
<div className={styles.roleBadges}>
{getScopeBadge(role)}
</div>
</div>
{/* Expanded Content - AccessRulesEditor */}
{expandedRoleId === role.id && (
<div className={styles.roleContent}>
{_isTemplateRole(role) && (
<div className={styles.infoBox} style={{ marginBottom: '0.75rem', background: 'var(--warning-bg, #fffbeb)', borderColor: 'var(--warning-color, #d69e2e)' }}>
<FaUserShield style={{ marginRight: '0.5rem', color: 'var(--warning-color, #d69e2e)' }} />
<span>
{t('Dies ist eine')} <strong>{t('Template-Rolle')}</strong>.{' '}
{t('Änderungen an den Berechtigungen wirken sich nur auf neu erstellte Mandanten aus. Bestehende Mandanten-Instanzen werden nicht aktualisiert.')}
</span>
</div>
)}
<AccessRulesEditor
roleId={role.id}
roleName={role.roleLabel}
isTemplate={_isTemplateRole(role)}
readOnly={false}
apiBasePath="/api/rbac"
mandateId={selectedMandateId}
/>
</div>
)}
</div>
))}
</div>
)}
{/* Cleanup Duplicates Modal */}
{showCleanupModal && (
<div className={styles.modalOverlay} onClick={_closeCleanupModal}>
<div className={styles.modal} style={{ maxWidth: '750px' }} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h3 className={styles.modalTitle}>
<FaBroom style={{ marginRight: '0.5rem' }} />
{t('Doppelte Regeln bereinigen')}
</h3>
<button className={styles.modalClose} onClick={_closeCleanupModal}>
<FaTimes />
</button>
</div>
<div className={styles.modalContent}>
{/* Loading */}
{cleanupLoading && (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{cleanupPhase === 'idle' ? t('Analysiere Duplikate') : t('Bereinige Duplikate')}</span>
</div>
)}
{/* Error */}
{cleanupError && (
<div style={{ padding: '1rem', background: '#fed7d7', borderRadius: '6px', color: '#c53030', marginBottom: '1rem' }}>
<FaExclamationTriangle style={{ marginRight: '0.5rem' }} />
{cleanupError}
</div>
)}
{/* Results */}
{cleanupResult && !cleanupLoading && (
<>
{/* Summary Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1.25rem' }}>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.totalRules}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Regeln total')}</div>
</div>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.uniqueSignatures}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Eindeutige Regeln')}</div>
</div>
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateGroups > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateGroups > 0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Duplikat-Gruppen')}</div>
</div>
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateRulesToDelete > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateRulesToDelete > 0 ? '#fc8181' : '#9ae6b4'}` }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateRulesToDelete > 0 ? '#c53030' : '#2f855a' }}>
{cleanupPhase === 'done' ? cleanupResult.deletedCount : cleanupResult.duplicateRulesToDelete}
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
{cleanupPhase === 'done' ? t('Gelöscht') : t('Zu löschen')}
</div>
</div>
</div>
{/* Status Message */}
{cleanupPhase === 'done' && (
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
<FaCheckCircle />
<span><strong>{cleanupResult.deletedCount}</strong> {t('Doppelte Regeln wurden erfolgreich entfernt')}</span>
</div>
)}
{cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && (
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
<FaCheckCircle />
<span>{t('Keine Duplikate gefunden, alles sauber')}</span>
</div>
)}
{/* Details Table */}
{cleanupResult.details && cleanupResult.details.length > 0 && (
<div style={{ marginTop: '0.5rem' }}>
<h4 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--text-secondary)' }}>
{t('Duplikat-Details')}{' '}
{cleanupResult.details.length < cleanupResult.duplicateGroups &&
`(${cleanupResult.details.length} ${t('von')} ${cleanupResult.duplicateGroups})`}
</h4>
<div style={{ maxHeight: '300px', overflowY: 'auto', border: '1px solid var(--border-color)', borderRadius: '6px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
<thead>
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Kontext')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Item')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Total')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)', color: '#c53030' }}>{t('Duplikate')}</th>
</tr>
</thead>
<tbody>
{cleanupResult.details.map((group, idx) => (
<tr key={idx} style={{ borderBottom: '1px solid var(--border-color)' }}>
<td style={{ padding: '0.375rem 0.75rem' }}>
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
{group.context}
</span>
</td>
<td style={{ padding: '0.375rem 0.75rem' }}>
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
{group.item}
</span>
</td>
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center' }}>{group.totalCount}</td>
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center', color: '#c53030', fontWeight: 600 }}>{group.deleteCount}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Template Role Assignments Section */}
{templateFixResult && templateFixResult.invalidAssignments > 0 && (
<div style={{ marginTop: '1.25rem', borderTop: '1px solid var(--border-color)', paddingTop: '1rem' }}>
<h4 style={{ fontSize: '0.9375rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text-primary)' }}>
{t('Template-Rollen-Zuweisungen')}
</h4>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1rem' }}>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{templateFixResult.totalUserMandateRoles}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Rollenzuweisungen total')}</div>
</div>
<div style={{ padding: '0.75rem', background: templateFixResult.invalidAssignments > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${templateFixResult.invalidAssignments > 0 ? '#fc8181' : '#9ae6b4'}` }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: templateFixResult.invalidAssignments > 0 ? '#c53030' : '#2f855a' }}>{templateFixResult.invalidAssignments}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Template statt Instanz')}</div>
</div>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: templateFixResult.fixedCount > 0 ? '#2f855a' : 'var(--text-primary)' }}>
{cleanupPhase === 'done' ? templateFixResult.fixedCount : templateFixResult.invalidAssignments}
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
{cleanupPhase === 'done' ? t('Repariert') : t('Zu reparieren')}
</div>
</div>
</div>
{templateFixResult.details && templateFixResult.details.length > 0 && (
<div style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid var(--border-color)', borderRadius: '6px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
<thead>
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Rolle')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Mandant')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Aktion')}</th>
</tr>
</thead>
<tbody>
{templateFixResult.details.map((detail, idx) => (
<tr key={idx} style={{ borderBottom: '1px solid var(--border-color)' }}>
<td style={{ padding: '0.375rem 0.75rem' }}>
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
{detail.templateRoleLabel}
</span>
</td>
<td style={{ padding: '0.375rem 0.75rem' }}>
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
{detail.mandateId.substring(0, 8)}...
</span>
</td>
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center' }}>
<span style={{
fontSize: '0.75rem',
padding: '0.125rem 0.5rem',
borderRadius: '10px',
background: detail.action.includes('replace') ? '#ebf8ff' : detail.action.includes('delete') ? '#fff5f5' : '#f7fafc',
color: detail.action.includes('replace') ? '#2b6cb0' : detail.action.includes('delete') ? '#c53030' : '#718096',
}}>
{detail.action}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{templateFixResult && templateFixResult.invalidAssignments === 0 && (
<div style={{ marginTop: '1rem', padding: '0.5rem 0.75rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', fontSize: '0.875rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
<FaCheckCircle />
<span>{t('Keine fehlerhaften Templaterollenzuweisungen')}</span>
</div>
)}
</>
)}
</div>
<div className={styles.modalFooter}>
<button className={styles.secondaryButton} onClick={_closeCleanupModal}>
{cleanupPhase === 'done' ? t('Schließen') : t('Abbrechen')}
</button>
{cleanupPhase === 'preview' && cleanupResult && (cleanupResult.duplicateRulesToDelete > 0 || (templateFixResult && templateFixResult.invalidAssignments > 0)) && (
<button
className={styles.dangerButton}
onClick={_executeCleanup}
disabled={cleanupLoading}
>
<FaBroom /> {t('Bereinigung ausführen')}
</button>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default AdminMandateRolePermissionsPage;