import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; import AdminShell from '~/components/AdminShell'; import type { CrudRecord } from '~/lib/admin/types'; const API = '/api/gateway'; 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 }; // Permission matrix: each module maps to CRUD permission keys const MODULE_PERMISSIONS = [ { module: 'Employee Management', prefix: 'EMPLOYEES' }, { module: 'Department Management', prefix: 'DEPARTMENTS' }, { module: 'Designation Management', prefix: 'DESIGNATIONS' }, { module: 'Role Management', prefix: 'ROLES' }, { module: 'Verification Management', prefix: 'VERIFICATIONS' }, { module: 'Approval Management', prefix: 'APPROVALS' }, { module: 'Users Management', prefix: 'USERS' }, { module: 'Company Management', prefix: 'COMPANIES' }, ]; const ACTIONS = ['VIEW', 'CREATE', 'UPDATE', 'DELETE'] as const; function permKey(prefix: string, action: string) { return `${prefix}_${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 ( {props.label}{props.required && *} props.onInput(e.currentTarget.value)} placeholder={props.placeholder} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box" /> ); } export default function RoleManagementPage() { const [view, setView] = createSignal<'list' | 'form' | 'detail'>('list'); 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(''); // 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 load = async () => { setIsLoading(true); setError(''); try { const params = new URLSearchParams({ audience: 'INTERNAL', per_page: '100', q: search().trim() }); const res = await fetch(`${API}/api/admin/roles?${params}`); 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 res = await fetch(`${API}/api/admin/departments?per_page=100`); if (!res.ok) return; const payload = await res.json().catch(() => null); const list: any[] = Array.isArray(payload?.departments) ? payload.departments : []; 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(); if (!q) return rows(); return rows().filter(r => r.name.toLowerCase().includes(q) || (r.key || '').toLowerCase().includes(q) ); }); 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)) { setSelectedPermissions(new Set(detail.permission_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 : []); } } catch { /* ignore */ } }; const save = async () => { if (!name().trim() || !roleKey().trim()) { setFormError('Role name and role key are required.'); setFormTab('general'); return; } setIsSaving(true); setFormError(''); try { 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()), }; if (departmentId().trim()) body.department_id = departmentId().trim(); if (isCreate) { body.key = roleKey().trim(); body.audience = 'INTERNAL'; } const res = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error((data as any).message || `Request failed (${res.status})`); } setView('list'); resetForm(); await load(); } catch (err: any) { setFormError(err?.message || 'Failed to save role.'); } finally { setIsSaving(false); } }; const deleteRole = async (id: string, roleName: string) => { if (!window.confirm(`Delete role "${roleName}"?`)) return; setOpenMenuId(null); try { const res = await fetch(`${API}/api/admin/roles/${id}`, { method: 'DELETE' }); 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 res = await fetch(`${API}/api/admin/roles/${row.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, 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) => ( {tab.label} ))} {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" /> Export {['Role Name', 'Role Key', 'Department', 'Users', 'Permissions', 'Status', 'Actions'].map(h => ( {h} ))} 0} fallback={ No internal roles found Create your first role to get started. Create Role > }> Loading... } > {(row) => ( {row.name} {row.description} {row.key || '—'} {row.department || '—'} {Number(row.usersAssigned || 0)} users {Number(row.permissionsCount || 0)} setOpenMenuId(openMenuId() === row.id ? null : row.id)} style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#9CA3AF;background:none;border:none;cursor:pointer"> void openDetail(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left"> View Role openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left"> Edit Role void toggleStatus(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left"> {row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'} void deleteRole(row.id, row.name)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left"> Delete Role )} {/* ── FORM VIEW ── */} { setView('list'); resetForm(); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">All Roles {editingId() ? 'Edit Role' : 'Create Role'} {(['general', 'permissions', 'settings'] as const).map((tab, i) => { const labels = ['General Information', 'Module Access', 'Role Settings']; const active = () => formTab() === tab; return ( setFormTab(tab)} style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}> {labels[i]} ); })} {formError()} {/* General */} Department setDepartmentId(e.currentTarget.value)} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box;appearance:none" > Select department {(d) => {d.name}} Status {(['ACTIVE', 'INACTIVE'] as const).map((s) => ( setStatus(s)} style={`flex:1;height:40px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? '#FF5E13' : '#E5E7EB'};background:${status() === s ? '#FFF3EE' : 'white'};color:${status() === s ? '#FF5E13' : '#6B7280'}`} > {s === 'ACTIVE' ? 'Active' : 'Inactive'} ))} Description setDescription(e.currentTarget.value)} placeholder="Brief description of this role's responsibilities..." rows="3" style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit" /> {/* Permissions */} Select the module access permissions for this role. Module {(action) => ( {action.charAt(0) + action.slice(1).toLowerCase()} )} {(mp) => ( {mp.module} {(action) => { const pk = permKey(mp.prefix, action); return ( togglePermission(pk)} style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer" /> ); }} )} {/* Settings */} Can Approve Requests Users with this role can make final decisions in Approval Management. setCanApproveRequests(v => !v)} style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canApproveRequests() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`} > Can Manage System Settings Users with this role can access and modify system-level configurations. setCanManageSystemSettings(v => !v)} style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canManageSystemSettings() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`} > { setView('list'); resetForm(); }} style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Cancel void save()} disabled={isSaving()} style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer"> {isSaving() ? 'Saving...' : editingId() ? 'Update Role' : 'Create Role'} {/* ── DETAIL VIEW ── */} { setView('list'); setListTab('all'); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">All Roles View Role {viewingRole()!.name} Key: {viewingRole()!.key || '—'} {viewingRole()!.department ? ` • ${viewingRole()!.department}` : ''} {` • ${Number(viewingRole()!.usersAssigned || 0)} users assigned`} {viewingRole()!.description} openEdit(viewingRole()!)} style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Edit Role {(['permissions', 'users', 'logs'] as const).map((tab, i) => { const labels = ['Permissions', 'Assigned Users', 'Activity Logs']; const active = () => detailTab() === tab; return ( setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}> {labels[i]} ); })} Module {(action) => ( {action.charAt(0) + action.slice(1).toLowerCase()} )} {(mp) => ( {mp.module} {(action) => { const pk = permKey(mp.prefix, action); const has = () => viewingPermissions().includes(pk); return ( ); }} )} Can Approve Requests: {viewingRole()!.canApproveRequests ? 'Yes' : 'No'} Can Manage Settings: {viewingRole()!.canManageSystemSettings ? 'Yes' : 'No'} {Number(viewingRole()!.usersAssigned || 0)} users assigned Detailed user list will be available when employee management is connected. Activity logs will appear here. { setView('list'); setListTab('all'); }} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List ); }
Define and manage organizational access levels with granular permission control
No internal roles found
Create your first role to get started.
Loading...
{row.name}
{row.description}
Select the module access permissions for this role.
Can Approve Requests
Users with this role can make final decisions in Approval Management.
Can Manage System Settings
Users with this role can access and modify system-level configurations.
Key: {viewingRole()!.key || '—'} {viewingRole()!.department ? ` • ${viewingRole()!.department}` : ''} {` • ${Number(viewingRole()!.usersAssigned || 0)} users assigned`}
{viewingRole()!.key || '—'}
{viewingRole()!.description}
Can Approve Requests:
Can Manage Settings:
{Number(viewingRole()!.usersAssigned || 0)} users assigned
Detailed user list will be available when employee management is connected.
Activity logs will appear here.