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 DepartmentRecord = CrudRecord & { code?: string; description?: string; totalEmployees?: number; createdDate?: string; departmentHead?: string; departmentEmail?: string; transfersEnabled?: boolean; visibility?: 'INTERNAL' | 'EXTERNAL'; }; const permissionGroups = [ { title: 'Employee Management', items: ['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees'] }, { title: 'Role Management', items: ['View Roles', 'Assign Roles'] }, { title: 'Department Settings', items: ['Manage Department Settings'] }, ]; type DepartmentListResponse = { departments?: any[]; data?: any[]; items?: any[]; }; function normalizeDepartment(item: any, idx: number): DepartmentRecord { const status = String(item.status ?? '').toUpperCase(); const isActive = typeof item.is_active === 'boolean' ? item.is_active : status ? status === 'ACTIVE' : true; return { id: String(item.id ?? `dep-${idx + 1}`), name: String(item.name ?? ''), code: item.code ? String(item.code) : undefined, description: item.description ? String(item.description) : undefined, totalEmployees: Number(item.total_employees ?? 0), departmentHead: item.department_head ? String(item.department_head) : undefined, departmentEmail: item.department_email ? String(item.department_email) : undefined, transfersEnabled: Boolean(item.transfers_enabled ?? false), visibility: String(item.visibility ?? 'INTERNAL').toUpperCase() === 'EXTERNAL' ? 'EXTERNAL' : 'INTERNAL', status: isActive ? 'ACTIVE' : 'INACTIVE', updatedAt: String(item.updated_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; type?: 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 DepartmentManagementPage() { const [searchParams] = useSearchParams(); const isPreview = () => searchParams._preview === '1'; const [view, setView] = createSignal<'list' | 'form'>('list'); const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general'); const [listTab, setListTab] = createSignal<'all' | 'create' | 'view' | 'inactive'>('all'); const [search, setSearch] = createSignal(''); const [statusFilter, setStatusFilter] = createSignal('all'); const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'employees_desc' | 'employees_asc'>('name_asc'); const [sortMenuOpen, setSortMenuOpen] = createSignal(false); const [filterMenuOpen, setFilterMenuOpen] = createSignal(false); const [rows, setRows] = createSignal([]); const [openMenuId, setOpenMenuId] = createSignal(null); const [editingId, setEditingId] = createSignal(null); const [viewingDept, setViewingDept] = createSignal(null); const [name, setName] = createSignal(''); const [code, setCode] = createSignal(''); const [description, setDescription] = createSignal(''); const [departmentHead, setDepartmentHead] = createSignal(''); const [departmentEmail, setDepartmentEmail] = createSignal(''); const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE'); const [transfersEnabled, setTransfersEnabled] = createSignal(false); const [visibility, setVisibility] = createSignal<'INTERNAL' | 'EXTERNAL'>('INTERNAL'); const [isLoading, setIsLoading] = createSignal(false); const [isSaving, setIsSaving] = createSignal(false); const [error, setError] = createSignal(''); const load = async () => { setIsLoading(true); setError(''); try { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const params = new URLSearchParams({ page: '1', per_page: '100', q: search().trim(), }); if (statusFilter() !== 'all') { params.set('status', statusFilter()); } const res = await fetch(`${API}/api/admin/departments?${params.toString()}`, { 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)) as DepartmentListResponse | null; const list = Array.isArray(payload) ? payload : Array.isArray(payload?.departments) ? payload.departments : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : []; setRows(list.map(normalizeDepartment)); } catch (err: any) { setError(err?.message || 'Could not reach departments API.'); setRows([]); } finally { setIsLoading(false); } }; onMount(() => void load()); const filteredRows = createMemo(() => { let r = rows(); if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase()); const q = search().toLowerCase(); if (q) { r = r.filter((d) => d.name.toLowerCase().includes(q) || String(d.code ?? '').toLowerCase().includes(q) || String(d.description ?? '').toLowerCase().includes(q) ); } const sorted = [...r]; const mode = sortBy(); sorted.sort((a, b) => { if (mode === 'name_desc') return b.name.localeCompare(a.name); if (mode === 'employees_desc') return Number(b.totalEmployees || 0) - Number(a.totalEmployees || 0); if (mode === 'employees_asc') return Number(a.totalEmployees || 0) - Number(b.totalEmployees || 0); return a.name.localeCompare(b.name); }); r = sorted; return r; }); const resetForm = () => { setEditingId(null); setName(''); setCode(''); setDescription(''); setDepartmentHead(''); setDepartmentEmail(''); setStatus('ACTIVE'); setTransfersEnabled(false); setVisibility('INTERNAL'); setFormTab('general'); setError(''); }; const openCreate = () => { resetForm(); setView('form'); }; const openEdit = (row: DepartmentRecord) => { setEditingId(row.id); setName(row.name || ''); setCode(String(row.code || '')); setDescription(String(row.description || '')); setDepartmentHead(String(row.departmentHead || '')); setDepartmentEmail(String(row.departmentEmail || '')); setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE'); setTransfersEnabled(Boolean(row.transfersEnabled)); setVisibility(row.visibility === 'EXTERNAL' ? 'EXTERNAL' : 'INTERNAL'); setFormTab('general'); setView('form'); setOpenMenuId(null); }; const save = async () => { if (!name().trim() || !code().trim()) { setError('Department name and code are required.'); setFormTab('general'); return; } setIsSaving(true); setError(''); const payload = { name: name().trim(), code: code().trim(), description: description().trim() || null, department_head: departmentHead().trim() || null, department_email: departmentEmail().trim() || null, status: status(), visibility: visibility(), transfers_enabled: transfersEnabled(), }; try { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const endpoint = editingId() ? `${API}/api/admin/departments/${editingId()}` : `${API}/api/admin/departments`; const method = editingId() ? 'PATCH' : 'POST'; const res = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', body: JSON.stringify(payload), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error((body as any).message || `Request failed (${res.status})`); } setView('list'); resetForm(); await load(); } catch (err: any) { setError(err?.message || 'Failed to save department.'); } finally { setIsSaving(false); } }; const formatDate = (v?: string) => { const s = v || ''; if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s; return s.slice(0, 10) || '—'; }; return ( {/* Page header */} Department Management Manage all departments and organizational structure {/* ── LIST VIEW ── */} {/* Tabs */} {([ { key: 'all', label: 'All Departments', action: () => { setListTab('all'); setStatusFilter('all'); void load(); } }, { key: 'create', label: 'Create Department', action: () => { setListTab('create'); openCreate(); } }, { key: 'view', label: 'View Department', action: () => setListTab('view') }, ] as const).map((tab) => ( {tab.label} ))} {/* View Department panel */} No department selected Click the ⋮ menu on any department row and choose View Department. {/* Header */} {viewingDept()!.name} {viewingDept()!.description || 'No description'} {/* Details grid — 3 cols using flex rows */} Department Code {viewingDept()!.code || '—'} Department Head {viewingDept()!.departmentHead || '—'} Department Email {viewingDept()!.departmentEmail || '—'} Total Employees {String(viewingDept()!.totalEmployees ?? 0)} Visibility {viewingDept()!.visibility || 'INTERNAL'} Created Date {viewingDept()!.createdDate?.slice(0, 10) || '—'} {/* Actions */} openEdit(viewingDept()!)} style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Edit Department { setViewingDept(null); 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 {/* Table card */} {/* Filter bar */} { setSearch(e.currentTarget.value); void load(); }} placeholder="Search departments..." style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none" /> { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer" > Sort {(['name_asc', 'name_desc', 'employees_desc', 'employees_asc'] as const).map((s, i) => ( { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}> {['Name (A-Z)', 'Name (Z-A)', 'Employees (High-Low)', 'Employees (Low-High)'][i]} ))} { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer" > Filters {(['all', 'active', 'inactive'] as const).map((s) => ( { setStatusFilter(s); setFilterMenuOpen(false); void load(); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}> {s === 'all' ? 'All Status' : s === 'active' ? 'Active' : 'Inactive'} ))} Export {/* Table */} Department Name Department Code Description Total Employees Status Created Date Actions 0} fallback={ No departments found Create your first department to get started. Create Department } > {(row) => ( {row.name} {String(row.code || '—')} {String(row.description || '—')} {Number(row.totalEmployees || 0)} {formatDate(String(row.createdDate || row.updatedAt || ''))} setOpenMenuId(openMenuId() === row.id ? null : row.id)} class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-[#9CA3AF] hover:bg-[#F3F4F6] hover:text-[#374151] transition-colors" aria-label="More actions" > { setViewingDept(row); setOpenMenuId(null); setListTab('view'); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left"> View Department openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left"> Edit Department { try { const res = await fetch(`${API}/api/admin/departments/${row.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }) }); if (!res.ok) throw new Error(`Request failed (${res.status})`); } catch (err: any) { setError(err?.message || 'Failed to update status.'); } finally { setOpenMenuId(null); await load(); } }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left" > {row.status === 'ACTIVE' ? 'Deactivate Department' : 'Activate Department'} { if (!window.confirm(`Delete department "${row.name}"?`)) return; try { const res = await fetch(`${API}/api/admin/departments/${row.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(`Request failed (${res.status})`); } catch (err: any) { setError(err?.message || 'Failed to delete department.'); } finally { setOpenMenuId(null); await load(); } }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left" > Delete Department )} {/* Pagination */} 0}> Showing 1–{filteredRows().length} of {filteredRows().length} departments ‹ 1 2 3 › {/* ── FORM VIEW (Create / Edit) ── */} {/* Top tabs */} setView('list')} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer"> All Departments {editingId() ? 'Edit Department' : 'Create Department'} {/* Sub-tabs */} {(['general', 'settings', 'permissions'] as const).map((tab, i) => { const labels = ['General Information', 'Department Settings', 'Permissions']; 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]} ); })} {error()} {/* General Information */} Description setDescription(e.currentTarget.value)} placeholder="Brief description of this department's purpose..." 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" /> {/* Department Settings */} Department Status Set whether this department is currently active {(['ACTIVE', 'INACTIVE'] as const).map((s) => ( setStatus(s)} style={`height:38px;border-radius:10px;padding:0 20px;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'} ))} Department Visibility Choose who can see this department {[ { key: 'INTERNAL', label: 'Internal', desc: 'Visible to internal employees only' }, { key: 'EXTERNAL', label: 'External', desc: 'Visible to external users and partners' }, ].map((opt) => ( setVisibility(opt.key as 'INTERNAL' | 'EXTERNAL')} style={`display:flex;align-items:flex-start;gap:10px;border-radius:12px;border:1px solid ${visibility() === opt.key ? '#FF5E13' : '#E5E7EB'};background:${visibility() === opt.key ? '#FFF7ED' : '#F9FAFB'};padding:14px 16px;text-align:left;cursor:pointer`} > {opt.label} {opt.desc} ))} Allow Employee Transfers Employees can request to transfer into this department setTransfersEnabled((v) => !v)} style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${transfersEnabled() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`} > {/* Permissions */} Select the permissions available to employees in this department. {(group) => ( {group.title} {(item) => ( {item} )} )} {/* Form actions */} { 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;opacity:1" > {isSaving() ? 'Saving...' : editingId() ? 'Update Department' : 'Create Department'} ); }
Manage all departments and organizational structure
No department selected
Click the ⋮ menu on any department row and choose View Department.
{viewingDept()!.description || 'No description'}
Department Code
{viewingDept()!.code || '—'}
Department Head
{viewingDept()!.departmentHead || '—'}
Department Email
{viewingDept()!.departmentEmail || '—'}
Total Employees
{String(viewingDept()!.totalEmployees ?? 0)}
Visibility
{viewingDept()!.visibility || 'INTERNAL'}
Created Date
{viewingDept()!.createdDate?.slice(0, 10) || '—'}
No departments found
Create your first department to get started.
{row.name}
{String(row.description || '—')}
Showing 1–{filteredRows().length} of {filteredRows().length} departments
Department Status
Set whether this department is currently active
Department Visibility
Choose who can see this department
{opt.label}
{opt.desc}
Allow Employee Transfers
Employees can request to transfer into this department
Select the permissions available to employees in this department.
{group.title}