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 { 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Search, Loader2, UserCog, Shield, Key, Eye, ChevronRight, Users, } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { rolesService } from "@/services/roles.service"; import type { UserWithRoles, UserRoleDetail, Role, Permission } from "@/types/role-permission"; const ROLE_COLORS = [ "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300", "bg-violet-100 text-violet-700 dark:bg-violet-900 dark:text-violet-300", "bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300", "bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300", "bg-rose-100 text-rose-700 dark:bg-rose-900 dark:text-rose-300", "bg-cyan-100 text-cyan-700 dark:bg-cyan-900 dark:text-cyan-300", "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300", ]; function roleColor(idx: number) { return ROLE_COLORS[idx % ROLE_COLORS.length]; } function initials(name: string) { return name.split(" ").map(w => w[0]).join("").toUpperCase().slice(0, 2); } const CATEGORY_BADGE: Record = { exam: "bg-violet-100 text-violet-700", user: "bg-blue-100 text-blue-700", entity: "bg-emerald-100 text-emerald-700", assignment: "bg-amber-100 text-amber-700", classroom: "bg-rose-100 text-rose-700", payment: "bg-cyan-100 text-cyan-700", report: "bg-orange-100 text-orange-700", }; function catBadge(cat: string) { return CATEGORY_BADGE[cat.toLowerCase()] ?? "bg-gray-100 text-gray-700"; } export default function UserRoles() { const { toast } = useToast(); const qc = useQueryClient(); const [search, setSearch] = useState(""); const [roleFilter, setRoleFilter] = useState("all"); const [editOpen, setEditOpen] = useState(false); const [editUserId, setEditUserId] = useState(null); const [editRoleIds, setEditRoleIds] = useState>(new Set()); const [viewOpen, setViewOpen] = useState(false); const [viewUserId, setViewUserId] = useState(null); const usersQ = useQuery({ queryKey: ["users-with-roles", search, roleFilter], queryFn: () => rolesService.listUsersWithRoles({ ...(search ? { search } : {}), ...(roleFilter !== "all" ? { role_id: Number(roleFilter) } : {}), size: 200, }), }); const rolesQ = useQuery({ queryKey: ["roles"], queryFn: () => rolesService.listRoles() }); const viewQ = useQuery({ queryKey: ["user-roles-detail", viewUserId], queryFn: () => rolesService.getUserRoles(viewUserId!), enabled: viewUserId !== null, }); const users: UserWithRoles[] = usersQ.data?.data ?? []; const allRoles: Role[] = rolesQ.data?.data ?? []; const saveRolesMut = useMutation({ mutationFn: ({ userId, roleIds }: { userId: number; roleIds: number[] }) => rolesService.updateUserRoles(userId, roleIds), onSuccess: () => { qc.invalidateQueries({ queryKey: ["users-with-roles"] }); qc.invalidateQueries({ queryKey: ["roles"] }); setEditOpen(false); toast({ title: "Roles updated successfully" }); }, onError: () => toast({ title: "Failed to update roles", variant: "destructive" }), }); const loading = usersQ.isLoading || rolesQ.isLoading; const totalWithRoles = users.filter(u => u.role_ids.length > 0).length; const totalWithoutRoles = users.filter(u => u.role_ids.length === 0).length; const openEditRoles = (u: UserWithRoles) => { setEditUserId(u.id); setEditRoleIds(new Set(u.role_ids)); setEditOpen(true); }; const openViewPerms = (u: UserWithRoles) => { setViewUserId(u.id); setViewOpen(true); }; const toggleRole = (rid: number) => { setEditRoleIds(prev => { const s = new Set(prev); if (s.has(rid)) s.delete(rid); else s.add(rid); return s; }); }; const handleSave = () => { if (editUserId !== null) { saveRolesMut.mutate({ userId: editUserId, roleIds: Array.from(editRoleIds) }); } }; const viewDetail: UserRoleDetail | undefined = viewQ.data; const effectivePerms: Permission[] = viewDetail?.effective_permissions ?? []; const permsByCategory = effectivePerms.reduce>((acc, p) => { const cat = p.category || "General"; (acc[cat] ??= []).push(p); return acc; }, {}); if (loading) { return
; } return (
{/* Header */}

User Roles

Assign roles to users and view their effective permissions.

{/* Summary */}

{users.length}

Total Users

{totalWithRoles}

With Roles

{totalWithoutRoles}

No Roles

{allRoles.length}

Available Roles

{/* Filters */}
setSearch(e.target.value)} />
{/* Users Table */} User Email / Login Assigned Roles Permissions Actions {users.length === 0 && ( No users found. )} {users.map(u => (
{initials(u.name)} {u.name}
{u.email || u.login}
{u.roles.length === 0 && No roles} {u.roles.map((r, i) => ( {r.name} ))}
{u.effective_permission_count}
View effective permissions Manage role assignments
))}
{/* ── Assign Roles Dialog ── */} { setEditOpen(o); if (!o) setEditUserId(null); }}> Assign Roles Toggle roles for user: {users.find(u => u.id === editUserId)?.name}
{allRoles.length === 0 &&

No roles available. Create roles first.

} {allRoles.map((r, i) => ( ))}
{editRoleIds.size} of {allRoles.length} selected
{/* ── View Effective Permissions Dialog ── */} { setViewOpen(o); if (!o) setViewUserId(null); }}> Effective Permissions {viewDetail && ( {viewDetail.user.name} ({viewDetail.user.login}) )} {viewQ.isLoading ? (
) : viewDetail ? (
{/* Assigned roles summary */}

Assigned Roles

{viewDetail.roles.filter(r => r.assigned).map((r, i) => ( {r.name} ({r.permission_count}) ))} {viewDetail.roles.filter(r => r.assigned).length === 0 && ( No roles assigned )}
{/* Permissions grouped by category */}

Effective Permissions ({effectivePerms.length})

{effectivePerms.length === 0 ? (

No permissions (assign roles to grant permissions)

) : (
{Object.entries(permsByCategory).map(([cat, perms]) => (
{cat} {perms.length}
{perms.map(p => (
{p.code} {p.name}
))}
))}
)}
) : null}
); }