/** * AccessManagementHub * * Central admin page for feature instance access management. * Shows mandate/feature context, overview stats, instance cards, and relationship diagram. */ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { useFeatureAccess, type FeatureInstance, type Feature, type FeatureAccessUser, } from '../../hooks/useFeatureAccess'; import { useUserMandates, type Mandate } from '../../hooks/useUserMandates'; import { FaBuilding, FaCube, FaUsers, FaCogs, FaSync, FaChartBar, FaLink, FaList, FaSitemap } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; import styles from './Admin.module.css'; import hubStyles from './AccessManagementHub.module.css'; import { InstanceDetailModal } from './InstanceDetailModal'; import { FeatureInstanceWizard } from './wizards/FeatureInstanceWizard'; import { InstanceHierarchyView } from './InstanceHierarchyView'; import { useLanguage } from '../../providers/language/LanguageContext'; import { labelAsI18nKey } from '../../types/mandate'; function getMandateName(mandate: Mandate): string { if (mandate.label) return mandate.label; if (typeof mandate.name === 'object') { return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id; } return mandate.name || mandate.id; } function getFeatureLabel(feature: Feature, t: (k: string) => string): string { return t(labelAsI18nKey(feature.label, feature.code)); } export interface InstanceWithStats extends FeatureInstance { userCount?: number; roleCount?: number; } export const AccessManagementHub: React.FC = () => { const { t } = useLanguage(); const { features, instances, loading, error, fetchFeatures, fetchInstances, syncInstanceRoles, } = useFeatureAccess(); const { fetchMandates } = useUserMandates(); const { showSuccess, showError } = useToast(); const [mandates, setMandates] = useState([]); const [selectedMandateId, setSelectedMandateId] = useState(''); const [selectedFeatureCode, setSelectedFeatureCode] = useState(''); const [instancesWithStats, setInstancesWithStats] = useState([]); const [statsLoading, setStatsLoading] = useState(false); const [detailInstance, setDetailInstance] = useState<{ instance: FeatureInstance; mandateId: string; mandateName: string; featureLabel: string; } | null>(null); const [showWizard, setShowWizard] = useState(false); type ViewMode = 'list' | 'hierarchy'; const [viewMode, setViewMode] = useState('hierarchy'); const [instanceUsersMap, setInstanceUsersMap] = useState>({}); const [hierarchyUsersLoading, setHierarchyUsersLoading] = useState(false); const [instancesByMandate, setInstancesByMandate] = useState>({}); useEffect(() => { fetchFeatures(); fetchMandates().then(data => { setMandates(data); if (data.length > 0 && !selectedMandateId) { setSelectedMandateId(data[0].id); } }); }, [fetchFeatures, fetchMandates]); useEffect(() => { if (selectedMandateId) { fetchInstances(selectedMandateId, selectedFeatureCode || undefined); } else { setInstancesWithStats([]); } }, [selectedMandateId, selectedFeatureCode, fetchInstances]); const loadInstanceStats = useCallback(async (mandateId: string, instanceList: FeatureInstance[]) => { if (instanceList.length === 0) { setInstancesWithStats([]); return; } setStatsLoading(true); try { const results = await Promise.all( instanceList.map(async (inst) => { try { const [usersRes, rolesRes] = await Promise.all([ api.get(`/api/features/instances/${inst.id}/users`, { headers: { 'X-Mandate-Id': mandateId }, }), api.get(`/api/features/instances/${inst.id}/available-roles`, { headers: { 'X-Mandate-Id': mandateId }, }), ]); const users = Array.isArray(usersRes.data) ? usersRes.data : usersRes.data?.items || []; const roles = Array.isArray(rolesRes.data) ? rolesRes.data : []; return { ...inst, userCount: users.length, roleCount: roles.length, }; } catch { return { ...inst, userCount: 0, roleCount: 0 }; } }) ); setInstancesWithStats(results); } catch { setInstancesWithStats(instanceList.map((i) => ({ ...i, userCount: 0, roleCount: 0 }))); } finally { setStatsLoading(false); } }, []); useEffect(() => { if (selectedMandateId && instances.length > 0 && !loading) { loadInstanceStats(selectedMandateId, instances); } else if (instances.length === 0) { setInstancesWithStats([]); } }, [selectedMandateId, instances, loading, loadInstanceStats]); const handleSyncRoles = async (instance: FeatureInstance) => { if (!selectedMandateId) return; try { const result = await syncInstanceRoles(selectedMandateId, instance.id, true); if (result.success && result.data) { showSuccess( 'Rollen synchronisiert', `Hinzugefügt: ${result.data.added}, Entfernt: ${result.data.removed}, Unverändert: ${result.data.unchanged}` ); fetchInstances(selectedMandateId, selectedFeatureCode || undefined); } else { showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren'); } } catch { showError('Fehler', 'Rollen konnten nicht synchronisiert werden'); } }; const handleOpenDetail = (instance: FeatureInstance, mandateIdOverride?: string) => { const mandateId = mandateIdOverride ?? selectedMandateId; const mandate = mandates.find((m) => m.id === mandateId); const feature = features.find((f) => f.code === instance.featureCode); setDetailInstance({ instance, mandateId: mandateId || '', mandateName: mandate ? getMandateName(mandate) : mandateId || '', featureLabel: feature ? getFeatureLabel(feature, t) : instance.featureCode, }); }; const handleCloseDetail = () => { setDetailInstance(null); }; const handleDetailSaved = () => { if (selectedMandateId) { fetchInstances(selectedMandateId, selectedFeatureCode || undefined); } setDetailInstance(null); }; const handleWizardComplete = () => { setShowWizard(false); if (selectedMandateId) { fetchInstances(selectedMandateId, selectedFeatureCode || undefined); } }; const filteredInstances = useMemo(() => { if (!selectedFeatureCode) return instancesWithStats; return instancesWithStats.filter((i) => i.featureCode === selectedFeatureCode); }, [instancesWithStats, selectedFeatureCode]); const loadAllHierarchyData = useCallback(async () => { if (mandates.length === 0) { setInstancesByMandate({}); setInstanceUsersMap({}); return; } setHierarchyUsersLoading(true); try { const mandateIds = mandates.map((m) => m.id); const instancesResults = await Promise.all( mandateIds.map(async (mandateId) => { try { const res = await api.get('/api/features/instances', { headers: { 'X-Mandate-Id': mandateId }, }); const list = res.data?.items ?? (Array.isArray(res.data) ? res.data : []); return { mandateId, instances: list as FeatureInstance[] }; } catch { return { mandateId, instances: [] as FeatureInstance[] }; } }) ); const byMandate: Record = {}; const allInstanceIds: { instanceId: string; mandateId: string }[] = []; for (const { mandateId, instances } of instancesResults) { const withStats = await Promise.all( instances.map(async (inst) => { try { const [usersRes, rolesRes] = await Promise.all([ api.get(`/api/features/instances/${inst.id}/users`, { headers: { 'X-Mandate-Id': mandateId }, }), api.get(`/api/features/instances/${inst.id}/available-roles`, { headers: { 'X-Mandate-Id': mandateId }, }), ]); const users = Array.isArray(usersRes.data) ? usersRes.data : usersRes.data?.items ?? []; const roles = Array.isArray(rolesRes.data) ? rolesRes.data : []; allInstanceIds.push({ instanceId: inst.id, mandateId }); return { ...inst, userCount: users.length, roleCount: roles.length, } as InstanceWithStats; } catch { allInstanceIds.push({ instanceId: inst.id, mandateId }); return { ...inst, userCount: 0, roleCount: 0 } as InstanceWithStats; } }) ); byMandate[mandateId] = withStats; } setInstancesByMandate(byMandate); const usersMap: Record = {}; await Promise.all( allInstanceIds.map(async ({ instanceId, mandateId }) => { try { const res = await api.get(`/api/features/instances/${instanceId}/users`, { headers: { 'X-Mandate-Id': mandateId }, }); const users = Array.isArray(res.data) ? res.data : res.data?.items ?? []; usersMap[instanceId] = users as FeatureAccessUser[]; } catch { usersMap[instanceId] = []; } }) ); setInstanceUsersMap(usersMap); } finally { setHierarchyUsersLoading(false); } }, [mandates]); useEffect(() => { if (viewMode === 'hierarchy' && mandates.length > 0) { loadAllHierarchyData(); } else if (viewMode !== 'hierarchy') { setInstancesByMandate({}); setInstanceUsersMap({}); } }, [viewMode, mandates, loadAllHierarchyData]); const overviewStats = useMemo(() => { const totalUsers = filteredInstances.reduce((sum, i) => sum + (i.userCount ?? 0), 0); const maxRoles = Math.max(0, ...filteredInstances.map((i) => i.roleCount ?? 0)); return { instances: filteredInstances.length, users: totalUsers, roles: maxRoles, }; }, [filteredInstances]); const relationshipData = useMemo(() => { if (!selectedMandateId || filteredInstances.length === 0) return null; const mandate = mandates.find((m) => m.id === selectedMandateId); return { mandateName: mandate ? getMandateName(mandate) : selectedMandateId, instances: filteredInstances.map((inst) => { const feature = features.find((f) => f.code === inst.featureCode); return { id: inst.id, label: inst.label, featureLabel: feature ? getFeatureLabel(feature) : inst.featureCode, userCount: inst.userCount ?? 0, }; }), }; }, [selectedMandateId, mandates, filteredInstances, features, t]); if (error && !selectedMandateId) { return (
⚠️

Fehler: {error}

); } return (

Zugriffsverwaltung

Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten

{/* Filter dropdowns only shown in list view - hierarchy shows everything */} {viewMode === 'list' && ( <>
{selectedMandateId && (
)} )}
Mandanten verwalten Mandant-Benutzer
{viewMode === 'hierarchy' ? ( ) : !selectedMandateId ? (

{t('accessManagementHub.keinMandantAusgewaehlt')}

Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten.

) : ( <>
{loading || statsLoading ? '…' : overviewStats.instances} Instanzen
{loading || statsLoading ? '…' : overviewStats.users} {t('accessManagementHub.benutzer')}
{loading || statsLoading ? '…' : overviewStats.roles} {t('accessManagementHub.rollenMax')}
{relationshipData && relationshipData.instances.length > 0 && (
Beziehungen
{relationshipData.mandateName}
{relationshipData.instances.slice(0, 5).map((inst) => (
{inst.label} ({inst.userCount})
))} {relationshipData.instances.length > 5 && (
+{relationshipData.instances.length - 5} weitere
)}
)}

Feature-Instanzen

{loading && filteredInstances.length === 0 ? (
{t('accessManagementHub.ladeInstanzen')}
) : filteredInstances.length === 0 ? (

{t('accessManagementHub.keineFeatureinstanzen')}

Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature.

) : (
{filteredInstances.map((inst) => (
{inst.label} {inst.enabled ? t('accessManagementHub.aktiv') : t('accessManagementHub.inaktiv')}
{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)} {inst.userCount ?? '—'} Benutzer {inst.roleCount ?? '—'} Rollen
))}
)}
)} {detailInstance && ( )} {showWizard && ( setShowWizard(false)} onComplete={handleWizardComplete} /> )}
); }; export default AccessManagementHub;