Files
erp-system/app/users/page.tsx
T
2026-05-20 18:58:23 +00:00

235 lines
10 KiB
TypeScript

// /opt/erp-system/app/users/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { Users, Plus, X, Mail, ShieldAlert, Edit2, Trash2 } from 'lucide-react';
import { useToast } from '../components/ToastProvider';
import { useSession } from 'next-auth/react';
export default function UsersPage() {
const [users, setUsers] = useState<any[]>([]);
const [roles, setRoles] = useState<any[]>([]);
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const { toast, confirm } = useToast();
const { data: session } = useSession();
const permissions = (session?.user as any)?.permissions || [];
const canDelete = permissions.includes('DATA_DELETE');
const [formData, setFormData] = useState({
firstName: '', lastName: '', email: '', password: '', roleId: ''
});
useEffect(() => {
fetchRoles();
fetchUsers();
}, []);
const fetchUsers = async () => {
const res = await fetch('/api/users');
if (res.ok) setUsers(await res.json());
};
const fetchRoles = async () => {
const res = await fetch('/api/roles');
if (res.ok) {
const data = await res.json();
setRoles(data);
if (data.length > 0 && !editingId) {
setFormData(prev => ({ ...prev, roleId: data[0].id.toString() }));
}
}
};
const handleCreateNew = () => {
setEditingId(null);
setFormData({
firstName: '', lastName: '', email: '', password: '',
roleId: roles.length > 0 ? roles[0].id.toString() : ''
});
setShowForm(!showForm);
};
const handleEdit = (user: any) => {
setEditingId(user.id);
setFormData({
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
password: '', // Passwort wird aus Sicherheitsgründen nie geladen
roleId: user.roleId ? user.roleId.toString() : (roles[0]?.id.toString() || '')
});
setShowForm(true);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const method = editingId ? 'PUT' : 'POST';
const payload = editingId ? { id: editingId, ...formData } : formData;
const res = await fetch('/api/users', {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (res.ok) {
setShowForm(false);
fetchUsers();
toast(editingId ? 'Mitarbeiter erfolgreich aktualisiert' : 'Mitarbeiter erfolgreich angelegt', 'success');
} else {
const data = await res.json();
toast(data.error || 'Fehler beim Speichern', 'error');
}
};
const handleDelete = async (user: any) => {
const isConfirmed = await confirm({
title: 'Mitarbeiter löschen',
message: `"${user.firstName} ${user.lastName}" wirklich löschen? Zugewiesene Tickets werden freigestellt.`,
danger: true
});
if (!isConfirmed) return;
const res = await fetch(`/api/users?id=${user.id}`, { method: 'DELETE' });
if (res.ok) {
toast('Mitarbeiter gelöscht', 'success');
fetchUsers();
} else {
const data = await res.json();
toast(data.error || 'Fehler beim Löschen', 'error');
}
};
return (
<div className="max-w-7xl mx-auto space-y-6 animate-fade-in-up">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<Users className="w-6 h-6 text-indigo-600" /> Team
</h1>
<p className="text-slate-500 mt-1">Verwalte Systemzugänge und weise Berechtigungsgruppen zu.</p>
</div>
<button
onClick={handleCreateNew}
className="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition flex items-center gap-2 font-medium shadow-sm"
>
{showForm ? <X className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
{showForm ? 'Abbrechen' : 'Neuer Mitarbeiter'}
</button>
</div>
{showForm && (
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 animate-in fade-in slide-in-from-top-4 duration-200">
<h2 className="text-lg font-semibold text-slate-800 mb-4">
{editingId ? 'Mitarbeiter bearbeiten' : 'Zugang anlegen'}
</h2>
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Vorname *</label>
<input type="text" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={formData.firstName} onChange={e => setFormData({...formData, firstName: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Nachname *</label>
<input type="text" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={formData.lastName} onChange={e => setFormData({...formData, lastName: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">E-Mail (Login) *</label>
<input type="email" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={formData.email} onChange={e => setFormData({...formData, email: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{editingId ? 'Neues Passwort (optional)' : 'Initiales Passwort *'}
</label>
<input
type="password"
required={!editingId}
minLength={6}
placeholder={editingId ? 'Leer lassen, um es nicht zu ändern' : ''}
className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all"
value={formData.password}
onChange={e => setFormData({...formData, password: e.target.value})}
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Berechtigungsgruppe *</label>
<select required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none bg-white transition-all" value={formData.roleId} onChange={e => setFormData({...formData, roleId: e.target.value})}>
{roles.map(r => (
<option key={r.id} value={r.id}>{r.name}</option>
))}
</select>
</div>
<div className="md:col-span-2 flex justify-end mt-2 gap-3">
<button type="button" onClick={() => setShowForm(false)} className="px-6 py-2.5 rounded-lg text-slate-600 hover:bg-slate-100 font-medium">Abbrechen</button>
<button type="submit" className="bg-slate-900 text-white px-6 py-2.5 rounded-lg hover:bg-slate-800 transition font-medium">
{editingId ? 'Änderungen speichern' : 'Zugang erstellen'}
</button>
</div>
</form>
</div>
)}
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden">
<table className="w-full text-left text-sm">
<thead className="bg-slate-50 text-slate-600 font-medium border-b border-slate-200">
<tr>
<th className="py-4 px-6">Mitarbeiter</th>
<th className="py-4 px-6">E-Mail</th>
<th className="py-4 px-6">Gruppe</th>
<th className="py-4 px-6">Angelegt am</th>
<th className="py-4 px-6 text-right">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{users.map((u) => (
<tr key={u.id} className="hover:bg-slate-50/80 transition-colors border-b border-slate-50 last:border-0 group">
<td className="py-4 px-6 font-semibold text-slate-900">{u.firstName} {u.lastName}</td>
<td className="py-4 px-6 text-slate-600">
<span className="flex items-center gap-2"><Mail className="w-4 h-4 text-slate-400" /> {u.email}</span>
</td>
<td className="py-4 px-6">
{u.role ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-slate-100 text-slate-700 border border-slate-200">
<ShieldAlert className="w-3 h-3 text-slate-500" /> {u.role.name}
</span>
) : (
<span className="text-red-500 text-xs font-medium">Ohne Rechte</span>
)}
</td>
<td className="py-4 px-6 text-slate-500">
{new Date(u.createdAt).toLocaleDateString('de-DE')}
</td>
<td className="py-4 px-6 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={() => handleEdit(u)}
className="p-2 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-lg transition-all inline-flex items-center"
title="Mitarbeiter bearbeiten"
>
<Edit2 className="w-4 h-4" />
</button>
{canDelete && (
<button
onClick={() => handleDelete(u)}
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all inline-flex items-center"
title="Mitarbeiter löschen"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
))}
{users.length === 0 && (
<tr><td colSpan={5} className="py-8 text-center text-slate-500">Keine Mitarbeiter gefunden.</td></tr>
)}
</tbody>
</table>
</div>
</div>
);
}