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 DesignationRecord = CrudRecord & { code?: string; department?: string; departmentId?: string; level?: string; description?: string; totalEmployees?: number; createdDate?: string; canManageTeam?: boolean; canApprove?: boolean; }; type DepartmentOption = { id: string; name: string }; const permissionGroups = [ { title: 'Employee Management', items: ['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees'] }, { title: 'Role Management', items: ['View Roles', 'Assign Roles'] }, { title: 'Workflow Actions', items: ['Approve Requests', 'Manage Team Members'] }, ]; const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level', 'Executive', 'Specialist', 'Analyst']; type DesignationListResponse = { designations?: any[]; data?: any[]; items?: any[]; }; function normalizeDesignation(item: any, idx: number): DesignationRecord { 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 ?? `des-${idx + 1}`), name: String(item.name ?? ''), code: item.code ? String(item.code) : undefined, department: item.department_name ? String(item.department_name) : undefined, departmentId: item.department_id ? String(item.department_id) : undefined, level: item.level ? String(item.level) : undefined, description: item.description ? String(item.description) : undefined, totalEmployees: Number(item.total_employees ?? 0), canManageTeam: Boolean(item.can_manage_team ?? false), canApprove: Boolean(item.can_approve ?? false), 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" /> ); } function FormSelect(props: { label: string; required?: boolean; value: string; onChange: (v: string) => void; children: any }) { return ( {props.label}{props.required && *} props.onChange(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" > {props.children} ); } export default function DesignationManagementPage() { 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'>('all'); const [search, setSearch] = createSignal(''); const [deptFilter, setDeptFilter] = createSignal('all'); 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 [viewingRecord, setViewingRecord] = createSignal(null); const [name, setName] = createSignal(''); const [code, setCode] = createSignal(''); const [departmentId, setDepartmentId] = createSignal(''); const [level, setLevel] = createSignal(''); const [description, setDescription] = createSignal(''); const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE'); const [canManageTeam, setCanManageTeam] = createSignal(false); const [canApprove, setCanApprove] = createSignal(false); const [isLoading, setIsLoading] = createSignal(false); const [isSaving, setIsSaving] = createSignal(false); const [error, setError] = createSignal(''); const [departments, setDepartments] = createSignal([]); const load = async () => { setIsLoading(true); setError(''); try { const params = new URLSearchParams({ page: '1', per_page: '100', q: search().trim(), }); if (statusFilter() !== 'all') { params.set('status', statusFilter()); } if (deptFilter() !== 'all') { params.set('department_id', deptFilter()); } const res = await fetch(`${API}/api/admin/designations?${params.toString()}`); if (!res.ok) throw new Error(`Request failed (${res.status})`); const payload = (await res.json().catch(() => null)) as DesignationListResponse | null; const list = Array.isArray(payload) ? payload : Array.isArray(payload?.designations) ? payload.designations : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : []; setRows(list.map(normalizeDesignation)); } catch (err: any) { setError(err?.message || 'Could not reach designations 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) ? payload : Array.isArray(payload?.departments) ? payload.departments : []; setDepartments(list.map((d: any) => ({ id: String(d.id), name: String(d.name) }))); } catch { // departments dropdown will just be empty } }; onMount(() => { void load(); void loadDepartments(); }); const filteredRows = createMemo(() => { let r = rows(); if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase()); if (deptFilter() !== 'all') r = r.filter((d) => d.department === deptFilter()); 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(''); setDepartmentId(''); setLevel(''); setDescription(''); setStatus('ACTIVE'); setCanManageTeam(false); setCanApprove(false); setFormTab('general'); setError(''); }; const openCreate = () => { resetForm(); setView('form'); }; const openEdit = (row: DesignationRecord) => { setEditingId(row.id); setName(row.name || ''); setCode(row.code || ''); setDepartmentId(row.departmentId || ''); setLevel(row.level || ''); setDescription(row.description || ''); setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE'); setCanManageTeam(Boolean(row.canManageTeam)); setCanApprove(Boolean(row.canApprove)); setFormTab('general'); setView('form'); setOpenMenuId(null); }; const save = async () => { if (!name().trim() || !code().trim()) { setError('Designation name and code are required.'); setFormTab('general'); return; } setIsSaving(true); setError(''); const payload: Record = { name: name().trim(), code: code().trim(), level: level().trim() || null, description: description().trim() || null, status: status(), can_manage_team: canManageTeam(), can_approve: canApprove(), }; if (departmentId().trim()) { payload.department_id = departmentId().trim(); } try { const endpoint = editingId() ? `${API}/api/admin/designations/${editingId()}` : `${API}/api/admin/designations`; const method = editingId() ? 'PATCH' : 'POST'; const res = await fetch(endpoint, { method, headers: { 'Content-Type': 'application/json' }, 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 designation.'); } 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 */} Designation Management Manage all job designations and position levels {/* ── LIST VIEW ── */} {/* Tabs */} {([ { key: 'all', label: 'All Designations', action: () => { setListTab('all'); setStatusFilter('all'); void load(); } }, { key: 'create', label: 'Create Designation', action: () => { setListTab('create'); openCreate(); } }, { key: 'view', label: 'View Designation', action: () => setListTab('view') }, ] as const).map((tab) => ( {tab.label} ))} {/* View Record panel */} No designation selected Click the ⋮ menu on any designation row and choose View Details. {/* Header */} {viewingRecord()!.name} {viewingRecord()!.description || 'No description'} {/* Details grid */} Designation Code {viewingRecord()!.code || '—'} Department {viewingRecord()!.department || '—'} Level {viewingRecord()!.level || '—'} Total Employees {String(viewingRecord()!.totalEmployees ?? 0)} Can Manage Team {viewingRecord()!.canManageTeam ? 'Yes' : 'No'} Created Date {viewingRecord()!.createdDate?.slice(0, 10) || '—'} {/* Actions */} openEdit(viewingRecord()!)} style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Edit Designation { setViewingRecord(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 designations..." 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 */} Designation Name Code Department Level Employees Status Actions 0} fallback={ No designations found Create your first designation to get started. Create Designation } > {(row) => ( {row.name} {String(row.code || '—')} {String(row.department || '—')} {String(row.level || '—')} {Number(row.totalEmployees || 0)} 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" > { setViewingRecord(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 Details 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 Designation { try { const res = await fetch(`${API}/api/admin/designations/${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 Designation' : 'Activate Designation'} { if (!window.confirm(`Delete designation "${row.name}"?`)) return; try { const res = await fetch(`${API}/api/admin/designations/${row.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(`Request failed (${res.status})`); } catch (err: any) { setError(err?.message || 'Failed to delete designation.'); } 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 Designation )} {/* Pagination */} 0}> Showing 1–{filteredRows().length} of {filteredRows().length} designations ‹ 1 2 › {/* ── 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 Designations {editingId() ? 'Edit Designation' : 'Create Designation'} {/* Sub-tabs */} {(['general', 'settings', 'permissions'] as const).map((tab, i) => { const labels = ['General Information', 'Designation 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 */} Select department {(d) => {d.name}} Select level {(l) => {l}} Description setDescription(e.currentTarget.value)} placeholder="Brief description of this designation'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" /> {/* Designation Settings */} Designation Status Set whether this designation 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'} ))} Can Manage Team This designation can manage team members setCanManageTeam((v) => !v)} style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canManageTeam() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`} > Can Approve Requests This designation can approve employee requests setCanApprove((v) => !v)} style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canApprove() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`} > {/* Permissions */} Select the permissions available to employees with this designation. {(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 Designation' : 'Create Designation'} ); }
Manage all job designations and position levels
No designation selected
Click the ⋮ menu on any designation row and choose View Details.
{viewingRecord()!.description || 'No description'}
Designation Code
{viewingRecord()!.code || '—'}
Department
{viewingRecord()!.department || '—'}
Level
{viewingRecord()!.level || '—'}
Total Employees
{String(viewingRecord()!.totalEmployees ?? 0)}
Can Manage Team
{viewingRecord()!.canManageTeam ? 'Yes' : 'No'}
Created Date
{viewingRecord()!.createdDate?.slice(0, 10) || '—'}
No designations found
Create your first designation to get started.
{row.name}
Showing 1–{filteredRows().length} of {filteredRows().length} designations
Designation Status
Set whether this designation is currently active
This designation can manage team members
Can Approve Requests
This designation can approve employee requests
Select the permissions available to employees with this designation.
{group.title}