720 lines
28 KiB
TypeScript
720 lines
28 KiB
TypeScript
/**
|
||
* AdminUserAccessOverviewPage
|
||
*
|
||
* Admin page for viewing comprehensive user access permissions.
|
||
* Shows what pages a user can see and what data they can access.
|
||
*/
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { FaSync, FaUserShield, FaEye, FaDatabase, FaCube, FaChevronDown, FaChevronRight, FaCheckCircle, FaTimesCircle, FaInfoCircle } from 'react-icons/fa';
|
||
import api from '../../api';
|
||
import styles from './Admin.module.css';
|
||
|
||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||
|
||
interface UserOption {
|
||
id: string;
|
||
username: string;
|
||
email: string;
|
||
fullName: string;
|
||
isSysAdmin: boolean;
|
||
enabled: boolean;
|
||
}
|
||
|
||
interface RoleInfo {
|
||
id: string;
|
||
roleLabel: string;
|
||
description: { [key: string]: string };
|
||
scope: 'global' | 'mandate' | 'instance';
|
||
mandateId?: string;
|
||
featureInstanceId?: string;
|
||
source: string;
|
||
sourceMandateId?: string;
|
||
sourceMandateName?: string;
|
||
sourceInstanceId?: string;
|
||
sourceInstanceLabel?: string;
|
||
}
|
||
|
||
interface AccessEntry {
|
||
item: string;
|
||
view?: boolean;
|
||
read?: string;
|
||
create?: string;
|
||
update?: string;
|
||
delete?: string;
|
||
grantedByRoleLabels: string[];
|
||
roleScope: string;
|
||
}
|
||
|
||
interface MandateInfo {
|
||
id: string;
|
||
name: string;
|
||
label?: string | null;
|
||
roleIds: string[];
|
||
featureInstances: {
|
||
id: string;
|
||
label: string;
|
||
featureCode: string;
|
||
featureLabel: { [key: string]: string };
|
||
roleIds: string[];
|
||
}[];
|
||
}
|
||
|
||
function _mandateNameLine(mandate: MandateInfo): string {
|
||
const label = mandate.label?.trim();
|
||
if (label) {
|
||
return `${mandate.name} (${label})`;
|
||
}
|
||
return mandate.name;
|
||
}
|
||
|
||
function _roleDescriptionLine(role: RoleInfo): string {
|
||
return role.description?.de || role.description?.en || '';
|
||
}
|
||
|
||
interface UserAccessOverview {
|
||
user: UserOption;
|
||
isSysAdmin: boolean;
|
||
sysAdminNote?: string;
|
||
roles: RoleInfo[];
|
||
mandates: MandateInfo[];
|
||
uiAccess: AccessEntry[];
|
||
dataAccess: AccessEntry[];
|
||
resourceAccess: AccessEntry[];
|
||
}
|
||
|
||
type TabId = 'overview' | 'ui' | 'data' | 'resources';
|
||
|
||
export const AdminUserAccessOverviewPage: React.FC = () => {
|
||
const { t } = useLanguage();
|
||
|
||
const [users, setUsers] = useState<UserOption[]>([]);
|
||
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
||
const [overview, setOverview] = useState<UserAccessOverview | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [loadingUsers, setLoadingUsers] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [activeTab, setActiveTab] = useState<TabId>('overview');
|
||
const [expandedRoles, setExpandedRoles] = useState<Set<string>>(new Set());
|
||
const [expandedMandates, setExpandedMandates] = useState<Set<string>>(new Set());
|
||
|
||
// Fetch users list on mount
|
||
useEffect(() => {
|
||
const fetchUsers = async () => {
|
||
try {
|
||
setLoadingUsers(true);
|
||
const response = await api.get('/api/admin/user-access-overview/users');
|
||
setUsers(response.data);
|
||
} catch (err: any) {
|
||
setError(err?.response?.data?.detail || err?.message || 'Failed to fetch users');
|
||
} finally {
|
||
setLoadingUsers(false);
|
||
}
|
||
};
|
||
|
||
fetchUsers();
|
||
}, []);
|
||
|
||
// Fetch access overview when user is selected
|
||
useEffect(() => {
|
||
if (!selectedUserId) {
|
||
setOverview(null);
|
||
return;
|
||
}
|
||
|
||
const fetchOverview = async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
const response = await api.get(`/api/admin/user-access-overview/${selectedUserId}`);
|
||
const data = response.data;
|
||
setOverview(data);
|
||
|
||
// Auto-expand all mandates
|
||
setExpandedMandates(new Set(data.mandates?.map((m: MandateInfo) => m.id) || []));
|
||
} catch (err: any) {
|
||
setError(err?.response?.data?.detail || err?.message || 'Failed to fetch overview');
|
||
setOverview(null);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchOverview();
|
||
}, [selectedUserId]);
|
||
|
||
const toggleRole = (roleId: string) => {
|
||
setExpandedRoles(prev => {
|
||
const newSet = new Set(prev);
|
||
if (newSet.has(roleId)) {
|
||
newSet.delete(roleId);
|
||
} else {
|
||
newSet.add(roleId);
|
||
}
|
||
return newSet;
|
||
});
|
||
};
|
||
|
||
const toggleMandate = (mandateId: string) => {
|
||
setExpandedMandates(prev => {
|
||
const newSet = new Set(prev);
|
||
if (newSet.has(mandateId)) {
|
||
newSet.delete(mandateId);
|
||
} else {
|
||
newSet.add(mandateId);
|
||
}
|
||
return newSet;
|
||
});
|
||
};
|
||
|
||
const getScopeColor = (scope: string): string => {
|
||
switch (scope) {
|
||
case 'instance': return '#10b981';
|
||
case 'mandate': return '#3b82f6';
|
||
case 'global': return '#8b5cf6';
|
||
default: return '#6b7280';
|
||
}
|
||
};
|
||
|
||
const getAccessLevelColor = (level: string): string => {
|
||
switch (level) {
|
||
case 'ALL': return '#10b981';
|
||
case 'GROUP': return '#3b82f6';
|
||
case 'MY': return '#f59e0b';
|
||
case 'NONE': return '#ef4444';
|
||
default: return '#6b7280';
|
||
}
|
||
};
|
||
|
||
const renderOverviewTab = () => {
|
||
if (!overview) return null;
|
||
|
||
const roleById = new Map(overview.roles.map((r) => [r.id, r]));
|
||
const globalRoles = overview.roles.filter((r) => r.scope === 'global');
|
||
|
||
const _resolveRoles = (roleIds: string[]): RoleInfo[] =>
|
||
roleIds.map((id) => roleById.get(id)).filter((r): r is RoleInfo => !!r);
|
||
|
||
return (
|
||
<div className={styles.scrollableContent}>
|
||
{overview.isSysAdmin && (
|
||
<div className={styles.infoBox} style={{ background: '#fef3c7', borderColor: '#f59e0b' }}>
|
||
<FaInfoCircle style={{ marginRight: '0.5rem', color: '#f59e0b' }} />
|
||
<span>{overview.sysAdminNote || 'Dieser Benutzer ist SysAdmin und hat vollen Systemzugriff.'}</span>
|
||
</div>
|
||
)}
|
||
|
||
<h3 style={{ marginBottom: '1rem', color: 'var(--text-primary)' }}>{t('adminUserAccessOverview.zugriffNachMandant')}</h3>
|
||
|
||
{overview.mandates.length === 0 ? (
|
||
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineMandatezuordnungenVorhanden')}</p>
|
||
) : (
|
||
<div className={styles.rolesList} style={{ flex: 'none', overflow: 'visible' }}>
|
||
{overview.mandates.map((mandate) => {
|
||
const mandateRoles = _resolveRoles(mandate.roleIds);
|
||
|
||
return (
|
||
<div key={mandate.id} className={styles.roleCard}>
|
||
<div className={styles.roleHeader} onClick={() => toggleMandate(mandate.id)}>
|
||
<div className={styles.roleInfo} style={{ flexWrap: 'wrap', rowGap: '0.35rem' }}>
|
||
{expandedMandates.has(mandate.id) ? (
|
||
<FaChevronDown className={styles.expandIcon} />
|
||
) : (
|
||
<FaChevronRight className={styles.expandIcon} />
|
||
)}
|
||
<span className={styles.roleLabel}>{_mandateNameLine(mandate)}</span>
|
||
<span className={styles.roleDescription}>
|
||
{mandateRoles.length} Mandantenrolle(n) · {mandate.featureInstances.length} Feature-Instanz(en)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{expandedMandates.has(mandate.id) && (
|
||
<div className={styles.roleContent}>
|
||
{mandateRoles.length === 0 ? (
|
||
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineRollenDirektAmMandanten')}</p>
|
||
) : (
|
||
<ul className={styles.accessOverviewRoleBullets}>
|
||
{mandateRoles.map((r) => (
|
||
<li key={r.id}>
|
||
<strong>{r.roleLabel}</strong>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getScopeColor(r.scope),
|
||
color: 'white',
|
||
marginLeft: '0.35rem',
|
||
fontSize: '0.65rem',
|
||
verticalAlign: 'middle',
|
||
}}
|
||
>
|
||
{r.scope}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
|
||
<div className={styles.accessOverviewSubheading}>Feature-Instanzen</div>
|
||
{mandate.featureInstances.length === 0 ? (
|
||
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineFeatureinstanzenZugewiesen')}</p>
|
||
) : (
|
||
<div className={styles.accessOverviewInstanceStack}>
|
||
{mandate.featureInstances.map((instance) => {
|
||
const instanceRoles = _resolveRoles(instance.roleIds);
|
||
const featureTitle = instance.featureLabel?.de || instance.featureCode;
|
||
return (
|
||
<div key={instance.id} className={styles.accessOverviewInstanceBlock}>
|
||
<div className={styles.accessOverviewInstanceTitle}>
|
||
{instance.label}{' '}
|
||
<span className={styles.accessOverviewInstanceFeature}>({featureTitle})</span>
|
||
</div>
|
||
{instanceRoles.length === 0 ? (
|
||
<p className={styles.emptyHint} style={{ margin: '0.35rem 0 0' }}>
|
||
Keine Rollen.
|
||
</p>
|
||
) : (
|
||
<ul className={styles.accessOverviewRoleBullets}>
|
||
{instanceRoles.map((r) => (
|
||
<li key={r.id}>
|
||
<strong>{r.roleLabel}</strong>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getScopeColor(r.scope),
|
||
color: 'white',
|
||
marginLeft: '0.35rem',
|
||
fontSize: '0.65rem',
|
||
verticalAlign: 'middle',
|
||
}}
|
||
>
|
||
{r.scope}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{globalRoles.length > 0 && (
|
||
<>
|
||
<h3 style={{ marginTop: '2rem', marginBottom: '1rem', color: 'var(--text-primary)' }}>
|
||
Globale Rollen
|
||
</h3>
|
||
<p className={styles.emptyHint} style={{ marginTop: '-0.5rem', marginBottom: '1rem' }}>
|
||
Nicht an einen Mandanten gebunden.
|
||
</p>
|
||
<div className={styles.rolesList} style={{ flex: 'none', overflow: 'visible' }}>
|
||
{globalRoles.map((role) => (
|
||
<div key={role.id} className={styles.roleCard}>
|
||
<div className={styles.roleHeader} onClick={() => toggleRole(role.id)}>
|
||
<div className={styles.roleInfo}>
|
||
{expandedRoles.has(role.id) ? (
|
||
<FaChevronDown className={styles.expandIcon} />
|
||
) : (
|
||
<FaChevronRight className={styles.expandIcon} />
|
||
)}
|
||
<span className={styles.roleLabel}>{role.roleLabel}</span>
|
||
<span
|
||
className={styles.badge}
|
||
style={{ background: getScopeColor(role.scope), color: 'white', marginLeft: '0.5rem' }}
|
||
>
|
||
{role.scope}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{expandedRoles.has(role.id) && (
|
||
<div className={styles.roleContent}>
|
||
<div style={{ fontSize: '0.875rem' }}>
|
||
<p>
|
||
<strong>{t('adminUserAccessOverview.beschreibung')}</strong> {_roleDescriptionLine(role) || '—'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const renderUiAccessTab = () => {
|
||
if (!overview) return null;
|
||
|
||
return (
|
||
<div className={styles.scrollableContent}>
|
||
<div className={styles.infoBox}>
|
||
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
|
||
<span>{t('adminUserAccessOverview.uizugriffsrechteBestimmenWelcheSeitenUnd')}</span>
|
||
</div>
|
||
|
||
{overview.uiAccess.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaEye className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineUiberechtigungen')}</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Diesem Benutzer wurden keine expliziten UI-Berechtigungen zugewiesen.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>UI-Element</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '80px' }}>Sichtbar</th>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{overview.uiAccess.map((entry, idx) => (
|
||
<tr
|
||
key={idx}
|
||
style={{
|
||
borderBottom: '1px solid var(--border-color)',
|
||
background: idx % 2 === 0 ? 'transparent' : 'var(--bg-secondary)'
|
||
}}
|
||
>
|
||
<td style={{ padding: '0.75rem', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||
{entry.item}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
{entry.view ? (
|
||
<FaCheckCircle style={{ color: '#10b981' }} />
|
||
) : (
|
||
<FaTimesCircle style={{ color: '#ef4444' }} />
|
||
)}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
|
||
{entry.grantedByRoleLabels?.join(', ') || '-'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const renderDataAccessTab = () => {
|
||
if (!overview) return null;
|
||
|
||
return (
|
||
<div className={styles.scrollableContent}>
|
||
<div className={styles.infoBox}>
|
||
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
|
||
<span>
|
||
Daten-Zugriffsrechte: <strong>ALL</strong> = Alle Datensätze, <strong>GROUP</strong> = Gruppen-Datensätze,
|
||
<strong> MY</strong> = Eigene Datensätze, <strong>NONE</strong> = Kein Zugriff
|
||
</span>
|
||
</div>
|
||
|
||
{overview.dataAccess.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaDatabase className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineDatenberechtigungen')}</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Diesem Benutzer wurden keine expliziten Daten-Berechtigungen zugewiesen.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Tabelle/Feld</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Lesen</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>{t('adminUserAccessOverview.erstellen')}</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Update</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>{t('adminUserAccessOverview.loeschen')}</th>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{overview.dataAccess.map((entry, idx) => (
|
||
<tr
|
||
key={idx}
|
||
style={{
|
||
borderBottom: '1px solid var(--border-color)',
|
||
background: idx % 2 === 0 ? 'transparent' : 'var(--bg-secondary)'
|
||
}}
|
||
>
|
||
<td style={{ padding: '0.75rem', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||
{entry.item}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getAccessLevelColor(entry.read || '-'),
|
||
color: 'white',
|
||
fontSize: '0.7rem'
|
||
}}
|
||
>
|
||
{entry.read || '-'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getAccessLevelColor(entry.create || '-'),
|
||
color: 'white',
|
||
fontSize: '0.7rem'
|
||
}}
|
||
>
|
||
{entry.create || '-'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getAccessLevelColor(entry.update || '-'),
|
||
color: 'white',
|
||
fontSize: '0.7rem'
|
||
}}
|
||
>
|
||
{entry.update || '-'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getAccessLevelColor(entry.delete || '-'),
|
||
color: 'white',
|
||
fontSize: '0.7rem'
|
||
}}
|
||
>
|
||
{entry.delete || '-'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
|
||
{entry.grantedByRoleLabels?.join(', ') || '-'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const renderResourceAccessTab = () => {
|
||
if (!overview) return null;
|
||
|
||
return (
|
||
<div className={styles.scrollableContent}>
|
||
<div className={styles.infoBox}>
|
||
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
|
||
<span>{t('adminUserAccessOverview.ressourcenzugriffsrechteBestimmenWelcheSystemressourcenZb')}</span>
|
||
</div>
|
||
|
||
{overview.resourceAccess.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaCube className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineRessourcenberechtigungen')}</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Diesem Benutzer wurden keine expliziten Ressourcen-Berechtigungen zugewiesen.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Ressource</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '80px' }}>Zugriff</th>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{overview.resourceAccess.map((entry, idx) => (
|
||
<tr
|
||
key={idx}
|
||
style={{
|
||
borderBottom: '1px solid var(--border-color)',
|
||
background: idx % 2 === 0 ? 'transparent' : 'var(--bg-secondary)'
|
||
}}
|
||
>
|
||
<td style={{ padding: '0.75rem', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||
{entry.item}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
{entry.view ? (
|
||
<FaCheckCircle style={{ color: '#10b981' }} />
|
||
) : (
|
||
<FaTimesCircle style={{ color: '#ef4444' }} />
|
||
)}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
|
||
{entry.grantedByRoleLabels?.join(', ') || '-'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
if (error && !overview) {
|
||
return (
|
||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||
<div className={styles.errorContainer}>
|
||
<span className={styles.errorIcon}>⚠️</span>
|
||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => window.location.reload()}
|
||
>
|
||
<FaSync /> Erneut versuchen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||
<div className={styles.pageHeader}>
|
||
<div>
|
||
<h1 className={styles.pageTitle}>{t('adminUserAccessOverview.benutzerzugriffsuebersicht')}</h1>
|
||
<p className={styles.pageSubtitle}>{t('adminUserAccessOverview.zeigtAlleBerechtigungenEinesBenutzers')}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* User Selection */}
|
||
<div className={styles.filterSection}>
|
||
<div className={styles.filterGroup}>
|
||
<label className={styles.filterLabel}>
|
||
<FaUserShield style={{ marginRight: '0.5rem' }} />
|
||
Benutzer auswählen:
|
||
</label>
|
||
<select
|
||
className={styles.filterSelect}
|
||
value={selectedUserId}
|
||
onChange={(e) => setSelectedUserId(e.target.value)}
|
||
disabled={loadingUsers}
|
||
style={{ minWidth: '300px' }}
|
||
>
|
||
<option value="">{t('adminUserAccessOverview.benutzerWaehlen')}</option>
|
||
{users.map(user => (
|
||
<option key={user.id} value={user.id}>
|
||
{user.fullName || user.username} ({user.email})
|
||
{user.isSysAdmin && ' [SysAdmin]'}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{selectedUserId && (
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => setSelectedUserId(selectedUserId)}
|
||
disabled={loading}
|
||
>
|
||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Content */}
|
||
{!selectedUserId ? (
|
||
<div className={styles.emptyState}>
|
||
<FaUserShield className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.benutzerAuswaehlen')}</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Wählen Sie einen Benutzer aus, um dessen Zugriffsberechtigungen anzuzeigen.
|
||
</p>
|
||
</div>
|
||
) : loading ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>{t('adminUserAccessOverview.ladeZugriffsuebersicht')}</span>
|
||
</div>
|
||
) : overview ? (
|
||
<>
|
||
{/* User Info */}
|
||
<div className={styles.infoBox} style={{ marginBottom: '1rem', flexShrink: 0 }}>
|
||
<strong>{overview.user.fullName || overview.user.username}</strong>
|
||
<span style={{ margin: '0 1rem', color: 'var(--text-secondary)' }}>|</span>
|
||
<span>{overview.user.email}</span>
|
||
{overview.isSysAdmin && (
|
||
<>
|
||
<span style={{ margin: '0 1rem', color: 'var(--text-secondary)' }}>|</span>
|
||
<span className={styles.badge} style={{ background: '#f59e0b', color: 'white' }}>
|
||
SysAdmin
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div style={{
|
||
display: 'flex',
|
||
gap: '0.5rem',
|
||
marginBottom: '1rem',
|
||
borderBottom: '1px solid var(--border-color)',
|
||
paddingBottom: '0.5rem',
|
||
flexShrink: 0
|
||
}}>
|
||
<button
|
||
className={activeTab === 'overview' ? styles.primaryButton : styles.secondaryButton}
|
||
onClick={() => setActiveTab('overview')}
|
||
style={{ padding: '0.5rem 1rem' }}
|
||
>
|
||
<FaUserShield /> Übersicht
|
||
</button>
|
||
<button
|
||
className={activeTab === 'ui' ? styles.primaryButton : styles.secondaryButton}
|
||
onClick={() => setActiveTab('ui')}
|
||
style={{ padding: '0.5rem 1rem' }}
|
||
>
|
||
<FaEye /> UI-Zugriff ({overview.uiAccess.length})
|
||
</button>
|
||
<button
|
||
className={activeTab === 'data' ? styles.primaryButton : styles.secondaryButton}
|
||
onClick={() => setActiveTab('data')}
|
||
style={{ padding: '0.5rem 1rem' }}
|
||
>
|
||
<FaDatabase /> Daten-Zugriff ({overview.dataAccess.length})
|
||
</button>
|
||
<button
|
||
className={activeTab === 'resources' ? styles.primaryButton : styles.secondaryButton}
|
||
onClick={() => setActiveTab('resources')}
|
||
style={{ padding: '0.5rem 1rem' }}
|
||
>
|
||
<FaCube /> Ressourcen ({overview.resourceAccess.length})
|
||
</button>
|
||
</div>
|
||
|
||
{/* Tab Content */}
|
||
<div className={styles.tableContainer}>
|
||
{activeTab === 'overview' && renderOverviewTab()}
|
||
{activeTab === 'ui' && renderUiAccessTab()}
|
||
{activeTab === 'data' && renderDataAccessTab()}
|
||
{activeTab === 'resources' && renderResourceAccessTab()}
|
||
</div>
|
||
</>
|
||
) : null}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AdminUserAccessOverviewPage;
|