import { useState, useEffect, DragEvent } from 'react' import { useNavigate } from 'react-router-dom' import { api } from '../services/api' import { User, UserRole } from '@skillmate/shared' import { TrashIcon, ShieldIcon, KeyIcon } from '../components/icons' import { useAuthStore } from '../stores/authStore' interface UserWithEmployee extends User { employeeName?: string } export default function UserManagement() { const navigate = useNavigate() const { user: currentUser } = useAuthStore() const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [editingUser, setEditingUser] = useState(null) const [editRole, setEditRole] = useState('user') const [resetPasswordUser, setResetPasswordUser] = useState(null) const [newPassword, setNewPassword] = useState('') useEffect(() => { fetchUsers() }, []) const fetchUsers = async () => { try { setLoading(true) const [usersResponse, employeesResponse] = await Promise.all([ api.get('/admin/users'), api.get('/employees') ]) const usersData = usersResponse.data.data || [] const employeesData = employeesResponse.data.data || [] // Match users with employee names const enrichedUsers = usersData.map((user: User) => { const employee = employeesData.find((emp: any) => emp.id === user.employeeId) return { ...user, employeeName: employee ? `${employee.firstName} ${employee.lastName}` : undefined } }) setUsers(enrichedUsers) setEmployees(employeesData) } catch (err: any) { console.error('Failed to fetch users:', err) setError('Benutzer konnten nicht geladen werden') } finally { setLoading(false) } } const [employees, setEmployees] = useState([]) // Import state type ImportRow = { firstName: string; lastName: string; email: string; department: string } const [dragActive, setDragActive] = useState(false) const [parsedRows, setParsedRows] = useState([]) const [parseError, setParseError] = useState('') const [isImporting, setIsImporting] = useState(false) const [importResults, setImportResults] = useState<{ index: number status: 'created' | 'error' employeeId?: string userId?: string username?: string email?: string temporaryPassword?: string error?: string }[]>([]) // Store temporary passwords per user to show + email const [tempPasswords, setTempPasswords] = useState>({}) // removed legacy creation helpers for employees without user accounts const handleRoleChange = async (userId: string) => { try { await api.put(`/admin/users/${userId}/role`, { role: editRole }) await fetchUsers() setEditingUser(null) } catch (err: any) { setError('Rolle konnte nicht geändert werden') } } const handleToggleActive = async (userId: string, isActive: boolean) => { try { await api.put(`/admin/users/${userId}/status`, { isActive: !isActive }) await fetchUsers() } catch (err: any) { setError('Status konnte nicht geändert werden') } } const handlePasswordReset = async (userId: string) => { try { const response = await api.post(`/admin/users/${userId}/reset-password`, { newPassword: newPassword || undefined }) const tempPassword = response.data.data?.temporaryPassword if (tempPassword) { setTempPasswords(prev => ({ ...prev, [userId]: { password: tempPassword } })) } setResetPasswordUser(null) setNewPassword('') } catch (err: any) { setError('Passwort konnte nicht zurückgesetzt werden') } } const sendTempPasswordEmail = async (userId: string, password: string) => { try { await api.post(`/admin/users/${userId}/send-temp-password`, { password }) alert('Temporäres Passwort per E-Mail versendet.') } catch (err: any) { setError('E-Mail-Versand des temporären Passworts fehlgeschlagen') } } const onDragOver = (e: DragEvent) => { e.preventDefault(); setDragActive(true) } const onDragLeave = () => setDragActive(false) const onDrop = (e: DragEvent) => { e.preventDefault(); setDragActive(false) if (e.dataTransfer.files && e.dataTransfer.files[0]) { handleFile(e.dataTransfer.files[0]) } } const handleFile = (file: File) => { setParseError('') const reader = new FileReader() reader.onload = () => { try { const text = String(reader.result || '') if (file.name.toLowerCase().endsWith('.json')) { const json = JSON.parse(text) const rows: ImportRow[] = Array.isArray(json) ? json : [json] validateAndSetRows(rows) } else { // Simple CSV parser (comma or semicolon) const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0) if (lines.length === 0) throw new Error('Leere Datei') const header = lines[0].split(/[;,]/).map(h => h.trim().toLowerCase()) const idx = (name: string) => header.indexOf(name) const rows: ImportRow[] = [] for (let i = 1; i < lines.length; i++) { const cols = lines[i].split(/[;,]/).map(c => c.trim()) rows.push({ firstName: cols[idx('firstname')] || cols[idx('vorname')] || '', lastName: cols[idx('lastname')] || cols[idx('nachname')] || '', email: cols[idx('email')] || '', department: cols[idx('department')] || cols[idx('abteilung')] || '' }) } validateAndSetRows(rows) } } catch (err: any) { setParseError(err.message || 'Datei konnte nicht gelesen werden') setParsedRows([]) } } reader.readAsText(file) } const validateAndSetRows = (rows: ImportRow[]) => { const valid: ImportRow[] = [] for (const r of rows) { if (!r.firstName || !r.lastName || !r.email || !r.department) continue if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(r.email)) continue valid.push({ ...r }) } setParsedRows(valid) } const startImport = async () => { if (parsedRows.length === 0) return setIsImporting(true) setImportResults([]) try { const results: any[] = [] for (let i = 0; i < parsedRows.length; i++) { const r = parsedRows[i] try { const res = await api.post('/employees', { firstName: r.firstName, lastName: r.lastName, email: r.email, department: r.department, createUser: true, userRole: 'user' }) const data = res.data?.data || {} results.push({ index: i, status: 'created', employeeId: data.id, userId: data.userId, email: r.email, username: r.email?.split('@')[0], temporaryPassword: data.temporaryPassword }) } catch (err: any) { results.push({ index: i, status: 'error', error: err.response?.data?.error?.message || 'Fehler beim Import' }) } } setImportResults(results) await fetchUsers() } finally { setIsImporting(false) } } const handleDeleteUser = async (userId: string) => { if (!confirm('Möchten Sie diesen Benutzer wirklich löschen?')) return try { await api.delete(`/admin/users/${userId}`) await fetchUsers() } catch (err: any) { setError('Benutzer konnte nicht gelöscht werden') } } const getRoleBadgeColor = (role: UserRole) => { switch (role) { case 'admin': return 'bg-red-100 text-red-800' case 'superuser': return 'bg-blue-100 text-blue-800' default: return 'bg-gray-100 text-gray-800' } } const getRoleLabel = (role: UserRole) => { switch (role) { case 'admin': return 'Administrator' case 'superuser': return 'Poweruser' default: return 'Benutzer' } } if (loading) { return (
Lade Benutzer...
) } return (

Benutzerverwaltung

Verwalten Sie Benutzerkonten, Rollen und Zugriffsrechte

{currentUser?.role === 'admin' && (

Administrative Aktionen

)} {error && (
{error}
)}
{users.map((user) => ( ))}
Benutzer Mitarbeitende Rolle Status Letzter Login Aktionen
{user.username === 'admin' ? 'A' : user.username.charAt(0).toUpperCase()}
{user.username === 'admin' ? 'admin' : user.username}
{user.employeeName ? ( {user.employeeName} ) : ( Nicht verknüpft )} {editingUser === user.id ? (
) : ( {getRoleLabel(user.role)} )}
{user.lastLogin ? new Date(user.lastLogin).toLocaleString('de-DE') : 'Nie'}
{resetPasswordUser === user.id ? (
setNewPassword(e.target.value)} className="input-field py-1 text-sm w-40" />
) : ( )}
{tempPasswords[user.id] && (
Temporäres Passwort: {tempPasswords[user.id].password}
)}
{users.length === 0 && (
Keine Benutzer gefunden
)}

Hinweise zur Benutzerverwaltung

  • Administrator: Vollzugriff auf alle Funktionen und Einstellungen
  • Poweruser: Kann Mitarbeitende und Skills verwalten, aber keine Systemeinstellungen ändern
  • Benutzer: Kann nur eigenes Profil bearbeiten und Daten einsehen
  • • Neue Benutzer können über den Import oder die Mitarbeitendenverwaltung angelegt werden
  • • Der Admin-Benutzer kann nicht gelöscht werden

Import neue Nutzer (CSV oder JSON)

Datei hierher ziehen oder auswählen

e.target.files && e.target.files[0] && handleFile(e.target.files[0])} className="hidden" id="user-import-input" /> {parseError &&
{parseError}
}

Eingabekonventionen:

  • CSV mit Kopfzeile: firstName;lastName;email;department (Komma oder Semikolon)
  • JSON: Array von Objekten mit Schlüsseln firstName, lastName, email, department
  • E-Mail muss valide sein; Rolle wird initial immer „user“
  • Es wird stets ein temporäres Passwort erzeugt; Anzeige nach Import
{parsedRows.length > 0 && (

Vorschau ({parsedRows.length} Einträge)

{parsedRows.map((r, idx) => ( ))}
Vorname Nachname E-Mail Abteilung
{r.firstName} {r.lastName} {r.email} {r.department}
)} {importResults.length > 0 && (

Import-Ergebnis

{importResults.map((r, idx) => ( ))}
Zeile E-Mail Status Temporäres Passwort Aktionen
{r.index + 1} {r.email || '—'} {r.status === 'created' ? 'Erstellt' : `Fehler: ${r.error}`} {r.temporaryPassword ? ( {r.temporaryPassword} ) : ( '—' )} {r.userId && r.temporaryPassword && (
)}
)}
) } function PurgeUsersPanel({ onDone }: { onDone: () => void }) { const [email, setEmail] = useState('') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const runPurge = async () => { if (!email || !email.includes('@')) { setMsg('Bitte gültige E-Mail eingeben') return } const ok = confirm(`Achtung: Dies löscht alle Benutzer außer 'admin' und ${email}. Fortfahren?`) if (!ok) return try { setBusy(true) setMsg('') const res = await api.post('/admin/users/purge', { email }) const { kept, deleted } = res.data.data || {} setMsg(`Bereinigung abgeschlossen. Behalten: ${kept}, gelöscht: ${deleted}`) onDone() } catch (e: any) { setMsg('Bereinigung fehlgeschlagen') } finally { setBusy(false) } } return (

Nur Administratoren: Löscht alle Benutzer und behält nur 'admin' und die angegebene E‑Mail.

setEmail(e.target.value)} />
{msg &&
{msg}
}
) }