602 lines
28 KiB
TypeScript
602 lines
28 KiB
TypeScript
/**
|
||
* 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;
|