feat: complete user management system
- Add UserProfile role field (superadmin/admin/trainer) - Create role-based permission classes - Implement UserManagementViewSet with CRUD - Add frontend types and components - Create users management page - Add sidebar navigation for users - Only superadmins can manage users
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useAuth } from "@/lib/auth"
|
||||
import { apiFetch, IUser, ICreateUserInput, IUpdateUserInput } from "@/lib/api"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Plus, Pencil, Trash2, Key } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { UserForm } from "@/components/users/user-form"
|
||||
import { FadeIn } from "@/components/animations"
|
||||
|
||||
const roleColors: Record<string, string> = {
|
||||
superadmin: 'bg-red-100 text-red-800',
|
||||
admin: 'bg-blue-100 text-blue-800',
|
||||
trainer: 'bg-green-100 text-green-800',
|
||||
}
|
||||
|
||||
const roleLabels: Record<string, string> = {
|
||||
superadmin: 'Super Admin',
|
||||
admin: 'Admin',
|
||||
trainer: 'Trainer',
|
||||
}
|
||||
|
||||
export default function UsersPage() {
|
||||
const { token } = useAuth()
|
||||
const [users, setUsers] = useState<IUser[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isFormOpen, setIsFormOpen] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState<IUser | null>(null)
|
||||
const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const data = await apiFetch<IUser[]>('/auth/users/', { token: token! })
|
||||
setUsers(data)
|
||||
} catch {
|
||||
toast.error('Fehler beim Laden der Benutzer')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
}, [token])
|
||||
|
||||
const handleCreate = async (data: ICreateUserInput) => {
|
||||
try {
|
||||
await apiFetch<IUser>('/auth/users/', {
|
||||
method: 'POST',
|
||||
token: token!,
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
toast.success('Benutzer erstellt')
|
||||
fetchUsers()
|
||||
} catch {
|
||||
toast.error('Fehler beim Erstellen')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async (data: IUpdateUserInput) => {
|
||||
if (!editingUser) return
|
||||
try {
|
||||
await apiFetch<IUser>(`/auth/users/${editingUser.id}/`, {
|
||||
method: 'PATCH',
|
||||
token: token!,
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
toast.success('Benutzer aktualisiert')
|
||||
fetchUsers()
|
||||
} catch {
|
||||
toast.error('Fehler beim Aktualisieren')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Bist du sicher?')) return
|
||||
try {
|
||||
await apiFetch(`/auth/users/${id}/`, {
|
||||
method: 'DELETE',
|
||||
token: token!,
|
||||
})
|
||||
toast.success('Benutzer gelöscht')
|
||||
fetchUsers()
|
||||
} catch {
|
||||
toast.error('Fehler beim Löschen')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasswordReset = async (user: IUser) => {
|
||||
const password = prompt(`Neues Passwort für ${user.username}:`)
|
||||
if (!password) return
|
||||
try {
|
||||
await apiFetch(`/auth/users/${user.id}/set_password/`, {
|
||||
method: 'POST',
|
||||
token: token!,
|
||||
body: JSON.stringify({ password }),
|
||||
})
|
||||
toast.success('Passwort geändert')
|
||||
} catch {
|
||||
toast.error('Fehler beim Passwort ändern')
|
||||
}
|
||||
}
|
||||
|
||||
const openCreateForm = () => {
|
||||
setEditingUser(null)
|
||||
setFormMode('create')
|
||||
setIsFormOpen(true)
|
||||
}
|
||||
|
||||
const openEditForm = (user: IUser) => {
|
||||
setEditingUser(user)
|
||||
setFormMode('edit')
|
||||
setIsFormOpen(true)
|
||||
}
|
||||
|
||||
if (loading) return <div>Laden...</div>
|
||||
|
||||
return (
|
||||
<FadeIn>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Benutzerverwaltung</CardTitle>
|
||||
<Button onClick={openCreateForm}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Neuer Benutzer
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Username</TableHead>
|
||||
<TableHead>E-Mail</TableHead>
|
||||
<TableHead>Rolle</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Aktionen</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
{user.first_name} {user.last_name}
|
||||
</TableCell>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={roleColors[user.role || 'trainer'] || 'bg-gray-100'}>
|
||||
{roleLabels[user.role || 'trainer'] || user.role}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.is_active ? (
|
||||
<span className="text-green-600">Aktiv</span>
|
||||
) : (
|
||||
<span className="text-red-600">Inaktiv</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handlePasswordReset(user)}
|
||||
title="Passwort ändern"
|
||||
>
|
||||
<Key className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => openEditForm(user)}
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(user.id!)}
|
||||
className="text-red-600"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<UserForm
|
||||
open={isFormOpen}
|
||||
onOpenChange={setIsFormOpen}
|
||||
onSubmit={formMode === 'create' ? handleCreate : handleUpdate}
|
||||
user={editingUser || undefined}
|
||||
mode={formMode}
|
||||
/>
|
||||
</FadeIn>
|
||||
)
|
||||
}
|
||||
@@ -100,6 +100,17 @@ export function Sidebar() {
|
||||
Einstellungen
|
||||
</motion.div>
|
||||
</Link>
|
||||
<Link href="/settings/users" className="block mb-2">
|
||||
<motion.div
|
||||
className="flex items-center gap-3 px-3 pl-8 py-1 rounded-lg text-xs text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
whileHover={{ x: 2 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
Benutzer
|
||||
</motion.div>
|
||||
</Link>
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<motion.span
|
||||
className="text-sm text-sidebar-foreground"
|
||||
|
||||
Reference in New Issue
Block a user