import { useEffect, useMemo, useState } from "react" import { useNavigate } from "react-router-dom" import { Plus, Search, Shield, ShieldCheck, ChevronLeft, ChevronRight, Loader2, AlertCircle, Eye, X, Pencil, Check, } 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 { Textarea } from "../../components/ui/textarea" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "../../components/ui/dialog" import { getRoles, getRoleDetail, getAllPermissions, setRolePermissions, updateRole } 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) // Role info editing state const [editingRole, setEditingRole] = useState(false) const [editName, setEditName] = useState("") const [editDescription, setEditDescription] = useState("") const [savingRole, setSavingRole] = useState(false) // Permissions editing state const [editingPermissions, setEditingPermissions] = useState(false) const [allPermissionsMap, setAllPermissionsMap] = useState>({}) const [permLoading, setPermLoading] = useState(false) const [selectedPermissionIds, setSelectedPermissionIds] = useState>(new Set()) const [permSearch, setPermSearch] = useState("") const [savingPermissions, setSavingPermissions] = 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) } } // Enter role info edit mode const handleEditRole = () => { if (!selectedRole) return setEditName(selectedRole.name) setEditDescription(selectedRole.description) setEditingRole(true) } const handleCancelEditRole = () => { setEditingRole(false) } const handleSaveRole = async () => { if (!selectedRole || !editName.trim()) return setSavingRole(true) try { await updateRole(selectedRole.id, { name: editName.trim(), description: editDescription.trim(), }) const res = await getRoleDetail(selectedRole.id) setSelectedRole(res.data.data) setEditingRole(false) toast.success("Role updated successfully.") } catch (err: unknown) { const message = (err as { response?: { data?: { message?: string } } })?.response?.data?.message ?? "Failed to update role." toast.error(message) } finally { setSavingRole(false) } } // Enter edit mode – fetch all permissions const handleEditPermissions = async () => { setEditingPermissions(true) setPermSearch("") setSelectedPermissionIds(new Set(selectedRole?.permissions.map((p) => p.id) ?? [])) if (Object.keys(allPermissionsMap).length === 0) { setPermLoading(true) try { const res = await getAllPermissions() setAllPermissionsMap(res.data.data ?? {}) } catch { toast.error("Failed to load permissions.") setEditingPermissions(false) } finally { setPermLoading(false) } } } const handleCancelEdit = () => { setEditingPermissions(false) setPermSearch("") } const togglePermission = (id: number) => { setSelectedPermissionIds((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const toggleGroup = (perms: RolePermission[]) => { const allSelected = perms.every((p) => selectedPermissionIds.has(p.id)) setSelectedPermissionIds((prev) => { const next = new Set(prev) for (const p of perms) { if (allSelected) next.delete(p.id) else next.add(p.id) } return next }) } const handleSavePermissions = async () => { if (!selectedRole) return setSavingPermissions(true) try { await setRolePermissions(selectedRole.id, { permission_ids: Array.from(selectedPermissionIds), }) // Refresh role detail const res = await getRoleDetail(selectedRole.id) setSelectedRole(res.data.data) setEditingPermissions(false) toast.success("Permissions updated successfully.") } catch { toast.error("Failed to update permissions.") } finally { setSavingPermissions(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]) // Filtered permission groups for edit mode const editPermissionGroups = useMemo(() => { const q = permSearch.toLowerCase() const entries: [string, RolePermission[]][] = [] for (const [groupName, perms] of Object.entries(allPermissionsMap)) { const filtered = q ? perms.filter( (p) => p.name.toLowerCase().includes(q) || p.key.toLowerCase().includes(q) || groupName.toLowerCase().includes(q), ) : perms if (filtered.length > 0) entries.push([groupName, filtered]) } return entries.sort(([a], [b]) => a.localeCompare(b)) }, [allPermissionsMap, permSearch]) 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 */} { setDetailOpen(open) if (!open) { setEditingPermissions(false) setEditingRole(false) setPermSearch("") } }}> {!editingRole ? ( <> {selectedRole?.is_system ? ( ) : ( )} {selectedRole?.name ?? "Role Details"} {selectedRole && ( )} {selectedRole?.description} ) : ( <> Edit Role Update the role name and description.
setEditName(e.target.value)} placeholder="e.g. CONTENT_MANAGER" />