'use client'; import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { type ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2, Plus, Lock } from 'lucide-react'; import { DataTable } from '@/components/shared/data-table'; import { PageHeader } from '@/components/shared/page-header'; import { ConfirmationDialog } from '@/components/shared/confirmation-dialog'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { apiFetch } from '@/lib/api/client'; import { formatRole } from '@/lib/constants'; import { RoleForm } from './role-form'; interface Role { id: string; name: string; description: string | null; isSystem: boolean; isGlobal: boolean; permissions: Record>; createdAt: string; } const ROLES_QUERY_KEY = ['admin', 'roles'] as const; export function RoleList() { const queryClient = useQueryClient(); const [formOpen, setFormOpen] = useState(false); const [editingRole, setEditingRole] = useState(null); const [viewingPermissions, setViewingPermissions] = useState(null); const { data: roles = [], isLoading: loading } = useQuery({ queryKey: ROLES_QUERY_KEY, queryFn: () => apiFetch<{ data: Role[] }>('/api/v1/admin/roles').then((r) => r.data), }); const deleteMutation = useMutation({ mutationFn: (id: string) => apiFetch(`/api/v1/admin/roles/${id}`, { method: 'DELETE' }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ROLES_QUERY_KEY }), }); const fetchRoles = () => queryClient.invalidateQueries({ queryKey: ROLES_QUERY_KEY }); function handleNewRole() { setEditingRole(null); setFormOpen(true); } function handleEditRole(role: Role) { setEditingRole(role); setFormOpen(true); } function countPermissions(perms: Record>): string { let granted = 0; let total = 0; for (const group of Object.values(perms)) { for (const val of Object.values(group)) { total++; if (val) granted++; } } return `${granted}/${total}`; } const columns: ColumnDef[] = [ { accessorKey: 'name', header: 'Name', cell: ({ row }) => (
{/* Display-normalize: snake_case → "Snake Case" so admin- created roles with arbitrary keys still read cleanly. The underlying name is stored verbatim and is what code checks against — display is purely cosmetic. */} {formatRole(row.original.name)} {row.original.isSystem && ( System )}
), }, { accessorKey: 'description', header: 'Description', cell: ({ row }) => ( {row.original.description ?? '-'} ), }, { id: 'permissions', header: 'Permissions', cell: ({ row }) => ( ), }, { id: 'actions', header: '', cell: ({ row }) => (
{!row.original.isSystem && ( Delete } title="Delete Role" description={`Delete "${row.original.name}"? Users assigned to this role must be reassigned first.`} confirmLabel="Delete" onConfirm={() => deleteMutation.mutate(row.original.id)} loading={deleteMutation.isPending && deleteMutation.variables === row.original.id} /> )}
), enableSorting: false, size: 80, }, ]; return (
New Role } /> row.id} cardRender={({ original }) => (

{formatRole(original.name)}

{original.isSystem ? ( System ) : null}
{original.description ? (

{original.description}

) : null}
{!original.isSystem ? ( } title="Delete Role" description={`Delete "${original.name}"? Users assigned to this role must be reassigned first.`} confirmLabel="Delete" onConfirm={() => deleteMutation.mutate(original.id)} loading={deleteMutation.isPending && deleteMutation.variables === original.id} /> ) : null}
)} emptyState={

No roles defined.

} /> {/* Permissions inspector — opens when admin clicks the count badge in the table. Lists granted vs denied per resource so they can spot gaps before opening the editor. */} !o && setViewingPermissions(null)}> Permissions — {viewingPermissions ? formatRole(viewingPermissions.name) : ''} Granted vs total per resource. Click Edit to change. {viewingPermissions && (
{Object.entries(viewingPermissions.permissions).map(([resource, actions]) => { const granted = Object.values(actions).filter(Boolean).length; const total = Object.keys(actions).length; return (
{resource.replace(/_/g, ' ')} {granted}/{total}
{Object.entries(actions).map(([action, allowed]) => ( {action.replace(/_/g, ' ')} ))}
); })}
)} {viewingPermissions && ( )}
); }