'use client'; import { useEffect, useMemo, useState, type FormEvent } from 'react'; import { Shield, Plus, Search, Trash2, X, RefreshCw, Check, Lock, ChevronRight, Info, AlertTriangle, Loader2, } from 'lucide-react'; import { toast } from 'sonner'; import { rolePermissionService, RoleResponse, PermissionResponse } from '@/services/api/rolePermissionService'; import { SlideOver } from '@/components/ui/SlideOver'; import { TablePagination, useClientPagination } from '@/components/ui/TablePagination'; export default function RolesPage() { const [roles, setRoles] = useState([]); const [permissions, setPermissions] = useState([]); const [selectedRole, setSelectedRole] = useState(null); const [selectedRolePerms, setSelectedRolePerms] = useState([]); const [loading, setLoading] = useState(true); const [savingPerms, setSavingPerms] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [showAddModal, setShowAddModal] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [newRoleName, setNewRoleName] = useState(''); const [newRolePrefix, setNewRolePrefix] = useState(''); const [newRoleDesc, setNewRoleDesc] = useState(''); const [deleteTarget, setDeleteTarget] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const fetchData = async () => { setLoading(true); try { const [fetchedRoles, fetchedPermissions] = await Promise.all([ rolePermissionService.getRoles(), rolePermissionService.getPermissions(), ]); setRoles(fetchedRoles); setPermissions(fetchedPermissions); const defaultRole = fetchedRoles.find((r) => r.role_name.toLowerCase() !== 'super admin') || fetchedRoles[0]; if (defaultRole) { await selectRole(defaultRole, fetchedPermissions); } } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to load roles and permissions data'; toast.error(message); } finally { setLoading(false); } }; useEffect(() => { fetchData(); }, []); const selectRole = async (role: RoleResponse, allPerms: PermissionResponse[] = permissions) => { setSelectedRole(role); if (role.role_name.toLowerCase() === 'super admin') { setSelectedRolePerms(allPerms.map((p) => p.permission_id)); return; } try { const assignedIds = await rolePermissionService.getRolePermissions(role.role_id); setSelectedRolePerms(assignedIds); } catch { toast.error(`Failed to fetch permissions for ${role.role_name}`); } }; const handleAddRole = async (e: FormEvent) => { e.preventDefault(); if (!newRoleName.trim()) { toast.error('Role name is required'); return; } if (!newRolePrefix.trim() || newRolePrefix.length > 5) { toast.error('Role prefix is required (max 5 letters)'); return; } try { setIsSubmitting(true); const created = await rolePermissionService.createRole({ role_name: newRoleName.trim(), role_prefix: newRolePrefix.trim().toUpperCase(), description: newRoleDesc.trim() || undefined, }); setRoles([...roles, created]); toast.success(`Role "${created.role_name}" created successfully`); setNewRoleName(''); setNewRolePrefix(''); setNewRoleDesc(''); setShowAddModal(false); await selectRole(created); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to create role'; toast.error(message); } finally { setIsSubmitting(false); } }; const handleDeleteRole = async () => { if (!deleteTarget) return; try { setIsDeleting(true); await rolePermissionService.deleteRole(deleteTarget.role_id); toast.success(`Role "${deleteTarget.role_name}" deleted`); setRoles(roles.filter((r) => r.role_id !== deleteTarget.role_id)); if (selectedRole?.role_id === deleteTarget.role_id) { setSelectedRole(null); setSelectedRolePerms([]); } setDeleteTarget(null); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to delete role'; toast.error(message); } finally { setIsDeleting(false); } }; const handleTogglePermission = (permissionId: string) => { if (!selectedRole || selectedRole.role_name.toLowerCase() === 'super admin') return; if (selectedRolePerms.includes(permissionId)) { setSelectedRolePerms(selectedRolePerms.filter((id) => id !== permissionId)); } else { setSelectedRolePerms([...selectedRolePerms, permissionId]); } }; const handleSavePermissions = async () => { if (!selectedRole) return; setSavingPerms(true); try { await rolePermissionService.updateRolePermissions(selectedRole.role_id, selectedRolePerms); toast.success(`Permissions for "${selectedRole.role_name}" updated successfully`); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to update role permissions'; toast.error(message); } finally { setSavingPerms(false); } }; const permissionsByModule = useMemo(() => { const groups: Record = {}; permissions.forEach((p) => { const moduleName = p.module.charAt(0).toUpperCase() + p.module.slice(1); if (!groups[moduleName]) groups[moduleName] = []; groups[moduleName].push(p); }); return groups; }, [permissions]); const filteredRoles = useMemo(() => { return roles.filter((r) => r.role_name.toLowerCase().includes(searchQuery.toLowerCase())); }, [roles, searchQuery]); const pager = useClientPagination(filteredRoles); const isSuperAdmin = selectedRole?.role_name.toLowerCase() === 'super admin'; const dataCardShell = 'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0'; const inputClass = 'w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary'; const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5'; const primaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50'; const secondaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50'; return (

Roles & Permissions

{roles.length}
setSearchQuery(e.target.value)} className="w-full h-9 pl-8 pr-3 crm-radius-control border border-border bg-card text-[13px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition-colors" />
{loading ? ( ) : filteredRoles.length === 0 ? ( ) : ( pager.items.map((role) => { const active = selectedRole?.role_id === role.role_id; return ( selectRole(role)} className={`border-b border-border hover:bg-muted/10 cursor-pointer transition-colors ${ active ? 'bg-primary/5' : '' }`} > ); }) )}
Role Actions

Loading roles...

{searchQuery ? `No roles match “${searchQuery}”` : 'No roles yet'}
{role.role_name} {role.is_system && ( System )}

Prefix: {role.role_prefix} {role.description ? ` · ${role.description}` : ''}

e.stopPropagation()}>
{!role.is_system && ( )}
{selectedRole ? (

{selectedRole.role_name}

{selectedRole.description || 'Manage access levels for this role.'}

{!isSuperAdmin && ( )}
{isSuperAdmin && (

Full access

Super Admin has access to every module. Permissions cannot be edited.

)}
{Object.keys(permissionsByModule).map((groupName) => (

{groupName}

{permissionsByModule[groupName].map((perm) => { const isChecked = selectedRolePerms.includes(perm.permission_id); return ( ); })}
))}
) : (

Select a role to manage its permissions.

)}
!isSubmitting && setShowAddModal(false)} title="Add role" icon={} >
setNewRoleName(e.target.value)} className={inputClass} />
setNewRolePrefix(e.target.value)} className={inputClass} />

Used for employee codes. Max 5 letters.