659 Zeilen
26 KiB
TypeScript
659 Zeilen
26 KiB
TypeScript
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<UserWithEmployee[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState('')
|
||
const [editingUser, setEditingUser] = useState<string | null>(null)
|
||
const [editRole, setEditRole] = useState<UserRole>('user')
|
||
const [resetPasswordUser, setResetPasswordUser] = useState<string | null>(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<any[]>([])
|
||
|
||
// Import state
|
||
type ImportRow = { firstName: string; lastName: string; email: string; department: string }
|
||
const [dragActive, setDragActive] = useState(false)
|
||
const [parsedRows, setParsedRows] = useState<ImportRow[]>([])
|
||
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<Record<string, { password: string }>>({})
|
||
|
||
// 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<HTMLDivElement>) => {
|
||
e.preventDefault();
|
||
setDragActive(true)
|
||
}
|
||
const onDragLeave = () => setDragActive(false)
|
||
const onDrop = (e: DragEvent<HTMLDivElement>) => {
|
||
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 (
|
||
<div className="flex items-center justify-center h-64">
|
||
<div className="text-secondary">Lade Benutzer...</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<div className="mb-8 flex justify-between items-start">
|
||
<div>
|
||
<h1 className="text-title-lg font-poppins font-bold text-primary mb-2">
|
||
Benutzerverwaltung
|
||
</h1>
|
||
<p className="text-body text-secondary">
|
||
Verwalten Sie Benutzerkonten, Rollen und Zugriffsrechte
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => navigate('/users/create-employee')}
|
||
className="btn-primary"
|
||
>
|
||
+ Neues Mitarbeitendenprofil anlegen
|
||
</button>
|
||
</div>
|
||
|
||
{currentUser?.role === 'admin' && (
|
||
<div className="card mb-6 bg-red-50 border border-red-200">
|
||
<h3 className="text-title-card font-poppins font-semibold text-primary mb-2">Administrative Aktionen</h3>
|
||
<PurgeUsersPanel onDone={fetchUsers} />
|
||
</div>
|
||
)}
|
||
|
||
{error && (
|
||
<div className="bg-error-bg text-error px-4 py-3 rounded-input text-sm mb-6">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
<div className="card">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="border-b border-border-default">
|
||
<th className="text-left py-3 px-4 font-poppins font-medium text-secondary">
|
||
Benutzer
|
||
</th>
|
||
<th className="text-left py-3 px-4 font-poppins font-medium text-secondary">
|
||
Mitarbeitende
|
||
</th>
|
||
<th className="text-left py-3 px-4 font-poppins font-medium text-secondary">
|
||
Rolle
|
||
</th>
|
||
<th className="text-left py-3 px-4 font-poppins font-medium text-secondary">
|
||
Status
|
||
</th>
|
||
<th className="text-left py-3 px-4 font-poppins font-medium text-secondary">
|
||
Letzter Login
|
||
</th>
|
||
<th className="text-right py-3 px-4 font-poppins font-medium text-secondary">
|
||
Aktionen
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{users.map((user) => (
|
||
<tr key={user.id} className="border-b border-border-light hover:bg-bg-hover">
|
||
<td className="py-4 px-4">
|
||
<div className="flex items-center">
|
||
<div className="w-8 h-8 bg-primary-blue rounded-full flex items-center justify-center text-white text-sm font-medium mr-3">
|
||
{user.username === 'admin' ? 'A' : user.username.charAt(0).toUpperCase()}
|
||
</div>
|
||
<span className="font-medium text-primary">
|
||
{user.username === 'admin' ? 'admin' : user.username}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td className="py-4 px-4">
|
||
{user.employeeName ? (
|
||
<span className="text-secondary">{user.employeeName}</span>
|
||
) : (
|
||
<span className="text-tertiary italic">Nicht verknüpft</span>
|
||
)}
|
||
</td>
|
||
<td className="py-4 px-4">
|
||
{editingUser === user.id ? (
|
||
<div className="flex items-center space-x-2">
|
||
<select
|
||
value={editRole}
|
||
onChange={(e) => setEditRole(e.target.value as UserRole)}
|
||
className="input-field py-1 text-sm"
|
||
>
|
||
<option value="user">Benutzer</option>
|
||
<option value="superuser">Poweruser</option>
|
||
<option value="admin">Administrator</option>
|
||
</select>
|
||
<button
|
||
onClick={() => handleRoleChange(user.id)}
|
||
className="text-primary-blue hover:text-primary-blue-hover"
|
||
>
|
||
✓
|
||
</button>
|
||
<button
|
||
onClick={() => setEditingUser(null)}
|
||
className="text-error hover:text-red-700"
|
||
>
|
||
✗
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getRoleBadgeColor(user.role)}`}>
|
||
{getRoleLabel(user.role)}
|
||
</span>
|
||
)}
|
||
</td>
|
||
<td className="py-4 px-4">
|
||
<button
|
||
onClick={() => handleToggleActive(user.id, user.isActive)}
|
||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||
user.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||
}`}
|
||
>
|
||
{user.isActive ? 'Aktiv' : 'Inaktiv'}
|
||
</button>
|
||
</td>
|
||
<td className="py-4 px-4 text-secondary text-sm">
|
||
{user.lastLogin ? new Date(user.lastLogin).toLocaleString('de-DE') : 'Nie'}
|
||
</td>
|
||
<td className="py-4 px-4">
|
||
<div className="flex justify-end space-x-2">
|
||
<button
|
||
onClick={() => {
|
||
setEditingUser(user.id)
|
||
setEditRole(user.role)
|
||
}}
|
||
className="p-1 text-secondary hover:text-primary-blue transition-colors"
|
||
title="Rolle bearbeiten"
|
||
>
|
||
<ShieldIcon className="w-5 h-5" />
|
||
</button>
|
||
|
||
{resetPasswordUser === user.id ? (
|
||
<div className="flex items-center space-x-2">
|
||
<input
|
||
type="text"
|
||
placeholder="Neues Passwort (leer = zufällig)"
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
className="input-field py-1 text-sm w-40"
|
||
/>
|
||
<button
|
||
onClick={() => handlePasswordReset(user.id)}
|
||
className="text-primary-blue hover:text-primary-blue-hover"
|
||
>
|
||
✓
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setResetPasswordUser(null)
|
||
setNewPassword('')
|
||
}}
|
||
className="text-error hover:text-red-700"
|
||
>
|
||
✗
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<button
|
||
onClick={() => setResetPasswordUser(user.id)}
|
||
className="p-1 text-secondary hover:text-warning transition-colors"
|
||
title="Passwort zurücksetzen"
|
||
>
|
||
<KeyIcon className="w-5 h-5" />
|
||
</button>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => handleDeleteUser(user.id)}
|
||
className="p-1 text-secondary hover:text-error transition-colors"
|
||
title="Benutzer löschen"
|
||
disabled={user.username === 'admin'}
|
||
>
|
||
<TrashIcon className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
{tempPasswords[user.id] && (
|
||
<div className="mt-2 bg-green-50 border border-green-200 rounded-input p-3">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div className="text-sm text-green-800">
|
||
Temporäres Passwort: <code className="bg-gray-100 px-2 py-0.5 rounded">{tempPasswords[user.id].password}</code>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
className="btn-secondary h-8 px-3"
|
||
onClick={() => navigator.clipboard.writeText(tempPasswords[user.id].password)}
|
||
>
|
||
Kopieren
|
||
</button>
|
||
<button
|
||
className="btn-primary h-8 px-3"
|
||
onClick={() => sendTempPasswordEmail(user.id, tempPasswords[user.id].password)}
|
||
>
|
||
E-Mail senden
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
{users.length === 0 && (
|
||
<div className="text-center py-8 text-secondary">
|
||
Keine Benutzer gefunden
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 card bg-blue-50 border-blue-200">
|
||
<h3 className="text-title-card font-poppins font-semibold text-primary mb-3">
|
||
Hinweise zur Benutzerverwaltung
|
||
</h3>
|
||
<ul className="space-y-2 text-body text-secondary">
|
||
<li>• <strong>Administrator:</strong> Vollzugriff auf alle Funktionen und Einstellungen</li>
|
||
<li>• <strong>Poweruser:</strong> Kann Mitarbeitende und Skills verwalten, aber keine Systemeinstellungen ändern</li>
|
||
<li>• <strong>Benutzer:</strong> Kann nur eigenes Profil bearbeiten und Daten einsehen</li>
|
||
<li>• Neue Benutzer können über den Import oder die Mitarbeitendenverwaltung angelegt werden</li>
|
||
<li>• Der Admin-Benutzer kann nicht gelöscht werden</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="mt-8 card">
|
||
<h3 className="text-title-card font-poppins font-semibold text-primary mb-3">
|
||
Import neue Nutzer (CSV oder JSON)
|
||
</h3>
|
||
<div
|
||
className={`border-2 border-dashed rounded-input p-6 text-center cursor-pointer ${dragActive ? 'border-primary-blue' : 'border-border-default'}`}
|
||
onDragOver={onDragOver}
|
||
onDragLeave={onDragLeave}
|
||
onDrop={onDrop}
|
||
>
|
||
<p className="text-secondary mb-2">Datei hierher ziehen oder auswählen</p>
|
||
<input
|
||
type="file"
|
||
accept=".csv,.json,application/json,text/csv"
|
||
onChange={(e) => e.target.files && e.target.files[0] && handleFile(e.target.files[0])}
|
||
className="hidden"
|
||
id="user-import-input"
|
||
/>
|
||
<label htmlFor="user-import-input" className="btn-secondary inline-block">Datei auswählen</label>
|
||
{parseError && <div className="text-error text-sm mt-3">{parseError}</div>}
|
||
</div>
|
||
|
||
<div className="mt-4 text-body text-secondary">
|
||
<p className="mb-2 font-medium">Eingabekonventionen:</p>
|
||
<ul className="list-disc ml-5 space-y-1">
|
||
<li>CSV mit Kopfzeile: firstName;lastName;email;department (Komma oder Semikolon)</li>
|
||
<li>JSON: Array von Objekten mit Schlüsseln firstName, lastName, email, department</li>
|
||
<li>E-Mail muss valide sein; Rolle wird initial immer „user“</li>
|
||
<li>Es wird stets ein temporäres Passwort erzeugt; Anzeige nach Import</li>
|
||
</ul>
|
||
</div>
|
||
|
||
{parsedRows.length > 0 && (
|
||
<div className="mt-4">
|
||
<h4 className="text-body font-semibold text-secondary mb-2">Vorschau ({parsedRows.length} Einträge)</h4>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="border-b border-border-default">
|
||
<th className="text-left py-2 px-3 text-secondary">Vorname</th>
|
||
<th className="text-left py-2 px-3 text-secondary">Nachname</th>
|
||
<th className="text-left py-2 px-3 text-secondary">E-Mail</th>
|
||
<th className="text-left py-2 px-3 text-secondary">Abteilung</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{parsedRows.map((r, idx) => (
|
||
<tr key={idx} className="border-b border-border-light">
|
||
<td className="py-2 px-3">{r.firstName}</td>
|
||
<td className="py-2 px-3">{r.lastName}</td>
|
||
<td className="py-2 px-3">{r.email}</td>
|
||
<td className="py-2 px-3">{r.department}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div className="mt-3 flex justify-end">
|
||
<button className="btn-primary" onClick={startImport} disabled={isImporting}>
|
||
{isImporting ? 'Importiere...' : 'Import starten'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{importResults.length > 0 && (
|
||
<div className="mt-6">
|
||
<h4 className="text-body font-semibold text-secondary mb-2">Import-Ergebnis</h4>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="border-b border-border-default">
|
||
<th className="text-left py-2 px-3 text-secondary">Zeile</th>
|
||
<th className="text-left py-2 px-3 text-secondary">E-Mail</th>
|
||
<th className="text-left py-2 px-3 text-secondary">Status</th>
|
||
<th className="text-left py-2 px-3 text-secondary">Temporäres Passwort</th>
|
||
<th className="text-right py-2 px-3 text-secondary">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{importResults.map((r, idx) => (
|
||
<tr key={idx} className="border-b border-border-light">
|
||
<td className="py-2 px-3">{r.index + 1}</td>
|
||
<td className="py-2 px-3">{r.email || '—'}</td>
|
||
<td className="py-2 px-3">{r.status === 'created' ? 'Erstellt' : `Fehler: ${r.error}`}</td>
|
||
<td className="py-2 px-3">
|
||
{r.temporaryPassword ? (
|
||
<code className="bg-gray-100 px-2 py-1 rounded">{r.temporaryPassword}</code>
|
||
) : (
|
||
'—'
|
||
)}
|
||
</td>
|
||
<td className="py-2 px-3 text-right">
|
||
{r.userId && r.temporaryPassword && (
|
||
<div className="flex justify-end gap-2">
|
||
<button
|
||
className="btn-secondary h-9 px-3"
|
||
onClick={() => navigator.clipboard.writeText(r.temporaryPassword!)}
|
||
>
|
||
Kopieren
|
||
</button>
|
||
<button
|
||
className="btn-primary h-9 px-3"
|
||
onClick={() => sendTempPasswordEmail(r.userId!, r.temporaryPassword!)}
|
||
>
|
||
E-Mail senden
|
||
</button>
|
||
</div>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<div className="space-y-3">
|
||
<p className="text-body text-secondary">Nur Administratoren: Löscht alle Benutzer und behält nur 'admin' und die angegebene E‑Mail.</p>
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
className="input-field w-80"
|
||
placeholder="E‑Mail, die erhalten bleiben soll"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
/>
|
||
<button className="btn-danger" onClick={runPurge} disabled={busy}>
|
||
Bereinigen
|
||
</button>
|
||
</div>
|
||
{msg && <div className="text-secondary">{msg}</div>}
|
||
</div>
|
||
)
|
||
}
|