import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Plus, Search, Trash2, Pencil, Shield, Key, Users, Loader2, ChevronRight, } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { rolesService } from "@/services/roles.service"; import type { Role, Permission, RoleCreateRequest, PermissionCreateRequest } from "@/types/role-permission"; const CATEGORY_COLORS: Record = { exam: "bg-violet-100 text-violet-700 dark:bg-violet-900 dark:text-violet-300", user: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300", entity: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300", assignment: "bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300", classroom: "bg-rose-100 text-rose-700 dark:bg-rose-900 dark:text-rose-300", payment: "bg-cyan-100 text-cyan-700 dark:bg-cyan-900 dark:text-cyan-300", report: "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300", }; function categoryBadgeClass(cat: string) { return CATEGORY_COLORS[cat.toLowerCase()] ?? "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"; } export default function RolesPermissions() { const { toast } = useToast(); const qc = useQueryClient(); const [search, setSearch] = useState(""); const [tab, setTab] = useState("roles"); const [roleDialogOpen, setRoleDialogOpen] = useState(false); const [editingRole, setEditingRole] = useState(null); const [roleForm, setRoleForm] = useState({ name: "", description: "" }); const [permDialogOpen, setPermDialogOpen] = useState(false); const [permForm, setPermForm] = useState({ name: "", code: "", description: "", category: "" }); const [permAssignOpen, setPermAssignOpen] = useState(false); const [assigningRole, setAssigningRole] = useState(null); const [selectedPermIds, setSelectedPermIds] = useState>(new Set()); const rolesQ = useQuery({ queryKey: ["roles", search], queryFn: () => rolesService.listRoles(search ? { search } : undefined) }); const permsQ = useQuery({ queryKey: ["permissions"], queryFn: () => rolesService.listPermissions() }); const roles: Role[] = rolesQ.data?.data ?? []; const permissions: Permission[] = permsQ.data?.data ?? []; const createRoleMut = useMutation({ mutationFn: (d: RoleCreateRequest) => rolesService.createRole(d), onSuccess: () => { qc.invalidateQueries({ queryKey: ["roles"] }); setRoleDialogOpen(false); toast({ title: "Role created" }); }, onError: () => toast({ title: "Failed to create role", variant: "destructive" }), }); const updateRoleMut = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => rolesService.updateRole(id, data), onSuccess: () => { qc.invalidateQueries({ queryKey: ["roles"] }); setRoleDialogOpen(false); setEditingRole(null); toast({ title: "Role updated" }); }, onError: () => toast({ title: "Failed to update role", variant: "destructive" }), }); const deleteRoleMut = useMutation({ mutationFn: rolesService.deleteRole, onSuccess: () => { qc.invalidateQueries({ queryKey: ["roles"] }); toast({ title: "Role deleted" }); }, onError: () => toast({ title: "Failed to delete role", variant: "destructive" }), }); const assignPermsMut = useMutation({ mutationFn: ({ id, permIds }: { id: number; permIds: number[] }) => rolesService.updateRolePermissions(id, permIds), onSuccess: () => { qc.invalidateQueries({ queryKey: ["roles"] }); setPermAssignOpen(false); toast({ title: "Permissions updated" }); }, onError: () => toast({ title: "Failed to update permissions", variant: "destructive" }), }); const createPermMut = useMutation({ mutationFn: (d: PermissionCreateRequest) => rolesService.createPermission(d), onSuccess: () => { qc.invalidateQueries({ queryKey: ["permissions"] }); setPermDialogOpen(false); toast({ title: "Permission created" }); }, onError: () => toast({ title: "Failed to create permission", variant: "destructive" }), }); const deletePermMut = useMutation({ mutationFn: rolesService.deletePermission, onSuccess: () => { qc.invalidateQueries({ queryKey: ["permissions"] }); toast({ title: "Permission deleted" }); }, onError: () => toast({ title: "Failed to delete permission", variant: "destructive" }), }); const loading = rolesQ.isLoading || permsQ.isLoading; const openNewRole = () => { setEditingRole(null); setRoleForm({ name: "", description: "" }); setRoleDialogOpen(true); }; const openEditRole = (r: Role) => { setEditingRole(r); setRoleForm({ name: r.name, description: r.description }); setRoleDialogOpen(true); }; const openAssignPerms = (r: Role) => { setAssigningRole(r); setSelectedPermIds(new Set(r.permission_ids)); setPermAssignOpen(true); }; const openNewPerm = () => { setPermForm({ name: "", code: "", description: "", category: "" }); setPermDialogOpen(true); }; const handleSaveRole = () => { if (!roleForm.name.trim()) return; if (editingRole) { updateRoleMut.mutate({ id: editingRole.id, data: roleForm }); } else { createRoleMut.mutate(roleForm); } }; const togglePerm = (pid: number) => { setSelectedPermIds(prev => { const s = new Set(prev); if (s.has(pid)) s.delete(pid); else s.add(pid); return s; }); }; const categorizedPerms = permissions.reduce>((acc, p) => { const cat = p.category || "General"; (acc[cat] ??= []).push(p); return acc; }, {}); const filteredPerms = search ? permissions.filter(p => p.name.toLowerCase().includes(search.toLowerCase()) || p.code.toLowerCase().includes(search.toLowerCase())) : permissions; if (loading) { return
; } return (
{/* Header */}

Roles & Permissions

Manage platform roles and their permission assignments.

{tab === "roles" ? ( ) : ( )}
{/* Summary Cards */}

{roles.length}

Total Roles

{permissions.length}

Total Permissions

{Object.keys(categorizedPerms).length}

Permission Categories

{/* Tabs */}
Roles ({roles.length}) Permissions ({permissions.length})
setSearch(e.target.value)} />
{/* ── Roles Tab ── */} {roles.length === 0 ? ( No roles configured yet. Create one to get started. ) : (
{roles.map(r => (
{r.name} {r.user_count} user{r.user_count !== 1 ? "s" : ""} assigned
Edit Delete
{r.description &&

{r.description}

}
{r.permissions.slice(0, 5).map(p => ( {p.code} ))} {r.permissions.length > 5 && +{r.permissions.length - 5}} {r.permissions.length === 0 && No permissions}
))}
)}
{/* ── Permissions Tab ── */} {Object.entries(categorizedPerms).length === 0 ? ( No permissions defined yet. ) : (
{Object.entries(categorizedPerms).map(([cat, perms]) => { const visible = search ? perms.filter(p => filteredPerms.includes(p)) : perms; if (visible.length === 0) return null; return ( {cat} {visible.length} permission{visible.length !== 1 ? "s" : ""} Code Name Description Actions {visible.map(p => ( {p.code} {p.name} {p.description || "—"} ))}
); })}
)}
{/* ── Create / Edit Role Dialog ── */} { setRoleDialogOpen(o); if (!o) setEditingRole(null); }}> {editingRole ? "Edit Role" : "Create New Role"}
setRoleForm(f => ({ ...f, name: e.target.value }))} />