"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { FormEvent } from "react"; import { Edit, Eye, Shield, Trash2 } from "lucide-react"; import ConfirmDialog from "@/components/common/ConfirmDialog"; import DataTable, { type DataTableColumn } from "@/components/common/DataTable"; import SearchInput from "@/components/common/SearchInput"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/lib/api"; import { hasPermission } from "@/lib/permissions"; import type { CurrentUser, Permission, Role, RolePayload } from "@/types/rbac"; const pageSize = 10; function getErrorMessage(error: unknown) { if (typeof error === "object" && error !== null && "response" in error) { const response = (error as { response?: { data?: { detail?: string } } }).response; return response?.data?.detail ?? "Aktion konnte nicht abgeschlossen werden"; } return "Aktion konnte nicht abgeschlossen werden"; } function formatDate(value: string) { return new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }).format(new Date(value)); } function getSortableValue(role: Role, key: string) { const value = role[key as keyof Role]; return typeof value === "string" ? value.toLowerCase() : value; } export default function RolesPage() { const [roles, setRoles] = useState([]); const [permissions, setPermissions] = useState([]); const [currentUser, setCurrentUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [formError, setFormError] = useState(""); const [deleteError, setDeleteError] = useState(""); const [pending, setPending] = useState(false); const [search, setSearch] = useState(""); const [sortKey, setSortKey] = useState("name"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [page, setPage] = useState(1); const [selectedRole, setSelectedRole] = useState(null); const [detailsRole, setDetailsRole] = useState(null); const [deleteRole, setDeleteRole] = useState(null); const [formOpen, setFormOpen] = useState(false); const loadRoles = useCallback(async () => { try { const [rolesResponse, permissionsResponse, meResponse] = await Promise.all([ api.get("/roles"), api.get("/permissions"), api.get("/me"), ]); setRoles(rolesResponse.data); setPermissions(permissionsResponse.data); setCurrentUser(meResponse.data); } catch (err) { setError(getErrorMessage(err)); } finally { setLoading(false); } }, []); useEffect(() => { queueMicrotask(() => { void loadRoles(); }); }, [loadRoles]); const filteredRoles = useMemo(() => { const term = search.trim().toLowerCase(); return roles .filter((role) => { if (!term) { return true; } return [ role.name, role.display_name, role.description, ].some((value) => value.toLowerCase().includes(term)); }) .sort((first, second) => { const firstValue = getSortableValue(first, sortKey); const secondValue = getSortableValue(second, sortKey); if (firstValue === secondValue) { return 0; } const result = firstValue > secondValue ? 1 : -1; return sortDirection === "asc" ? result : -result; }); }, [roles, search, sortDirection, sortKey]); const pageCount = Math.max(1, Math.ceil(filteredRoles.length / pageSize)); const currentPage = Math.min(page, pageCount); const pageRoles = filteredRoles.slice( (currentPage - 1) * pageSize, currentPage * pageSize, ); const columns: DataTableColumn[] = [ { key: "display_name", label: "Rolle", sortable: true, render: (role) => (

{role.display_name}

{role.name}

), }, { key: "description", label: "Beschreibung", sortable: true, render: (role) => role.description || "-", }, { key: "is_system", label: "Typ", sortable: true, render: (role) => ( {role.is_system ? "Systemrolle" : "Individuell"} ), }, { key: "permissions", label: "Berechtigungen", render: (role) => `${role.permissions.length}`, }, { key: "created_at", label: "Erstellt", sortable: true, render: (role) => formatDate(role.created_at), }, { key: "actions", label: "Aktionen", className: "px-4 py-3 text-right", render: (role) => (
{hasPermission(currentUser, "roles.update") && ( )} {hasPermission(currentUser, "roles.delete") && !role.is_system && ( )}
), }, ]; function handleSort(key: string) { if (sortKey === key) { setSortDirection((current) => (current === "asc" ? "desc" : "asc")); return; } setSortKey(key); setSortDirection("asc"); } function openCreateDialog() { setSelectedRole(null); setFormError(""); setFormOpen(true); } function openEditDialog(role: Role) { setSelectedRole(role); setFormError(""); setFormOpen(true); } async function saveRole(payload: RolePayload) { setPending(true); setFormError(""); try { if (selectedRole) { const response = await api.put(`/roles/${selectedRole.id}`, payload); setRoles((current) => current.map((role) => ( role.id === selectedRole.id ? response.data : role ))); } else { const response = await api.post("/roles", payload); setRoles((current) => [response.data, ...current]); } setFormOpen(false); } catch (err) { setFormError(getErrorMessage(err)); } finally { setPending(false); } } async function confirmDelete() { if (!deleteRole) { return; } setPending(true); setDeleteError(""); try { await api.delete(`/roles/${deleteRole.id}`); setRoles((current) => current.filter((role) => role.id !== deleteRole.id)); setDeleteRole(null); } catch (err) { setDeleteError(getErrorMessage(err)); } finally { setPending(false); } } return (

Rollen & Rechte

{filteredRoles.length} von {roles.length} Rollen

{hasPermission(currentUser, "roles.create") && ( )}
{ setSearch(value); setPage(1); }} placeholder="Rollen suchen" />
role.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Rollen gefunden" emptyDescription="Passe Suche oder Filter an oder erstelle eine neue Rolle." onSort={handleSort} />
Seite {currentPage} von {pageCount}
{ if (!open) { setDetailsRole(null); } }} /> { if (!open) { setDeleteRole(null); setDeleteError(""); } }} onConfirm={confirmDelete} > {deleteRole && (
Name
{deleteRole.display_name}
Systemrolle
{deleteRole.is_system ? "Ja" : "Nein"}
{deleteError &&

{deleteError}

}
)}
); } function RoleFormDialog({ open, role, permissions, pending, serverError, onOpenChange, onSubmit, }: { open: boolean; role: Role | null; permissions: Permission[]; pending: boolean; serverError?: string; onOpenChange: (open: boolean) => void; onSubmit: (payload: RolePayload) => Promise; }) { const initialPermissionIds = role?.permissions.map((permission) => permission.id) ?? []; const [name, setName] = useState(role?.name ?? ""); const [displayName, setDisplayName] = useState(role?.display_name ?? ""); const [description, setDescription] = useState(role?.description ?? ""); const [permissionIds, setPermissionIds] = useState(initialPermissionIds); if (!open) { return null; } const groupedPermissions = permissions.reduce>((groups, permission) => { groups[permission.module] = groups[permission.module] ?? []; groups[permission.module].push(permission); return groups; }, {}); const valid = name.trim().length >= 3 && displayName.trim().length >= 2; function togglePermission(permissionId: number) { setPermissionIds((current) => ( current.includes(permissionId) ? current.filter((id) => id !== permissionId) : [...current, permissionId] )); } async function handleSubmit(event: FormEvent) { event.preventDefault(); if (!valid) { return; } await onSubmit({ name, display_name: displayName, description, permission_ids: permissionIds, }); } return (
{role ? "Rolle bearbeiten" : "Rolle erstellen"} Rollenstammdaten und Berechtigungen verwalten.
setName(event.target.value)} disabled={role?.is_system} />
setDisplayName(event.target.value)} />
setDescription(event.target.value)} />
{Object.entries(groupedPermissions).map(([module, modulePermissions]) => (

{module}

{modulePermissions.map((permission) => ( ))}
))}
{serverError &&

{serverError}

}
); } function RoleDetailDialog({ role, onOpenChange, }: { role: Role | null; onOpenChange: (open: boolean) => void; }) { return ( {role && ( <> {role.display_name} {role.description || role.name}

Technischer Name

{role.name}

Typ

{role.is_system ? "Systemrolle" : "Individuell"}

Berechtigungen

{role.permissions.map((permission) => ( {permission.name} ))}
)}
); }