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
|
Einstellungen
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Link>
|
</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">
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
<motion.span
|
<motion.span
|
||||||
className="text-sm text-sidebar-foreground"
|
className="text-sm text-sidebar-foreground"
|
||||||
|
|||||||
Reference in New Issue
Block a user