diff --git a/src/routes/admin/roles/index.tsx b/src/routes/admin/roles/index.tsx index 1ef4bcd..af2d0b3 100644 --- a/src/routes/admin/roles/index.tsx +++ b/src/routes/admin/roles/index.tsx @@ -1,29 +1,57 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; -import { useSearchParams } from '@solidjs/router'; import AdminShell from '~/components/AdminShell'; import type { CrudRecord } from '~/lib/admin/types'; const API = '/api/gateway'; type RoleRecord = CrudRecord & { - code?: string; + key?: string; department?: string; + departmentId?: string; + description?: string; usersAssigned?: number; permissionsCount?: number; - status: 'ACTIVE' | 'INACTIVE'; + canApproveRequests?: boolean; + canManageSystemSettings?: boolean; createdDate?: string; }; -const FALLBACK_ROLES: RoleRecord[] = [ - { id: 'r1', name: 'System Administrator', code: 'ADM-SYS', department: 'IT', usersAssigned: 12, permissionsCount: 150, status: 'ACTIVE', createdDate: '2026-01-12' }, - { id: 'r2', name: 'HR Manager', code: 'HR-MGR', department: 'HR', usersAssigned: 4, permissionsCount: 45, status: 'ACTIVE', createdDate: '2026-02-05' }, - { id: 'r3', name: 'Finance Controller', code: 'FIN-CON', department: 'Finance', usersAssigned: 2, permissionsCount: 60, status: 'INACTIVE', createdDate: '2026-03-18' }, -]; +type DepartmentOption = { id: string; name: string }; -const MODULES = [ - 'Employee Management', 'Department Management', 'Designation Management', 'Internal Role Management', - 'Verification Management', 'Approval Management', 'Users Management', 'Company Management' +// 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'; @@ -57,47 +85,196 @@ export default function RoleManagementPage() { 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 [code, setCode] = createSignal(''); - const [dept, setDept] = createSignal(''); - const [desc, setDesc] = 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 () => { - setRows(FALLBACK_ROLES); + 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); + } }; - onMount(() => void load()); + 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.code || '').toLowerCase().includes(q)); + 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(''); setCode(''); setDept(''); setDesc(''); setFormTab('general'); + 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); setCode(row.code || ''); - setDept(row.department || ''); setView('form'); setOpenMenuId(null); + 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 = (row: RoleRecord) => { + + 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

@@ -107,7 +284,7 @@ export default function RoleManagementPage() {
{([ - { key: 'all', label: 'All Roles', action: () => setListTab('all') }, + { 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) => ( @@ -121,15 +298,18 @@ export default function RoleManagementPage() { ))}
-
+ +
{error()}
+
+ +
setSearch(e.currentTarget.value)} + onInput={(e) => { 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" /> -
@@ -137,35 +317,74 @@ export default function RoleManagementPage() { - {['Role Name', 'Role Code', 'Department', 'Users', 'Status', 'Actions'].map(h => ( + {['Role Name', 'Role Key', 'Department', 'Users', 'Permissions', 'Status', 'Actions'].map(h => ( ))} - - {(row) => ( - - - - - - - + - )} - + } + > + + {(row) => ( + + + + + + + + + + )} + +
{h}
{row.name}{row.code || '—'}{row.department || '—'}{row.usersAssigned} users - - -
- - - -
+ 0} + fallback={ +
+ +

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{Number(row.permissionsCount || 0)} + + +
+ + + +
+ +
+ +
@@ -175,7 +394,7 @@ export default function RoleManagementPage() { {/* ── FORM VIEW ── */}
- +
@@ -194,60 +413,139 @@ export default function RoleManagementPage() {
+ +
{formError()}
+
+ + {/* General */} -
- - - -
-
- - -
- - - - - {['View', 'Create', 'Update', 'Delete'].map(p => ( - +
+
+ + +
+
+ +
- - - - {(mod) => ( - - - {[1, 2, 3, 4].map(() => ( - - ))} - - )} - - -
Module{p}
{mod} - -
+
+ +
+