import { For, Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js'; import type { CrudRecord } from '~/lib/admin/types'; const API = ''; const ACTIONS = ['View', 'Create', 'Update', 'Delete'] as const; const STATIC_MODULES = [ 'Department Management', 'Designation Management', 'Internal Role Management', 'Employee Management', 'External Role Management', 'Internal Dashboard Management', 'External Dashboard Management', 'Verification Management', 'Approval Management', 'Users Management', 'Company Management', 'Candidate Management', 'Customer Management', 'Photographer Management', 'Makeup Artist Management', 'Tutor Management', 'Developer Management', 'Fitness Trainer Management', 'Graphic Designer Management', 'Social Media Management', 'Video Editor Management', 'Catering Services Management', 'Jobs Management', 'Leads Management', 'Applications Management', 'Responses Management', 'Review Management', 'Pricing Management', 'Credit Management', 'Coupon Management', 'Discount Management', 'Tax Management', 'Order Management', 'Invoice Management', 'Ledger Management', 'Knowledge Base Management', 'Support Management', 'Report Management', 'Notifications', ] as const; function formatRoleKey(input: string): string { return input .trim() .toUpperCase() .replace(/[^A-Z0-9]+/g, '_') .replace(/^_+|_+$/g, '') .replace(/_{2,}/g, '_'); } type RoleRecord = CrudRecord & { key?: string; department?: string; departmentId?: string; description?: string; usersAssigned?: number; permissionsCount?: number; canApproveRequests?: boolean; canManageSystemSettings?: boolean; createdDate?: string; }; type DepartmentOption = { id: string; name: string }; type Permission = { key: string; module: string; action: string }; function makeKey(module: string, action: string) { return `${module.replace(/ /g, '_').toLowerCase()}:${action.toLowerCase()}`; } async function loadPermissions(): Promise { try { const res = await fetch(`${API}/api/admin/permissions`); if (!res.ok) throw new Error(); const data = await res.json(); return Array.isArray(data) ? data : []; } catch { return STATIC_MODULES.flatMap((module) => ACTIONS.map((action) => ({ key: makeKey(module, action), module, action, })), ); } } function normalizeRole(item: any, idx: number): RoleRecord { return { id: String(item.id ?? `role-${idx + 1}`), name: String(item.name ?? ''), key: item.key ? String(item.key) : undefined, department: item.department_name ? String(item.department_name) : undefined, departmentId: item.department_id ? String(item.department_id) : undefined, description: item.description ? String(item.description) : undefined, usersAssigned: Number(item.users_assigned ?? 0), permissionsCount: Number(item.permissions_count ?? 0), canApproveRequests: Boolean(item.can_approve_requests ?? false), canManageSystemSettings: Boolean(item.can_manage_system_settings ?? false), status: item.is_active === false ? 'INACTIVE' : 'ACTIVE', updatedAt: String(item.updated_at ?? item.created_at ?? ''), createdDate: String(item.created_at ?? ''), }; } function StatusBadge(props: { status: string }) { const active = () => props.status === 'ACTIVE'; return ( {active() ? 'Active' : 'Inactive'} ); } function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string }) { return ( ); } export default function RoleManagementPage() { const [view, setView] = createSignal<'list' | 'form' | 'detail'>('list'); const [permissions] = createResource(loadPermissions); const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all'); const [formTab, setFormTab] = createSignal<'general' | 'permissions' | 'settings'>('general'); const [detailTab, setDetailTab] = createSignal<'permissions' | 'users' | 'logs'>('permissions'); const [search, setSearch] = createSignal(''); const [rows, setRows] = createSignal([]); const [viewingRole, setViewingRole] = createSignal(null); const [viewingPermissions, setViewingPermissions] = createSignal([]); const [editingId, setEditingId] = createSignal(null); const [openMenuId, setOpenMenuId] = createSignal(null); const [isLoading, setIsLoading] = createSignal(false); const [error, setError] = createSignal(''); const [statusFilter, setStatusFilter] = createSignal<'all' | 'ACTIVE' | 'INACTIVE'>('all'); const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'users_desc' | 'users_asc'>('name_asc'); const [sortMenuOpen, setSortMenuOpen] = createSignal(false); const [filterMenuOpen, setFilterMenuOpen] = createSignal(false); // Form state const [name, setName] = createSignal(''); const [roleKey, setRoleKey] = createSignal(''); const [description, setDescription] = createSignal(''); const [departmentId, setDepartmentId] = createSignal(''); const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE'); const [canApproveRequests, setCanApproveRequests] = createSignal(false); const [canManageSystemSettings, setCanManageSystemSettings] = createSignal(false); const [selectedPermissions, setSelectedPermissions] = createSignal>(new Set()); const [isSaving, setIsSaving] = createSignal(false); const [formError, setFormError] = createSignal(''); const [departments, setDepartments] = createSignal([]); const isViewingSuperAdmin = createMemo(() => (viewingRole()?.key || '').toUpperCase() === 'SUPER_ADMIN'); const load = async () => { setIsLoading(true); setError(''); try { const params = new URLSearchParams({ audience: 'INTERNAL', per_page: '100', q: search().trim() }); const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const res = await fetch(`${API}/api/admin/roles?${params}`, { headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', }); if (!res.ok) throw new Error(`Request failed (${res.status})`); const payload = await res.json().catch(() => null); const list: any[] = Array.isArray(payload) ? payload : Array.isArray(payload?.roles) ? payload.roles : []; setRows(list.map(normalizeRole)); } catch (err: any) { setError(err?.message || 'Could not reach roles API.'); setRows([]); } finally { setIsLoading(false); } }; const loadDepartments = async () => { try { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const res = await fetch(`${API}/api/admin/departments?per_page=100`, { headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', }); if (!res.ok) return; const payload = await res.json().catch(() => null); const list: any[] = Array.isArray(payload) ? payload : Array.isArray(payload?.departments) ? payload.departments : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : []; setDepartments(list.map((d: any) => ({ id: String(d.id), name: String(d.name) }))); } catch { /* dropdown just empty */ } }; onMount(() => { void load(); void loadDepartments(); }); const filteredRows = createMemo(() => { const q = search().toLowerCase(); let list = rows(); if (statusFilter() !== 'all') { list = list.filter((r) => r.status === statusFilter()); } if (q) { list = list.filter(r => r.name.toLowerCase().includes(q) || (r.key || '').toLowerCase().includes(q) ); } const sorted = [...list]; sorted.sort((a, b) => { if (sortBy() === 'name_desc') return b.name.localeCompare(a.name); if (sortBy() === 'users_desc') return Number(b.usersAssigned || 0) - Number(a.usersAssigned || 0); if (sortBy() === 'users_asc') return Number(a.usersAssigned || 0) - Number(b.usersAssigned || 0); return a.name.localeCompare(b.name); }); return sorted; }); const exportCsv = () => { const headers = ['Role Name', 'Role Key', 'Department', 'Users', 'Permissions', 'Status']; const rowsData = filteredRows().map((row) => [ row.name || '', row.key || '', row.department || '', String(row.usersAssigned ?? 0), (row.key || '').toUpperCase() === 'SUPER_ADMIN' ? 'All' : String(row.permissionsCount ?? 0), row.status || '', ]); const csv = [headers, ...rowsData] .map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `internal-roles-${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const permissionKeyByModuleAction = createMemo(() => { const map = new Map(); const src = permissions() ?? []; src.forEach((p) => { const moduleKey = String(p.module || '').trim().toUpperCase(); const actionKey = String(p.action || '').trim().toUpperCase(); if (!moduleKey || !actionKey) return; map.set(`${moduleKey}::${actionKey}`, String(p.key || '').trim()); }); return map; }); const orderedModules = createMemo(() => { const fromApi = Array.from( new Set((permissions() ?? []).map((p) => String(p.module || '').trim()).filter(Boolean)), ); const ordered = [...STATIC_MODULES.filter((m) => fromApi.includes(m))]; const extras = fromApi.filter((m) => !ordered.includes(m)).sort(); return [...ordered, ...extras]; }); const permissionKeyFor = (module: string, action: string) => permissionKeyByModuleAction().get(`${module.toUpperCase()}::${action.toUpperCase()}`) || ''; const togglePermission = (key: string) => { setSelectedPermissions(prev => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); }; const resetForm = () => { setEditingId(null); setName(''); setRoleKey(''); setDescription(''); setDepartmentId(''); setStatus('ACTIVE'); setCanApproveRequests(false); setCanManageSystemSettings(false); setSelectedPermissions(new Set()); setFormTab('general'); setFormError(''); }; const openCreate = () => { resetForm(); setView('form'); }; const openEdit = (row: RoleRecord) => { setEditingId(row.id); setName(row.name); setRoleKey(row.key || ''); setDescription(row.description || ''); setDepartmentId(row.departmentId || ''); setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE'); setCanApproveRequests(Boolean(row.canApproveRequests)); setCanManageSystemSettings(Boolean(row.canManageSystemSettings)); setSelectedPermissions(new Set()); setFormTab('general'); setView('form'); setOpenMenuId(null); // Fetch permission_keys for this role fetch(`${API}/api/admin/roles/${row.id}`).then(r => r.json()).then((detail) => { if (Array.isArray(detail?.permission_keys)) { const keys = (detail.permission_keys as any[]).map((k) => String(k)); setSelectedPermissions(new Set(keys)); } }).catch(() => {}); }; const openDetail = async (row: RoleRecord) => { setViewingRole(row); setView('detail'); setListTab('view'); setOpenMenuId(null); setViewingPermissions([]); try { const res = await fetch(`${API}/api/admin/roles/${row.id}`); if (res.ok) { const detail = await res.json(); setViewingPermissions(Array.isArray(detail?.permission_keys) ? (detail.permission_keys as any[]).map((k: any) => String(k)) : []); } } catch { /* ignore */ } }; const save = async () => { if (isSaving()) return; const normalizedRoleKey = editingId() ? formatRoleKey(roleKey()) : formatRoleKey(name()); if (!name().trim() || !normalizedRoleKey) { setFormError('Role name and role key are required.'); setFormTab('general'); return; } setIsSaving(true); setFormError(''); try { const accessToken = typeof sessionStorage !== 'undefined' ? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim() : ''; const isCreate = !editingId(); const endpoint = isCreate ? `${API}/api/admin/roles` : `${API}/api/admin/roles/${editingId()}`; const method = isCreate ? 'POST' : 'PATCH'; const body: Record = { name: name().trim(), description: description().trim() || null, is_active: status() === 'ACTIVE', can_approve_requests: canApproveRequests(), can_manage_system_settings: canManageSystemSettings(), permission_keys: Array.from(selectedPermissions()) as string[], }; if (departmentId().trim()) body.department_id = departmentId().trim(); if (isCreate) { body.key = normalizedRoleKey; body.audience = 'INTERNAL'; } const res = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', body: JSON.stringify(body), }); const raw = await res.text(); let message = ''; if (raw) { try { const parsed = JSON.parse(raw) as { message?: string; error?: string }; message = parsed?.message || parsed?.error || ''; } catch { message = raw; } } if (!res.ok) throw new Error(message || `Request failed (${res.status})`); setView('list'); resetForm(); await load(); } catch (err: any) { setFormError(String(err?.message || '').trim() || 'Failed to save role.'); } finally { setIsSaving(false); } }; createEffect(() => { if (editingId()) return; setRoleKey(formatRoleKey(name())); }); const deleteRole = async (id: string, roleName: string) => { if (!window.confirm(`Delete role "${roleName}"?`)) return; setOpenMenuId(null); try { const accessToken = typeof sessionStorage !== 'undefined' ? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim() : ''; const res = await fetch(`${API}/api/admin/roles/${id}`, { method: 'DELETE', headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', }); if (!res.ok) throw new Error(`Request failed (${res.status})`); await load(); } catch (err: any) { setError(err?.message || 'Failed to delete role.'); } }; const toggleStatus = async (row: RoleRecord) => { setOpenMenuId(null); try { const accessToken = typeof sessionStorage !== 'undefined' ? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim() : ''; const res = await fetch(`${API}/api/admin/roles/${row.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', body: JSON.stringify({ is_active: row.status !== 'ACTIVE' }), }); if (!res.ok) throw new Error(`Request failed (${res.status})`); await load(); } catch (err: any) { setError(err?.message || 'Failed to update status.'); } }; return (

Internal Role Management

Define and manage organizational access levels with granular permission control

{/* ── LIST VIEW ── */}
{([ { key: 'all', label: 'All Roles', action: () => { setListTab('all'); void load(); } }, { key: 'create', label: 'Create Role', action: () => { setListTab('create'); openCreate(); } }, { key: 'view', label: 'View Role', action: () => setListTab('view') }, ] as const).map((tab) => ( ))}
{error()}
{ setSearch(e.currentTarget.value); void load(); }} placeholder="Search roles..." style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none" />
{([ ['name_asc', 'Name (A-Z)'], ['name_desc', 'Name (Z-A)'], ['users_desc', 'Users (High-Low)'], ['users_asc', 'Users (Low-High)'], ] as const).map(([key, label]) => ( ))}
{([ ['all', 'All Status'], ['ACTIVE', 'Active'], ['INACTIVE', 'Inactive'], ] as const).map(([key, label]) => ( ))}
{['Role Name', 'Role Key', 'Department', 'Users', 'Permissions', 'Status', 'Actions'].map(h => ( ))} 0} fallback={ } > {(row) => ( )}
{h}

No internal roles found

Create your first role to get started.

}>

Loading...

{row.name}

{row.description}

{row.key || '—'} {row.department || '—'} {Number(row.usersAssigned || 0)} users {(row.key || '').toUpperCase() === 'SUPER_ADMIN' ? 'All' : Number(row.permissionsCount || 0)}
{/* ── FORM VIEW ── */}
{(['general', 'permissions', 'settings'] as const).map((tab, i) => { const labels = ['General Information', 'Module Access', 'Role Settings']; const active = () => formTab() === tab; return ( ); })}
{formError()}
{/* General */}