import { useEffect, useMemo, useState } from "react" import { useNavigate } from "react-router-dom" import { Plus, Search, Shield, ShieldCheck, ChevronLeft, ChevronRight, Loader2, AlertCircle, Eye, X, } from "lucide-react" import { Button } from "../../components/ui/button" import { Card, CardContent } from "../../components/ui/card" import { Badge } from "../../components/ui/badge" import { Input } from "../../components/ui/input" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "../../components/ui/dialog" import { getRoles, getRoleDetail } from "../../api/rbac.api" import type { Role, RoleDetail, RolePermission } from "../../types/rbac.types" import { cn } from "../../lib/utils" import { toast } from "sonner" export function RolesListPage() { const navigate = useNavigate() // List state const [roles, setRoles] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize] = useState(20) const [query, setQuery] = useState("") const [debouncedQuery, setDebouncedQuery] = useState("") const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Detail modal state const [selectedRole, setSelectedRole] = useState(null) const [detailOpen, setDetailOpen] = useState(false) const [detailLoading, setDetailLoading] = useState(false) // Debounce search query useEffect(() => { const timer = setTimeout(() => { setDebouncedQuery(query) setPage(1) }, 400) return () => clearTimeout(timer) }, [query]) // Fetch roles useEffect(() => { const fetchRoles = async () => { setLoading(true) setError(null) try { const res = await getRoles({ query: debouncedQuery || undefined, page, page_size: pageSize, }) setRoles(res.data.data.roles ?? []) setTotal(res.data.data.total ?? 0) } catch { setError("Failed to load roles.") } finally { setLoading(false) } } fetchRoles() }, [debouncedQuery, page, pageSize]) // Open role detail const handleViewRole = async (roleId: number) => { setDetailOpen(true) setDetailLoading(true) setSelectedRole(null) try { const res = await getRoleDetail(roleId) setSelectedRole(res.data.data) } catch { toast.error("Failed to load role details.") setDetailOpen(false) } finally { setDetailLoading(false) } } // Group permissions by group_name const permissionGroups = useMemo(() => { if (!selectedRole?.permissions) return [] const map = new Map() for (const p of selectedRole.permissions) { const group = map.get(p.group_name) ?? [] group.push(p) map.set(p.group_name, group) } return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b)) }, [selectedRole]) const totalPages = Math.max(1, Math.ceil(total / pageSize)) return (
{/* Header */}

Role Management

Manage roles and their permissions.

{/* Search */}
setQuery(e.target.value)} placeholder="Search roles…" className="pl-9" /> {query && ( )}
{/* Error */} {error && (

{error}

)} {/* Loading */} {loading && (
)} {/* Roles grid */} {!loading && !error && ( <> {roles.length === 0 ? (

No roles found.

{debouncedQuery ? `No roles match "${debouncedQuery}".` : "Create a new role to get started."}

) : (
{roles.map((role) => (
{role.is_system ? ( ) : ( )}

{role.name}

{role.description}

{role.is_system && ( System )}
Created {new Date(role.created_at).toLocaleDateString()}
))}
)} {/* Pagination */} {totalPages > 1 && (

Showing {(page - 1) * pageSize + 1}–{Math.min(page * pageSize, total)} of {total} roles

{page} / {totalPages}
)} )} {/* Role detail dialog */} {selectedRole?.is_system ? ( ) : ( )} {selectedRole?.name ?? "Role Details"} {selectedRole?.description} {detailLoading && (
)} {!detailLoading && selectedRole && (
{/* Meta row */}
{selectedRole.is_system && ( System Role )} Created {new Date(selectedRole.created_at).toLocaleDateString()} {selectedRole.permissions.length} permission{selectedRole.permissions.length !== 1 ? "s" : ""}
{/* Permissions grouped */}

Permissions

{permissionGroups.length === 0 ? (

No permissions assigned.

) : (
{permissionGroups.map(([groupName, perms]) => (

{groupName}

{perms.map((p) => ( {p.name} ))}
))}
)}
)}
) }