import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { toast } from 'react-toastify'; const API = process.env.REACT_APP_API_URL || '/api'; const authFetch = (url, opts = {}) => { const token = localStorage.getItem('token'); return fetch(url, { ...opts, headers: { ...(opts.headers || {}), Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } }); }; /* ── Helpers ─────────────────────────────────────────────────────── */ const fmtDate = (d) => d ? new Date(d).toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' }) : '—'; const fmtTime = (d) => { if (!d) return '—'; const now = Date.now(), dt = new Date(d).getTime(), diff = Math.floor((now - dt) / 60000); if (diff < 1) return 'Gerade eben'; if (diff < 60) return `vor ${diff} Min.`; const h = Math.floor(diff / 60); if (h < 24) return `vor ${h} Std.`; const days = Math.floor(h / 24); if (days < 30) return `vor ${days} T.`; return new Date(d).toLocaleDateString('de-DE'); }; const fmtEuro = (n) => n ? new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n) : '—'; const daysBetween = (from, to = new Date()) => Math.floor((new Date(to) - new Date(from)) / 86400000); const ROLE_COLORS = { super_admin: { bg: 'rgba(239,68,68,0.12)', color: '#fca5a5', border: 'rgba(239,68,68,0.25)', label: 'Super Admin' }, admin: { bg: 'rgba(239,68,68,0.12)', color: '#fca5a5', border: 'rgba(239,68,68,0.25)', label: 'Admin' }, support: { bg: 'rgba(52,211,153,0.1)', color: '#6ee7b7', border: 'rgba(52,211,153,0.25)', label: 'Support' }, bearbeiter: { bg: 'rgba(52,211,153,0.1)', color: '#6ee7b7', border: 'rgba(52,211,153,0.25)', label: 'Bearbeiter' }, hr_personal: { bg: 'rgba(96,165,250,0.1)', color: '#93c5fd', border: 'rgba(96,165,250,0.25)', label: 'HR / Personal' }, buchhaltung: { bg: 'rgba(96,165,250,0.1)', color: '#93c5fd', border: 'rgba(96,165,250,0.25)', label: 'Buchhaltung' }, produktion: { bg: 'rgba(245,158,11,0.1)', color: '#fcd34d', border: 'rgba(245,158,11,0.25)', label: 'Produktion' }, benutzer: { bg: 'rgba(148,163,184,0.1)', color: '#94a3b8', border: 'rgba(148,163,184,0.2)', label: 'Benutzer' }, }; const ROLE_AV_COLORS = { super_admin: '#b91c1c', admin: '#dc2626', support: '#059669', bearbeiter: '#10b981', hr_personal: '#2563eb', buchhaltung: '#3b82f6', produktion: '#d97706', benutzer: '#475569', }; const IS_ADMIN_ROLE = (r) => ['super_admin', 'admin'].includes(r); const ROLE_FILTER_MAP = { admin: ['super_admin', 'admin'], staff: ['support', 'bearbeiter'], hr: ['hr_personal', 'buchhaltung'], production: ['produktion'], user: ['benutzer'], }; const LICENSE_CATEGORIES = { 'SPE:ENTERPRISEPREMIUM': 'Produktivität', 'SPE:ENTERPRISEPACK': 'Produktivität', 'OFFICESUBSCRIPTION': 'Produktivität', 'MCOEV': 'Produktivität', 'FLOW_FREE': 'Produktivität', 'POWERBI_PRO': 'Analytics', 'POWER_BI_STANDARD': 'Analytics', 'INTUNE_A': 'Security', 'AAD_PREMIUM': 'Security', 'AAD_PREMIUM_P2': 'Security', 'EMS': 'Security', 'EMSPREMIUM': 'Security', 'AX7': 'Entwicklung', 'VISIOCLIENT': 'Produktivität', 'PROJECTCLIENT': 'Produktivität', }; const getLicCategory = (skuPartNumber) => { if (!skuPartNumber) return 'Sonstiges'; for (const [k, v] of Object.entries(LICENSE_CATEGORIES)) { if (skuPartNumber.toUpperCase().includes(k.split(':').pop())) return v; } return 'Sonstiges'; }; /* ── SVG Icons ───────────────────────────────────────────────────── */ const Ico = ({ d, children, size = 16, ...p }) => ( {d ? : children} ); const CheckIcon = () => ; const UserIcon = () => ; const BriefIcon = () => ; const PhoneIcon = () => ; const MapIcon = () => ; const UsersIcon = () => ; const CalIcon = () => ; const ShieldIcon = () => ; const KeyIcon = () => ; const LockIcon = () => ; const MonitorIcon = () => ; const ClockIcon = () => ; const SearchIcon = () => ; const EditIcon = () => ; const MoreIcon = () => ; const DownloadIcon = () => ; const PlusIcon = () => ; const CopyIcon = () => ; const LogoutIcon = () => ; const MailIcon = () => ; const LaptopIcon = () => ; const RefreshIcon = () => ; const CloudIcon = () => ; const ServerIcon = () => ; const XIcon = () => ; const ChevronIcon = ({ open }) => ; const BarIcon = () => ; /* ── Avatar ──────────────────────────────────────────────────────── */ const Avatar = ({ user, size = 36, radius = 10, fontSize = 13 }) => { const initials = ((user.first_name?.[0] || '') + (user.last_name?.[0] || '')).toUpperCase() || (user.username?.[0] || '?').toUpperCase(); const bg = ROLE_AV_COLORS[user.role_name] || '#475569'; if (user.avatar_url) { return ( {initials} { e.target.style.display = 'none'; e.target.nextSibling && (e.target.nextSibling.style.display = 'flex'); }} /> ); } return (
{initials}
); }; /* ── Presence dot ────────────────────────────────────────────────── */ const presenceFromLastSeen = (lastSeen) => { if (!lastSeen) return 'offline'; const diff = (Date.now() - new Date(lastSeen).getTime()) / 60000; if (diff < 5) return 'online'; if (diff < 60) return 'away'; return 'offline'; }; /* ══════════════════════════════════════════════════════════════════ TAB: Übersicht ══════════════════════════════════════════════════════════════════ */ const OverviewTab = ({ user, full, auditLogs }) => { const rc = ROLE_COLORS[user.role_name] || ROLE_COLORS.benutzer; const mfaMethod = user.mfa_methods?.join(' + ') || '—'; const riskLevel = user.risk_level || 'low'; const riskLabel = { low: 'Niedrig', medium: 'Mittel', high: 'Hoch' }[riskLevel] || 'Unbekannt'; const pwdAgeDays = user.updated_at ? daysBetween(user.updated_at) : null; const pwdOk = !pwdAgeDays || pwdAgeDays < 90; const activityIcons = { login: 'ad-login', asset: 'ad-asset', password: 'ad-pwd', fido: 'ad-key', group: 'ad-perm' }; const mapLogToActivity = (log) => { const a = log.action?.toLowerCase() || ''; let type = 'ad-perm'; if (a.includes('login') || a.includes('anmeld')) type = 'ad-login'; else if (a.includes('asset')) type = 'ad-asset'; else if (a.includes('password') || a.includes('passwort')) type = 'ad-pwd'; else if (a.includes('fido') || a.includes('key')) type = 'ad-key'; return { ...log, dotClass: type }; }; const activities = (auditLogs || []).slice(0, 5).map(mapLogToActivity); const managerName = full?.manager_first_name ? `${full.manager_first_name} ${full.manager_last_name}` : user.manager_username || '—'; const managerInit = full?.manager_first_name ? (full.manager_first_name[0] + (full.manager_last_name?.[0] || '')).toUpperCase() : ''; return (
{/* Stat Row */}
{fmtTime(user.last_login)}
{user.is_active ? 'Aktiv' : 'Inaktiv'}
Konto Status
zugewiesen
{full?.asset_count ?? '—'}
Assets
aktiv
{user.license_count ?? '—'}
Lizenzen
{full?.fido_count > 0 ? 'gepaart' : '—'}
{full?.fido_count ?? '—'}
FIDO-Keys
{/* Grid 2 */}
{/* Persönliche Informationen */}
Persönliche Informationen
Position
{user.position || }
Abteilung
{user.department || }
Telefon
{user.phone || '—'} {user.phone && ( )}
Standort
{user.location || }
Vorgesetzter
{managerInit &&
{managerInit}
} {managerName}
Eintritt
{fmtDate(user.joined_date || full?.onboarding_protocol?.start_date)}
{/* Sicherheit & MFA */}
Sicherheit & MFA
Risiko: {riskLabel}
Mehrfaktor-Authentifizierung
{mfaMethod}
Aktiv
Passwort
{pwdAgeDays !== null ? `Letzte Änderung vor ${pwdAgeDays} Tagen` : 'Unbekannt'}
{pwdOk ? 'OK' : 'Veraltet'}
Conditional Access
{user.ca_policy_count ?? '—'} Richtlinien zugewiesen · Compliant
OK
Aktive Sessions
Letzte Anmeldung {fmtTime(user.last_login)}
{user.is_active ? 'Aktiv' : 'Keine'}
{/* Grid 3 (2 cols) */}
{/* Letzte Aktivitäten */}
Letzte Aktivitäten
Audit-Log
{activities.length === 0 ? (
Keine Aktivitäten vorhanden
) : (
{activities.map((log, i) => (
{log.dotClass === 'ad-login' && } {log.dotClass === 'ad-asset' && } {log.dotClass === 'ad-pwd' && } {log.dotClass === 'ad-key' && } {log.dotClass === 'ad-perm' && }
{log.action}
{log.details || log.entity_type}
{fmtTime(log.created_at)}
))}
)}
{/* Gruppen & Rollen */}
Gruppen & Rollen

IT Nexus Rolle

{rc.label}
{user.entra_groups?.securityGroups?.length > 0 && (

Sicherheitsgruppen

{user.entra_groups.securityGroups.slice(0, 5).map((g, i) => ( {g.displayName} ))}
)} {user.entra_groups?.distributionGroups?.length > 0 && (

Verteiler

{user.entra_groups.distributionGroups.slice(0, 3).map((g, i) => ( {g.mail || g.displayName} ))}
)}

App-Berechtigungen

IT Nexus Portal
{user.role_name}
{user.azure_id &&
Microsoft 365
aktiv
}
); }; /* ══════════════════════════════════════════════════════════════════ TAB: Assets ══════════════════════════════════════════════════════════════════ */ const AssetsTab = ({ user, full }) => { const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(true); const [agents, setAgents] = useState({}); useEffect(() => { setLoading(true); Promise.all([ authFetch(`${API}/assets`).then(r => r.json()), authFetch(`${API}/monitoring/agents`).then(r => r.json()).catch(() => ({ data: [] })), ]).then(([aRes, mRes]) => { const all = aRes.data || []; const userAssets = all.filter(a => a.assigned_to_user_id === user.id || (user.username && a.assigned_to_username === user.username) ); setAssets(userAssets); const agentMap = {}; for (const ag of (mRes.data || [])) agentMap[ag.hostname?.toLowerCase()] = ag; setAgents(agentMap); }).finally(() => setLoading(false)); }, [user.id]); const totalValue = assets.reduce((s, a) => s + (a.purchase_price || 0), 0); const agentForAsset = (asset) => agents[asset.name?.toLowerCase()] || agents[asset.hostname?.toLowerCase()]; const onlineCount = assets.filter(a => { const ag = agentForAsset(a); return ag && presenceFromLastSeen(ag.last_seen) === 'online'; }).length; const assetIcon = (type) => { const t = (type || '').toLowerCase(); if (t.includes('notebook') || t.includes('laptop')) return ; if (t.includes('monitor')) return ; if (t.includes('phone') || t.includes('smartphone')) return ; return ; }; const warrantyPct = (asset) => { if (!asset.warranty_expiry_date || !asset.purchase_date) return null; const total = daysBetween(asset.purchase_date, asset.warranty_expiry_date); const elapsed = daysBetween(asset.purchase_date); return Math.max(0, Math.min(100, Math.round((1 - elapsed / total) * 100))); }; const oldestDate = full?.asset_oldest_date; if (loading) return
Lade Assets…
; return (
{assets.length}
Geräte zugewiesen
{fmtEuro(totalValue)}
Buchwert gesamt
{onlineCount}/{assets.length}
Aktuell online
{oldestDate ? `${daysBetween(oldestDate)} T.` : '—'}
Ältestes Gerät
{assets.length === 0 ? (
Keine Assets zugewiesen
) : ( <>
Zugewiesene Hardware
{assets.map(asset => { const ag = agentForAsset(asset); const isOnline = ag && presenceFromLastSeen(ag.last_seen) === 'online'; const cpuPct = ag?.cpu_usage || 0; const ramPct = ag && ag.ram_total_gb > 0 ? Math.round((ag.ram_used_gb / ag.ram_total_gb) * 100) : 0; const wPct = warrantyPct(asset); const barColor = (v) => v > 85 ? 'red' : v > 65 ? 'amber' : ''; return (
{assetIcon(asset.category || asset.type)}
{asset.name}
{asset.model || '—'}
{ag ? ( <>
CPU-Last{cpuPct}%
Speicher{ramPct}%
) : null} {wPct !== null &&
Garantie{wPct}%
}
{asset.category || asset.type || 'Gerät'} {asset.serial_number || asset.inventory_number || ''}
); })}
)}
); }; /* ══════════════════════════════════════════════════════════════════ TAB: Lizenzen ══════════════════════════════════════════════════════════════════ */ const SKU_DISPLAY = { 'SPE:ENTERPRISEPREMIUM': { name: 'Microsoft 365 E5', color: '#0078d4' }, 'SPE:ENTERPRISEPACK': { name: 'Microsoft 365 E3', color: '#0078d4' }, 'O365_BUSINESS_PREMIUM': { name: 'Microsoft 365 Business', color: '#0078d4' }, 'INTUNE_A': { name: 'Microsoft Intune', color: '#4caf50' }, 'AAD_PREMIUM': { name: 'Entra ID P1', color: '#00bcf2' }, 'AAD_PREMIUM_P2': { name: 'Entra ID P2', color: '#00bcf2' }, 'EMS': { name: 'EMS E3', color: '#7b5ea7' }, 'EMSPREMIUM': { name: 'EMS E5', color: '#7b5ea7' }, 'POWER_BI_PRO': { name: 'Power BI Pro', color: '#f2c811' }, 'FLOW_FREE': { name: 'Power Automate', color: '#0066ff' }, }; const LicensesTab = ({ user }) => { const [licenses, setLicenses] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { if (!user.azure_id) { setLoading(false); return; } authFetch(`${API}/entra/users/${encodeURIComponent(user.azure_id)}/licenses`) .then(r => r.json()) .then(d => setLicenses(d.data || [])) .catch(() => setLicenses([])) .finally(() => setLoading(false)); }, [user.azure_id]); const grouped = useMemo(() => { const g = {}; for (const lic of licenses) { const cat = getLicCategory(lic.skuPartNumber); if (!g[cat]) g[cat] = []; g[cat].push(lic); } return g; }, [licenses]); const getLicDisplay = (lic) => { const key = Object.keys(SKU_DISPLAY).find(k => { const part = k.includes(':') ? k.split(':')[1] : k; return lic.skuPartNumber?.toUpperCase().includes(part); }); return key ? SKU_DISPLAY[key] : { name: lic.skuPartNumber || 'Unbekannte Lizenz', color: '#64748b' }; }; const getInitials = (name) => name.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase(); if (!user.azure_id) return
Kein Azure-Konto verknüpft — Lizenzdaten nicht verfügbar
; if (loading) return
Lade Lizenzen…
; return (
Aktive Lizenzen
{licenses.length}
Aus {licenses.length} zugewiesenen Plätzen
Kosten pro Monat
Preise in Entra konfigurierbar
Verlängerung fällig
Keine fälligen Verlängerungen
{licenses.length === 0 ? (
Keine Lizenzen zugewiesen
) : ( Object.entries(grouped).map(([cat, lics]) => (
{cat} {lics.length}
{lics.map((lic, i) => { const display = getLicDisplay(lic); return (
{getInitials(display.name)}
{display.name}
{lic.skuInfo?.capabilityStatus || 'Enabled'}
/Monat
); })}
)) )}
); }; /* ══════════════════════════════════════════════════════════════════ TAB: Entra/AD ══════════════════════════════════════════════════════════════════ */ const EntraTab = ({ user }) => { const [profile, setProfile] = useState(null); const [groups, setGroups] = useState([]); const [risk, setRisk] = useState(null); const [caps, setCaps] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { if (!user.azure_id) { setLoading(false); return; } Promise.all([ authFetch(`${API}/entra/users/${encodeURIComponent(user.azure_id)}/profile`).then(r => r.json()).catch(() => ({})), authFetch(`${API}/entra/users/${encodeURIComponent(user.azure_id)}/groups`).then(r => r.json()).catch(() => ({})), authFetch(`${API}/entra/users/${encodeURIComponent(user.azure_id)}/signin-risk`).then(r => r.json()).catch(() => ({})), authFetch(`${API}/entra/conditional-access-policies`).then(r => r.json()).catch(() => ({})), ]).then(([pRes, gRes, rRes, capRes]) => { setProfile(pRes.data || null); setGroups(gRes.data || []); setRisk(rRes.data || null); setCaps((capRes.data || []).slice(0, 4)); }).finally(() => setLoading(false)); }, [user.azure_id]); const jwtPreview = profile ? JSON.stringify({ aud: 'api://it-nexus', oid: profile.id, preferred_username: profile.userPrincipalName, roles: [user.role_name], groups: (groups || []).slice(0, 3).map(g => g.displayName), amr: ['pwd', 'mfa'], iat: Math.floor(Date.now() / 1000) }, null, 0) : null; const capGrantType = (policy) => { const controls = policy.grantControls; if (!controls) return 'REPORT'; if (policy.state === 'disabled') return 'DISABLED'; const ops = controls.builtInControls || []; if (ops.includes('block')) return 'BLOCK'; return 'GRANT'; }; if (!user.azure_id) return
Kein Azure-Konto verknüpft
; if (loading) return
Lade Entra-Daten…
; return (
{/* Hybrid Identity */}
Hybrid Identity
Synchronisiert
Entra ID
{profile?.userPrincipalName?.split('@')[1] || 'cereda.onmicrosoft.com'}
CONNECT SYNC
Active Directory
CEREDA.local
Object ID{profile?.id || '—'}
UPN{profile?.userPrincipalName || user.email}
sAMAccountName{profile?.onPremisesSamAccountName || user.username}
Distinguished Name{profile?.onPremisesDistinguishedName || `CN=${user.first_name} ${user.last_name},OU=Users,DC=cereda,DC=local`}
User Type{profile?.userType || 'Member'}
Source of Authority{profile?.onPremisesSyncEnabled ? 'Hybrid · Windows Server AD' : 'Cloud Only'}
Account Status {profile?.accountEnabled !== false ? 'Enabled · Not locked' : 'Disabled'}
{jwtPreview && (
{jwtPreview}
)}
{/* Rechte Spalte */}
{/* Conditional Access */}
Conditional Access
{caps.length} zugewiesen
{caps.length === 0 ? (
Keine Richtlinien geladen
) : (
{caps.map((cap, i) => { const grant = capGrantType(cap); return (
{grant === 'GRANT' && } {grant === 'BLOCK' && } {grant === 'REPORT' && }
{cap.displayName}
Status: {cap.state}
{grant}
); })}
)}
{/* Aktive Sessions */}
Aktive Sessions
Letzte Anmeldung
{fmtTime(user.last_login)}
Sign-in Risk
{risk?.riskLevel ? risk.riskLevel.charAt(0).toUpperCase() + risk.riskLevel.slice(1) : 'Niedrig'}
User Risk
{risk?.riskDetail || 'Niedrig'}
Konto-Status
{user.is_active ? 'Aktiv' : 'Gesperrt'}
); }; /* ══════════════════════════════════════════════════════════════════ TAB: FIDO-Keys ══════════════════════════════════════════════════════════════════ */ const FidoTab = ({ user }) => { const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { authFetch(`${API}/fido-keys?user_id=${user.id}`) .then(r => r.json()) .then(d => setKeys(d.data || [])) .catch(() => setKeys([])) .finally(() => setLoading(false)); }, [user.id]); if (loading) return
Lade FIDO-Keys…
; const primaryKey = keys.find(k => k.key_type === 'primary') || keys[0]; const otherKeys = keys.filter(k => k !== primaryKey); const hasEnoughKeys = keys.length >= 2; return (
{keys.map((key, i) => (
{i === 0 ? 'PRIMÄR' : 'BACKUP'}
{key.name}
{key.manufacturer || 'Yubico'} · {key.connection_type || 'USB'} · SN {key.serial_number}
Auth. Gesamt
{fmtTime(key.last_used_at)}
Letzte Nutzung
))}
Weiteren Schlüssel registrieren
YubiKey, Titan, Feitian — WebAuthn
Authentifizierungs-Aktivität
Letzte 7 Tage
Erfolgreiche Anmeldungen— via FIDO2
Touch-Bestätigungen— · User Presence
PIN-Eingaben— · User Verification
Fehlversuche0 blockiert
Verwendete Browser
Empfehlungen
{hasEnoughKeys ? : }
Mindestens 2 Schlüssel registriert
Primär & Backup vorhanden
{hasEnoughKeys ? 'Erfüllt' : 'Ausstehend'}
Schlüssel-Attestation aktiv
Nur zertifizierte Authenticators
Erfüllt
Passwortlose Anmeldung
Optional — Passwort noch aktiv
Empfohlen
); }; /* ══════════════════════════════════════════════════════════════════ TAB: Lifecycle ══════════════════════════════════════════════════════════════════ */ const LifecycleTab = ({ user, full }) => { const ob = full?.onboarding_protocol; const offb = full?.offboarding_protocol; const stage = full?.lifecycle_stage || 3; const joinDate = user.joined_date || ob?.start_date; const stages = [ { n: 1, label: 'Pre-Onboarding', date: joinDate ? fmtDate(new Date(joinDate).getTime() - 7 * 86400000) : '— 7 Tage vor Start' }, { n: 2, label: 'Onboarding', date: ob?.start_date ? fmtDate(ob.start_date) : '—' }, { n: 3, label: 'Aktiv', date: ob?.start_date ? `Seit ${fmtDate(ob.start_date)}` : (joinDate ? `Seit ${fmtDate(joinDate)}` : '—') }, { n: 4, label: 'Rollenwechsel', date: '— optional' }, { n: 5, label: 'Offboarding', date: offb?.exit_date ? fmtDate(offb.exit_date) : '— offen' }, ]; const daysUntil = (months) => months * 30 + (joinDate ? daysBetween(joinDate) % (months * 30) : 0); const milestones = [ { label: 'Vertrag unterzeichnet', sub: `IT & Infrastruktur · ${user.position || 'Mitarbeiter'}`, date: fmtDate(joinDate), type: joinDate ? 'done' : 'upcoming' }, { label: 'Hardware ausgehändigt', sub: `${full?.asset_count || 0} Geräte · Setup durch IT`, date: fmtDate(joinDate), type: full?.asset_count > 0 ? 'done' : 'upcoming' }, { label: 'IT-Schulung & Security Awareness', sub: 'ISO 27001 · Phishing-Simulation', date: joinDate ? `vor ${Math.min(90, daysBetween(joinDate))} T.` : '—', type: joinDate && daysBetween(joinDate) > 90 ? 'done' : joinDate ? 'current' : 'upcoming' }, { label: 'Probezeit abgeschlossen', sub: '90 Tage Probezeit', date: joinDate ? (daysBetween(joinDate) > 90 ? `vor ${daysBetween(joinDate) - 90} T.` : `in ${90 - daysBetween(joinDate)} T.`) : '—', type: joinDate && daysBetween(joinDate) > 90 ? 'done' : 'upcoming' }, { label: 'Letzte Recertification der Berechtigungen', sub: 'Quartalsweise', date: 'vor 14 T.', type: 'current' }, { label: 'Nächstes Mitarbeitergespräch', sub: 'Jährlich geplant', date: 'in 42 T.', type: 'upcoming', future: true }, { label: 'Hardware-Refresh fällig', sub: 'Notebook-Tausch nach 36 Monaten', date: joinDate && daysBetween(joinDate) < 36*30 ? `in ${36*30 - daysBetween(joinDate)} T.` : 'in 184 T.', type: 'upcoming', future: true }, { label: 'Security-Schulung Wiederholung', sub: 'Jährliche Pflichtschulung', date: 'in 275 T.', type: 'upcoming', future: true }, ]; const navigate = useNavigate(); return (
{/* Stage Bar */}
{stages.map(s => (
{String(s.n).padStart(2, '0')} · {s.n < stage ? 'ABGESCHLOSSEN' : s.n === stage ? 'LAUFEND' : ''}
{s.label}
{s.date}
))}
{/* Meilensteine */}
Meilensteine & Termine
Alle anzeigen
{milestones.map((ms, i) => (
{ms.type === 'done' && }
{ms.label}
{ms.sub}
{ms.date}
))}
{/* Rechte Spalte */}
{/* Vertrag & Rolle */}
Vertrag & Rolle
Eintritt
{fmtDate(joinDate)}
{user.employment_type || 'Unbefristet'}
Beschäftigung
{user.employment_type || 'Vollzeit'} · {user.work_hours || 40}h
TVöD-äquiv.
Standort
{user.location || '—'}
Hybrid 3:2
Kostenstelle
{user.cost_center || '—'}
{user.department || ''}
{/* Offboarding Bereitschaft */}
Offboarding-Bereitschaft
{offb ? 'Aktiv' : 'Nicht aktiv'}
Geplantes Austrittsdatum{offb?.exit_date ? fmtDate(offb.exit_date) : '— Keines hinterlegt'}
Datenübergabe-PlanAutomatisch · OneDrive, Mail-Archiv
Hardware-Rückgabe{full?.asset_count || 0} Geräte registriert
Lizenzfreigabe{user.license_count || '—'} Lizenzen
Wissenstransfer— Noch nicht initiiert
); }; /* ══════════════════════════════════════════════════════════════════ Edit Modal ══════════════════════════════════════════════════════════════════ */ const EditModal = ({ user, allUsers, onClose, onSaved }) => { const [form, setForm] = useState({ first_name: user.first_name || '', last_name: user.last_name || '', email: user.email || '', department: user.department || '', position: user.position || '', phone: user.phone || '', location: user.location || '', manager_id: user.manager_id || '', cost_center: user.cost_center || '', employment_type: user.employment_type || 'Vollzeit', work_hours: user.work_hours || 40, joined_date: user.joined_date ? user.joined_date.split('T')[0] : '', }); const [saving, setSaving] = useState(false); const save = async () => { setSaving(true); try { const r = await authFetch(`${API}/users/${user.id}`, { method: 'PUT', body: JSON.stringify(form) }); if (!r.ok) throw new Error('Fehler beim Speichern'); toast.success('Gespeichert'); onSaved({ ...user, ...form }); onClose(); } catch { toast.error('Fehler beim Speichern'); } finally { setSaving(false); } }; return (
e.target === e.currentTarget && onClose()}>
Benutzer bearbeiten
{[ ['first_name','Vorname'],['last_name','Nachname'],['email','E-Mail'], ['department','Abteilung'],['position','Position'],['phone','Telefon'], ['location','Standort'],['cost_center','Kostenstelle'], ].map(([k, label]) => (
setForm(p => ({...p, [k]: e.target.value}))} />
))}
setForm(p => ({...p, work_hours: parseInt(e.target.value)}))} />
setForm(p => ({...p, joined_date: e.target.value}))} />
); }; /* ══════════════════════════════════════════════════════════════════ More Actions Dropdown ══════════════════════════════════════════════════════════════════ */ const MoreDropdown = ({ user, onDeactivate, onDelete }) => { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, []); return (
{open && (
)}
); }; /* ══════════════════════════════════════════════════════════════════ MAIN PAGE ══════════════════════════════════════════════════════════════════ */ const UserManagementPage = () => { const { isSuperAdmin, isAdmin } = useAuth(); const navigate = useNavigate(); const [users, setUsers] = useState([]); const [selectedId, setSelectedId] = useState(null); const [full, setFull] = useState(null); const [auditLogs, setAuditLogs] = useState([]); const [tab, setTab] = useState('overview'); const [search, setSearch] = useState(''); const [roleFilter, setRoleFilter] = useState('all'); const [loading, setLoading] = useState(true); const [showEdit, setShowEdit] = useState(false); const [agents, setAgents] = useState([]); // Load users + agents useEffect(() => { Promise.all([ authFetch(`${API}/users`).then(r => r.json()), authFetch(`${API}/monitoring/agents`).then(r => r.json()).catch(() => ({ data: [] })), ]).then(([uRes, aRes]) => { setUsers(uRes.data || []); setAgents(aRes.data || []); if ((uRes.data || []).length > 0) setSelectedId(uRes.data[0].id); }).finally(() => setLoading(false)); }, []); const selectedUser = useMemo(() => users.find(u => u.id === selectedId), [users, selectedId]); // Load full profile when user changes useEffect(() => { if (!selectedId) return; setFull(null); setAuditLogs([]); Promise.all([ authFetch(`${API}/users/${selectedId}/full`).then(r => r.json()).catch(() => ({})), authFetch(`${API}/audit-logs/user/${selectedId}?limit=10`).then(r => r.json()).catch(() => ({})), ]).then(([fRes, lRes]) => { setFull(fRes.data || null); setAuditLogs(lRes.data || []); }); }, [selectedId]); // Listen for edit event from OverviewTab useEffect(() => { const h = () => setShowEdit(true); document.addEventListener('bv-edit', h); return () => document.removeEventListener('bv-edit', h); }, []); // Presence from agents const presenceForUser = useCallback((u) => { const ag = agents.find(a => a.username?.toLowerCase() === u.username?.toLowerCase() || a.hostname?.toLowerCase()?.includes(u.username?.toLowerCase())); return ag ? presenceFromLastSeen(ag.last_seen) : 'offline'; }, [agents]); // Filter + sort users const filtered = useMemo(() => { let list = [...users]; if (roleFilter !== 'all') { const roles = ROLE_FILTER_MAP[roleFilter] || []; list = list.filter(u => roles.includes(u.role_name)); } if (search.trim()) { const t = search.trim().toLowerCase(); list = list.filter(u => `${u.first_name} ${u.last_name} ${u.email} ${u.username}`.toLowerCase().includes(t)); } return list; }, [users, roleFilter, search]); const favorites = filtered.filter(u => IS_ADMIN_ROLE(u.role_name)); const others = filtered.filter(u => !IS_ADMIN_ROLE(u.role_name)); const handleSelect = (id) => { setSelectedId(id); setTab('overview'); }; const handleDeactivate = async () => { if (!selectedUser) return; const r = await authFetch(`${API}/users/${selectedUser.id}/activate`, { method: 'PUT', body: JSON.stringify({ is_active: !selectedUser.is_active }) }); if (r.ok) { setUsers(p => p.map(u => u.id === selectedUser.id ? { ...u, is_active: !u.is_active } : u)); toast.success(selectedUser.is_active ? 'Benutzer deaktiviert' : 'Benutzer aktiviert'); } }; const handleDelete = async () => { if (!selectedUser || !window.confirm(`Benutzer "${selectedUser.username}" wirklich löschen?`)) return; const r = await authFetch(`${API}/users/${selectedUser.id}`, { method: 'DELETE' }); if (r.ok) { setUsers(p => p.filter(u => u.id !== selectedUser.id)); setSelectedId(users.find(u => u.id !== selectedUser.id)?.id || null); toast.success('Benutzer gelöscht'); } }; const handleResetEntra = async () => { if (!selectedUser) return; const r = await authFetch(`${API}/users/${selectedUser.id}/reset-entra`, { method: 'POST' }); const d = await r.json(); if (r.ok) toast.success(d.message || 'Entra-Sessions invalidiert'); else toast.error(d.message || 'Fehler beim Reset'); }; const handleExportCsv = () => { const token = localStorage.getItem('token'); window.open(`${API}/users/export/csv?token=${token}`, '_blank'); }; const rc = selectedUser ? (ROLE_COLORS[selectedUser.role_name] || ROLE_COLORS.benutzer) : null; const presence = selectedUser ? presenceForUser(selectedUser) : 'offline'; const initials = selectedUser ? ((selectedUser.first_name?.[0] || '') + (selectedUser.last_name?.[0] || '')).toUpperCase() || selectedUser.username?.[0]?.toUpperCase() || '?' : '?'; const TABS = [ { id: 'overview', label: 'Übersicht' }, { id: 'assets', label: 'Assets', count: full?.asset_count }, { id: 'licenses', label: 'Lizenzen', count: selectedUser?.license_count }, { id: 'entra', label: 'Entra/AD' }, { id: 'fido', label: 'FIDO-Keys', count: full?.fido_count }, { id: 'lifecycle', label: 'Lifecycle' }, ]; const UserRow = ({ u }) => { const p = presenceForUser(u); const rcolor = ROLE_COLORS[u.role_name] || ROLE_COLORS.benutzer; const avatarBg = ROLE_AV_COLORS[u.role_name] || '#475569'; const init = ((u.first_name?.[0] || '') + (u.last_name?.[0] || '')).toUpperCase() || u.username?.[0]?.toUpperCase() || '?'; return (
handleSelect(u.id)}> {u.avatar_url ? ( {init} { e.target.style.display='none'; }} /> ) : (
{init}
)}
{u.first_name} {u.last_name}
{u.email}
{rcolor.label}
); }; if (loading) return
Lade Benutzerdaten…
; return (
{/* ── User List ─────────────────────────────────────── */}
Benutzer
{filtered.length} von {users.length}
Verwalte Identitäten, Rollen & Zugriffe
setSearch(e.target.value)} placeholder="Name oder E-Mail suchen…" />
{[ ['all','Alle'],['admin','Admin'],['staff','Support'], ['hr','HR/Buchhaltung'],['production','Produktion'],['user','Benutzer'], ].map(([k, l]) => ( ))}
{favorites.length > 0 && <>
Favoriten
{favorites.map(u => )} } {others.length > 0 && <>
Alle Benutzer
{others.map(u => )} } {filtered.length === 0 &&
Keine Benutzer gefunden
}
{/* ── Detail Panel ──────────────────────────────────── */}
{!selectedUser ? (
Benutzer auswählen
) : (
{/* Header */}
{selectedUser.avatar_url ? ( {initials} { e.target.style.display='none'; e.target.closest('.bv-dh-avatar').style.background = ROLE_AV_COLORS[selectedUser.role_name] || '#475569'; }} /> ) : ( initials )}

{selectedUser.first_name} {selectedUser.last_name}

@{selectedUser.username}
{selectedUser.email}
{rc.label} {selectedUser.department && ( {selectedUser.department} )} {selectedUser.is_active ? 'Aktiv' : 'Inaktiv'}
{selectedUser.azure_id && ( )} {isSuperAdmin() && ( )}
{/* Tabs */}
{TABS.map(t => ( ))}
{/* Tab Content */} {tab === 'overview' && } {tab === 'assets' && } {tab === 'licenses' && } {tab === 'entra' && } {tab === 'fido' && } {tab === 'lifecycle' && }
)}
{/* Edit Modal */} {showEdit && selectedUser && ( setShowEdit(false)} onSaved={(updated) => setUsers(p => p.map(u => u.id === updated.id ? { ...u, ...updated } : u))} /> )}
); }; export default UserManagementPage;