diff --git a/src/pages/admin/AdminMandateRolePermissionsPage.tsx b/src/pages/admin/AdminMandateRolePermissionsPage.tsx
index ec94651..619e39c 100644
--- a/src/pages/admin/AdminMandateRolePermissionsPage.tsx
+++ b/src/pages/admin/AdminMandateRolePermissionsPage.tsx
@@ -10,12 +10,15 @@
* - 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,
@@ -24,10 +27,35 @@ import {
FaChevronRight,
FaGlobe,
FaBuilding,
- FaFilter
+ 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,
@@ -44,6 +72,13 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('all');
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 () => {
@@ -105,6 +140,49 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
);
};
+ // --- 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: 'all', label: 'Alle Rollen' },
@@ -140,6 +218,14 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
+
)}
+
+ {/* 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})`}
+
+
+
+
+
+ | Kontext |
+ Item |
+ Total |
+ Duplikate |
+
+
+
+ {cleanupResult.details.map((group, idx) => (
+
+ |
+
+ {group.context}
+
+ |
+
+
+ {group.item}
+
+ |
+ {group.totalCount} |
+ {group.deleteCount} |
+
+ ))}
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+
+ {cleanupPhase === 'preview' && cleanupResult && cleanupResult.duplicateRulesToDelete > 0 && (
+
+ )}
+
+
+
+ )}
);
};