Update admin panel routes: approval, designation, employees, dashboard mgmt, onboarding, roles, verification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-27 02:28:34 +01:00
parent fd4f6ceba8
commit a95c955ad4
7 changed files with 2965 additions and 2900 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,30 +1,53 @@
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import AdminShell from '~/components/AdminShell'; import AdminShell from '~/components/AdminShell';
import { updateModuleRecord, deleteModuleRecord } from '~/lib/admin/client'; import { ChevronDown, SlidersHorizontal, Download, MoreVertical } from 'lucide-solid';
import type { CrudRecord } from '~/lib/admin/types';
type DesignationRecord = CrudRecord & { type DesignationRecord = {
code?: string; id: string;
department?: string; name: string;
level?: string; code: string;
department: string;
level: string;
description?: string; description?: string;
totalEmployees?: number; totalEmployees: number;
createdDate?: string; status: 'ACTIVE' | 'INACTIVE';
createdDate: string;
canManageTeam?: boolean; canManageTeam?: boolean;
canApprove?: boolean; canApprove?: boolean;
}; };
const FALLBACK_DESIGNATIONS: DesignationRecord[] = [ const FALLBACK_DESIGNATIONS: DesignationRecord[] = [
{ id: 'z1', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-15' }, { id: 'z1', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', createdDate: '2024-01-15' },
{ id: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-20' }, { id: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', createdDate: '2024-01-20' },
{ id: 'z3', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-01' }, { id: 'z3', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', createdDate: '2024-02-01' },
{ id: 'z4', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-05' }, { id: 'z4', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', createdDate: '2024-02-10' },
{ id: 'z5', name: 'Product Manager', code: 'PM-005', department: 'Product', level: 'Manager', totalEmployees: 6, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-10' }, { id: 'z5', name: 'Financial Analyst', code: 'FA-005', department: 'Finance', level: 'Analyst', totalEmployees: 6, status: 'ACTIVE', createdDate: '2024-02-15' },
{ id: 'z6', name: 'Finance Analyst', code: 'FA-006', department: 'Finance', level: 'Analyst', totalEmployees: 9, status: 'INACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-15' }, { id: 'z6', name: 'Operations Manager', code: 'OM-006', department: 'Operations', level: 'Manager', totalEmployees: 4, status: 'INACTIVE', createdDate: '2024-03-01' },
{ id: 'z7', name: 'Customer Support Lead', code: 'CSL-007', department: 'Customer Support', level: 'Lead', totalEmployees: 9, status: 'ACTIVE', createdDate: '2024-03-05' },
{ id: 'z8', name: 'Product Designer', code: 'PD-008', department: 'Product', level: 'Designer', totalEmployees: 7, status: 'ACTIVE', createdDate: '2024-03-10' },
]; ];
const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level', 'Executive', 'Specialist', 'Analyst']; const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level', 'Executive', 'Specialist', 'Analyst', 'Designer'];
const DEPARTMENTS = ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance', 'Operations', 'Product', 'Customer Success']; const DEPARTMENTS = ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance', 'Operations', 'Product', 'Customer Support'];
function levelBadge(level: string) {
const map: Record<string, { bg: string; color: string; border: string }> = {
Senior: { bg: '#EFF6FF', color: '#1D4ED8', border: '#BFDBFE' },
Manager: { bg: '#F5F3FF', color: '#7C3AED', border: '#DDD6FE' },
Executive: { bg: '#ECFDF5', color: '#059669', border: '#A7F3D0' },
Specialist: { bg: '#F0FDFA', color: '#0D9488', border: '#99F6E4' },
Analyst: { bg: '#FFF7ED', color: '#EA580C', border: '#FED7AA' },
Lead: { bg: '#EFF6FF', color: '#2563EB', border: '#BFDBFE' },
Designer: { bg: '#FDF4FF', color: '#A21CAF', border: '#F0ABFC' },
Director: { bg: '#FFF1F2', color: '#BE123C', border: '#FECDD3' },
};
const s = map[level] ?? { bg: '#F3F4F6', color: '#4B5563', border: '#D1D5DB' };
return (
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${s.border};background:${s.bg};color:${s.color};padding:2px 10px;font-size:12px;font-weight:500`}>
{level}
</span>
);
}
function StatusBadge(props: { status: string }) { function StatusBadge(props: { status: string }) {
const active = () => props.status === 'ACTIVE'; const active = () => props.status === 'ACTIVE';
@ -36,6 +59,18 @@ function StatusBadge(props: { status: string }) {
); );
} }
function Toggle(props: { on: boolean; onChange: (v: boolean) => void }) {
return (
<button
type="button"
onClick={() => props.onChange(!props.on)}
style={`width:40px;height:22px;border-radius:11px;background:${props.on ? '#FF5E13' : '#E5E7EB'};position:relative;cursor:pointer;border:none;padding:0;flex-shrink:0`}
>
<span style={`position:absolute;width:18px;height:18px;border-radius:50%;background:white;top:2px;transition:left 0.2s;left:${props.on ? '20px' : '2px'}`} />
</button>
);
}
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) { function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
return ( return (
<label style="display:block"> <label style="display:block">
@ -71,32 +106,32 @@ function FormSelect(props: { label: string; required?: boolean; value: string; o
} }
export default function DesignationManagementPage() { export default function DesignationManagementPage() {
const [view, setView] = createSignal<'list' | 'form'>('list'); const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general'); const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
const [search, setSearch] = createSignal(''); const [search, setSearch] = createSignal('');
const [deptFilter, setDeptFilter] = createSignal('all');
const [statusFilter, setStatusFilter] = createSignal('all');
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
const [rows, setRows] = createSignal<DesignationRecord[]>([]); const [rows, setRows] = createSignal<DesignationRecord[]>([]);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null); const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [editingId, setEditingId] = createSignal<string | null>(null);
// form state
const [name, setName] = createSignal(''); const [name, setName] = createSignal('');
const [code, setCode] = createSignal(''); const [code, setCode] = createSignal('');
const [department, setDepartment] = createSignal(''); const [department, setDepartment] = createSignal('');
const [level, setLevel] = createSignal(''); const [level, setLevel] = createSignal('');
const [description, setDescription] = createSignal(''); const [description, setDescription] = createSignal('');
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE'); const [formStatus, setFormStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
const [canManageTeam, setCanManageTeam] = createSignal(false); const [canManageTeam, setCanManageTeam] = createSignal(false);
const [canApprove, setCanApprove] = createSignal(false); const [canApprove, setCanApprove] = createSignal(false);
const [isSaving, setIsSaving] = createSignal(false);
const [error, setError] = createSignal(''); // permissions toggles
const [permViewEmp, setPermViewEmp] = createSignal(false);
const [permCreateEmp, setPermCreateEmp] = createSignal(false);
const [permEditEmp, setPermEditEmp] = createSignal(false);
const [permDeleteEmp, setPermDeleteEmp] = createSignal(false);
const [permAssignRoles, setPermAssignRoles] = createSignal(false);
const load = async () => { const load = async () => {
try { try {
const res = await fetch(`/api/gateway/api/admin/designations?page=1&limit=100&q=${encodeURIComponent(search().trim())}`); const res = await fetch(`/api/gateway/api/admin/designations?page=1&limit=100`);
if (res.ok) { if (res.ok) {
const payload = await res.json().catch(() => null); const payload = await res.json().catch(() => null);
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : []; const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : [];
@ -110,7 +145,6 @@ export default function DesignationManagementPage() {
description: String(item.description ?? ''), description: String(item.description ?? ''),
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? 0), totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? 0),
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE', status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: String(item.updatedAt ?? item.updated_at ?? ''),
createdDate: String(item.createdDate ?? item.created_at ?? ''), createdDate: String(item.createdDate ?? item.created_at ?? ''),
}))); })));
return; return;
@ -123,206 +157,148 @@ export default function DesignationManagementPage() {
onMount(() => void load()); onMount(() => void load());
const filteredRows = createMemo(() => { 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(); const q = search().toLowerCase();
if (q) r = r.filter((d) => d.name.toLowerCase().includes(q) || String(d.code ?? '').toLowerCase().includes(q)); if (!q) return rows();
return r; return rows().filter((d) =>
d.name.toLowerCase().includes(q) || d.code.toLowerCase().includes(q) || d.department.toLowerCase().includes(q)
);
}); });
const resetForm = () => { const resetForm = () => {
setEditingId(null); setName(''); setCode(''); setDepartment(''); setName(''); setCode(''); setDepartment(''); setLevel('');
setLevel(''); setDescription(''); setStatus('ACTIVE'); setDescription(''); setFormStatus('ACTIVE');
setCanManageTeam(false); setCanApprove(false); setFormTab('general'); setError(''); setCanManageTeam(false); setCanApprove(false);
setPermViewEmp(false); setPermCreateEmp(false);
setPermEditEmp(false); setPermDeleteEmp(false);
setPermAssignRoles(false);
setFormTab('general');
}; };
const openCreate = () => { resetForm(); setView('form'); }; const handleTabChange = (tab: 'all' | 'create') => {
setMainTab(tab);
const openEdit = (row: DesignationRecord) => { if (tab === 'create') resetForm();
setEditingId(row.id); setOpenMenuId(null);
setName(row.name || ''); setCode(String(row.code || ''));
setDepartment(String(row.department || '')); setLevel(String(row.level || ''));
setDescription(String(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 () => { const formatDate = (d: string) => {
if (!name().trim()) { setError('Designation name is required.'); setFormTab('general'); return; } if (!d) return '—';
setIsSaving(true); setError('');
try { try {
const payload: Partial<DesignationRecord> = { return new Date(d).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
name: name().trim(), code: code().trim() || undefined, } catch { return d; }
department: department().trim(), level: level().trim(),
description: description().trim(), status: status(),
canManageTeam: canManageTeam(), canApprove: canApprove(),
};
if (editingId()) {
await updateModuleRecord<DesignationRecord>('designation', editingId()!, payload);
} else {
const res = await fetch('/api/gateway/api/admin/designations', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`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 ( return (
<AdminShell> <AdminShell>
<div style="width:100%;padding-bottom:32px"> <div style="padding:24px">
{/* Page header */} {/* Page header */}
<div style="margin-bottom:24px"> <div>
<h1 style="font-size:28px;font-weight:700;color:#111827;line-height:1.2">Designation Management</h1> <h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Designation Management</h1>
<p style="margin-top:4px;font-size:14px;color:#6B7280">Manage all job designations and position levels</p> <p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage all designations and job positions</p>
</div> </div>
{/* ── LIST VIEW ── */} {/* Main tabs */}
<Show when={view() === 'list'}> <div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
<div>
{/* Tabs */}
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
{ 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) => (
<button <button
type="button" type="button"
onClick={tab.action} onClick={() => handleTabChange('all')}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`} style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${mainTab() === 'all' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px'}`}
> >
{tab.label} All Designations
</button>
<button
type="button"
onClick={() => handleTabChange('create')}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${mainTab() === 'create' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px'}`}
>
Create Designation
</button> </button>
))}
</div> </div>
{/* Table card */} {/* All Designations view */}
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)"> <Show when={mainTab() === 'all'}>
<div style="margin-top:20px;margin-left:-24px;margin-right:-24px;border-radius:0;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white">
{/* Filter bar */} {/* Filter bar */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6"> <div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input <input
value={search()} type="text"
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
placeholder="Search designations..." 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" value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="height:34px;flex:1;max-width:240px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827;background:white"
/> />
<select
value={deptFilter()}
onChange={(e) => setDeptFilter(e.currentTarget.value)}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151;outline:none"
>
<option value="all">All Departments</option>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</select>
<div style="position:relative">
<button <button
type="button" type="button"
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }} style="display:inline-flex;align-items:center;height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;gap:6px;cursor:pointer"
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"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg> <ChevronDown size={14} />
Sort
</button>
<button
type="button"
style="display:inline-flex;align-items:center;height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;gap:6px;cursor:pointer"
>
<SlidersHorizontal size={14} />
Filters Filters
</button> </button>
<Show when={filterMenuOpen()}> <button
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)"> type="button"
{(['all', 'ACTIVE', 'INACTIVE'] as const).map((s) => ( style="display:inline-flex;align-items:center;height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer;gap:6px"
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}> >
{s === 'all' ? 'All Status' : s === 'ACTIVE' ? 'Active' : 'Inactive'} <Download size={14} />
</button>
))}
</div>
</Show>
</div>
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export Export
</button> </button>
</div> </div>
{/* Table */} {/* Table */}
<div style="overflow-x:auto"> <div style="overflow-x:auto">
<table style="min-width:100%"> <table style="width:100%;border-collapse:collapse">
<thead> <thead>
<tr style="background:#0D0D2A;text-align:left"> <tr style="background:#0D0D2A">
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Designation Name</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Designation Name</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Code</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Designation Code</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Department</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Department</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Level</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Level</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Employees</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Total Employees</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Status</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Created Date</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Created Date</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<Show
when={filteredRows().length > 0}
fallback={
<tr>
<td colspan="8" style="padding:64px 24px;text-align:center">
<p style="font-size:15px;font-weight:600;color:#111827">No designations found</p>
<p style="margin-top:4px;font-size:13px;color:#6B7280">Create your first designation to get started.</p>
<button type="button" onClick={openCreate} style="margin-top:16px;display:inline-flex;align-items:center;gap:8px;border-radius:12px;background:#0D0D2A;padding:8px 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">
Create Designation
</button>
</td>
</tr>
}
>
<For each={filteredRows()}> <For each={filteredRows()}>
{(row) => ( {(row) => (
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors"> <tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 20px"> <td style="padding:12px 20px;font-size:14px;color:#111827;font-weight:600">{row.name}</td>
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p> <td style="padding:12px 20px;font-size:14px;color:#111827">{row.code}</td>
</td> <td style="padding:12px 20px;font-size:14px;color:#111827">{row.department}</td>
<td style="padding:12px 20px"> <td style="padding:12px 20px;font-size:14px;color:#111827">{levelBadge(row.level)}</td>
<span style="font-size:12px;font-family:monospace;color:#6B7280">{String(row.code || '—')}</span> <td style="padding:12px 20px;font-size:14px;color:#111827">{row.totalEmployees}</td>
</td> <td style="padding:12px 20px;font-size:14px;color:#111827"><StatusBadge status={row.status} /></td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{String(row.department || '—')}</td> <td style="padding:12px 20px;font-size:14px;color:#111827">{formatDate(row.createdDate)}</td>
<td style="padding:12px 20px"> <td style="padding:12px 20px;font-size:14px;color:#111827;position:relative">
<span style="display:inline-flex;border-radius:9999px;background:#EFF6FF;color:#2563EB;padding:2px 10px;font-size:12px;font-weight:500">{String(row.level || '—')}</span>
</td>
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">{Number(row.totalEmployees || 0)}</td>
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
<td style="padding:12px 20px;position:relative">
<button <button
type="button" type="button"
onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)}
style="display:inline-flex;width:32px;height:32px;align-items:center;justify-content:center;border-radius:8px;border:none;background:none;color:#9CA3AF;cursor:pointer" style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;border:1px solid #E5E7EB;background:white;cursor:pointer;color:#374151"
aria-label="More actions"
> >
<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg> <MoreVertical size={16} />
</button> </button>
<Show when={openMenuId() === row.id}> <Show when={openMenuId() === row.id}>
<div style="position:absolute;right:20px;top:44px;z-index:20;width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)"> <div style="position:absolute;right:20px;top:44px;z-index:50;background:white;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);min-width:140px;overflow:hidden">
<button type="button" onClick={() => openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer"> <button
<svg style="width:16px;height:16px;color:#FF5E13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z"/></svg> type="button"
Edit Designation onClick={() => { setOpenMenuId(null); }}
style="display:block;width:100%;text-align:left;padding:9px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer"
class="hover:bg-[#FAFAFA]"
>
Edit
</button> </button>
<button type="button" onClick={async () => { await updateModuleRecord<DesignationRecord>('designation', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer"> <button
<svg style="width:16px;height:16px;color:#FF5E13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg> type="button"
{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'} onClick={() => { setOpenMenuId(null); }}
</button> style="display:block;width:100%;text-align:left;padding:9px 14px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer"
<div style="height:1px;background:#F3F4F6;margin:4px 0" /> class="hover:bg-[#FFF5F5]"
<button type="button" onClick={async () => { if (!window.confirm(`Delete designation "${row.name}"?`)) return; await deleteModuleRecord('designation', row.id); setOpenMenuId(null); await load(); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#DC2626;background:none;border:none;cursor:pointer"> >
<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6"/></svg>
Delete Delete
</button> </button>
</div> </div>
@ -331,185 +307,214 @@ export default function DesignationManagementPage() {
</tr> </tr>
)} )}
</For> </For>
</Show>
</tbody> </tbody>
</table> </table>
</div> </div>
{/* Pagination */} {/* Pagination */}
<Show when={filteredRows().length > 0}> <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-top:1px solid #F3F4F6">
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px"> <span style="font-size:13px;color:#6B7280">
<p style="font-size:13px;color:#6B7280"> Showing 1-{filteredRows().length} of {filteredRows().length} designations
Showing <strong style="font-weight:600;color:#111827">1{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> designations </span>
</p>
<div style="display:flex;align-items:center;gap:4px"> <div style="display:flex;align-items:center;gap:4px">
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
</div>
</div>
</Show>
</div>
</div>
</Show>
{/* ── FORM VIEW ── */}
<Show when={view() === 'form'}>
{/* Top tabs */}
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
<button type="button" onClick={() => { setView('list'); resetForm(); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
All Designations
</button>
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">
{editingId() ? 'Edit Designation' : 'Create Designation'}
</button>
</div>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(['general', 'settings', 'permissions'] as const).map((tab, i) => {
const labels = ['General Information', 'Designation Settings', 'Permissions'];
const active = () => formTab() === tab;
return (
<button <button
type="button" type="button"
onClick={() => setFormTab(tab)} style="display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;border:1px solid #E5E7EB;background:white;color:#374151;cursor:pointer;font-size:13px"
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}
> >
{labels[i]}
<Show when={active()}> </button>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /> <button
</Show> type="button"
style="display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;background:#FF5E13;color:white;border:none;cursor:pointer;font-size:13px;font-weight:500"
>
1
</button>
<button
type="button"
style="display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;border:1px solid #E5E7EB;background:white;color:#374151;cursor:pointer;font-size:13px"
>
</button> </button>
);
})}
</div> </div>
</div>
<div style="padding:24px">
<Show when={error()}>
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
{error()}
</div> </div>
</Show> </Show>
{/* General Information */} {/* Create Designation view */}
<Show when={formTab() === 'general'}> <Show when={mainTab() === 'create'}>
<div style="display:flex;flex-direction:column;gap:20px"> <div style="margin-top:20px;border-radius:16px;border:1px solid #E5E7EB;background:white;overflow:hidden">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px"> {/* Form sub-tabs */}
<FormInput label="Designation Name" required value={name()} onInput={setName} placeholder="e.g. Senior Software Engineer" /> <div style="display:flex;align-items:center;gap:0;border-bottom:1px solid #E5E7EB;padding:0 24px">
<FormInput label="Designation Code" required value={code()} onInput={setCode} placeholder="e.g. SSE-001" /> <button
type="button"
onClick={() => setFormTab('general')}
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${formTab() === 'general' ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px;font-weight:500'}`}
>
General Information
</button>
<button
type="button"
onClick={() => setFormTab('settings')}
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${formTab() === 'settings' ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px;font-weight:500'}`}
>
Designation Settings
</button>
<button
type="button"
onClick={() => setFormTab('permissions')}
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${formTab() === 'permissions' ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px;font-weight:500'}`}
>
Permissions
</button>
</div> </div>
{/* General Information tab */}
<Show when={formTab() === 'general'}>
<div style="padding:24px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormSelect label="Department" required value={department()} onChange={setDepartment}> <FormInput
label="Designation Name"
required
value={name()}
onInput={setName}
placeholder="e.g. Senior Software Engineer"
/>
<FormInput
label="Designation Code"
required
value={code()}
onInput={setCode}
placeholder="e.g. SSE-001"
/>
<FormSelect
label="Department"
required
value={department()}
onChange={setDepartment}
>
<option value="">Select department</option> <option value="">Select department</option>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For> <For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</FormSelect> </FormSelect>
<FormSelect label="Designation Level" required value={level()} onChange={setLevel}> <FormSelect
label="Designation Level"
required
value={level()}
onChange={setLevel}
>
<option value="">Select level</option> <option value="">Select level</option>
<For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For> <For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For>
</FormSelect> </FormSelect>
</div> <div style="grid-column:1 / -1">
<label style="display:block"> <label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span> <span style="font-size:13px;font-weight:600;color:#374151">Description</span>
<textarea <textarea
value={description()} value={description()}
onInput={(e) => setDescription(e.currentTarget.value)} onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Brief description of this designation's responsibilities..." placeholder="Describe the designation role and responsibilities..."
rows="3" rows={4}
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" 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;box-sizing:border-box;resize:vertical;font-family:inherit"
/> />
</label> </label>
</div> </div>
</div>
</div>
</Show> </Show>
{/* Designation Settings */} {/* Designation Settings tab */}
<Show when={formTab() === 'settings'}> <Show when={formTab() === 'settings'}>
<div style="display:flex;flex-direction:column;gap:32px"> <div style="padding:24px;display:flex;flex-direction:column;gap:24px">
{/* Status */}
<div> <div>
<p style="font-size:14px;font-weight:600;color:#111827">Designation Status</p> <p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 10px 0">Designation Status</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">Set whether this designation is currently active</p> <div style="display:flex;gap:10px">
<div style="margin-top:12px;display:flex;gap:10px">
{(['ACTIVE', 'INACTIVE'] as const).map((s) => (
<button <button
type="button" type="button"
onClick={() => setStatus(s)} onClick={() => setFormStatus('ACTIVE')}
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'}`} style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${formStatus() === 'ACTIVE' ? 'border:1px solid #FF5E13;background:#FFF3EE;color:#FF5E13' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
> >
{s === 'ACTIVE' ? 'Active' : 'Inactive'} Active
</button>
<button
type="button"
onClick={() => setFormStatus('INACTIVE')}
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${formStatus() === 'INACTIVE' ? 'border:1px solid #FF5E13;background:#FFF3EE;color:#FF5E13' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
>
Inactive
</button> </button>
))}
</div> </div>
</div> </div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px"> {/* Toggle: Manage Team */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
<div> <div>
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage Team</p> <p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Designation to Manage Team Members</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can manage team members</p> <p style="font-size:12px;color:#6B7280;margin:0">Enable this to allow team member management capabilities</p>
</div> </div>
<button <Toggle on={canManageTeam()} onChange={setCanManageTeam} />
type="button"
onClick={() => 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`}
>
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canManageTeam() ? '22px' : '2px'}`} />
</button>
</div> </div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px"> {/* Toggle: Approval Permissions */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
<div> <div>
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests</p> <p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Approval Permissions</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can approve employee requests</p> <p style="font-size:12px;color:#6B7280;margin:0">Enable this to allow approving requests and actions</p>
</div> </div>
<button <Toggle on={canApprove()} onChange={setCanApprove} />
type="button"
onClick={() => 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`}
>
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canApprove() ? '22px' : '2px'}`} />
</button>
</div> </div>
</div> </div>
</Show> </Show>
{/* Permissions */} {/* Permissions tab */}
<Show when={formTab() === 'permissions'}> <Show when={formTab() === 'permissions'}>
<div style="display:flex;flex-direction:column;gap:16px"> <div style="padding:24px">
<p style="font-size:13px;color:#6B7280">Select the permissions available to employees with this designation.</p> {/* Employee Management */}
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"> <p style="font-size:14px;font-weight:600;color:#111827;margin:0 0 12px 0">Employee Management</p>
{['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees', 'Assign Roles', 'Approve Requests', 'Manage Team Members'].map((item) => ( <div style="display:flex;flex-direction:column;gap:0">
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer"> {[
<input type="checkbox" style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer" /> { label: 'View Employees', sig: permViewEmp, set: setPermViewEmp },
<span style="font-size:13px;font-weight:500;color:#374151">{item}</span> { label: 'Create Employees', sig: permCreateEmp, set: setPermCreateEmp },
</label> { label: 'Edit Employees', sig: permEditEmp, set: setPermEditEmp },
{ label: 'Delete Employees', sig: permDeleteEmp, set: setPermDeleteEmp },
].map((item) => (
<div style="border-radius:8px;border:1px solid #E5E7EB;padding:12px 16px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between">
<span style="font-size:14px;color:#374151">{item.label}</span>
<Toggle on={item.sig()} onChange={item.set} />
</div>
))} ))}
</div> </div>
{/* Additional Permissions */}
<p style="font-size:14px;font-weight:600;color:#111827;margin:20px 0 12px 0">Additional Permissions</p>
<div style="border-radius:8px;border:1px solid #E5E7EB;padding:12px 16px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between">
<span style="font-size:14px;color:#374151">Assign Roles</span>
<Toggle on={permAssignRoles()} onChange={setPermAssignRoles} />
</div>
</div> </div>
</Show> </Show>
</div>
{/* Form actions */} {/* Form footer */}
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px"> <div style="display:flex;align-items:center;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #E5E7EB">
<button <button
type="button" type="button"
onClick={() => { setView('list'); resetForm(); }} onClick={() => { resetForm(); setMainTab('all'); }}
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" style="height:38px;padding:0 20px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:14px;cursor:pointer"
> >
Cancel Cancel
</button> </button>
<button <button
type="button" type="button"
onClick={() => void save()} onClick={() => {
disabled={isSaving()} if (!name().trim()) { setFormTab('general'); return; }
style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer" setMainTab('all');
resetForm();
}}
style="height:38px;padding:0 20px;border-radius:8px;background:#0D0D2A;color:white;font-size:14px;font-weight:500;border:none;cursor:pointer"
> >
{isSaving() ? 'Saving...' : editingId() ? 'Update Designation' : 'Create Designation'} Create Designation
</button> </button>
</div> </div>
</div> </div>
</Show> </Show>
</div> </div>
</AdminShell> </AdminShell>
); );

View file

@ -1,71 +1,41 @@
import { createResource, createSignal, For, Show } from 'solid-js'; import { createResource, createSignal, For, Show } from 'solid-js';
import { Eye, SquarePen, Search, Trash2 } from 'lucide-solid';
import AdminShell from '~/components/AdminShell'; import AdminShell from '~/components/AdminShell';
import { Upload } from 'lucide-solid';
const API = '/api/gateway'; const API = '/api/gateway';
type Employee = { type Employee = {
id: string; id: string;
empId?: string;
employeeId?: string;
full_name?: string; full_name?: string;
name?: string; name?: string;
email: string; email: string;
role?: { id: string; name: string } | string; phone?: string;
role_name?: string;
department?: { id: string; name: string } | string; department?: { id: string; name: string } | string;
department_name?: string; department_name?: string;
designation?: { id: string; name: string } | string; designation?: { id: string; name: string } | string;
designation_name?: string; designation_name?: string;
role?: { id: string; name: string } | string;
role_name?: string;
joiningDate?: string;
joining_date?: string;
created_at?: string;
employmentType?: string;
employment_type?: string;
is_active?: boolean; is_active?: boolean;
status?: string; status?: string;
}; };
const FALLBACK: Employee[] = [ const FALLBACK_EMPLOYEES: Employee[] = [
{ { id: 'e1', empId: 'EMP001', name: 'John Smith', email: 'john.smith@nxtgauge.com', phone: '+1 234-567-8901', department: 'Engineering', designation: 'Senior Software Engineer', role: 'Engineering Lead', joiningDate: '2024-01-15', employmentType: 'Full Time', status: 'ACTIVE' },
id: 'EMP001', { id: 'e2', empId: 'EMP002', name: 'Sarah Johnson', email: 'sarah.j@nxtgauge.com', phone: '+1 234-567-8902', department: 'Marketing', designation: 'Marketing Manager', role: 'Marketing Manager', joiningDate: '2024-01-20', employmentType: 'Full Time', status: 'ACTIVE' },
full_name: 'John Smith', { id: 'e3', empId: 'EMP003', name: 'Mike Davis', email: 'mike.d@nxtgauge.com', phone: '+1 234-567-8903', department: 'Sales', designation: 'Sales Executive', role: 'Sales Director', joiningDate: '2024-02-01', employmentType: 'Full Time', status: 'ACTIVE' },
email: 'john.smith@nxtgauge.com', { id: 'e4', empId: 'EMP004', name: 'Emma Wilson', email: 'emma.w@nxtgauge.com', phone: '+1 234-567-8904', department: 'Human Resources', designation: 'HR Specialist', role: 'HR Admin', joiningDate: '2024-02-10', employmentType: 'Full Time', status: 'ACTIVE' },
department: { id: 'd1', name: 'Engineering' }, { id: 'e5', empId: 'EMP005', name: 'David Wilson', email: 'david.w@nxtgauge.com', phone: '+1 234-567-8905', department: 'Finance', designation: 'Financial Analyst', role: 'Finance Controller', joiningDate: '2024-02-15', employmentType: 'Full Time', status: 'ACTIVE' },
designation: { id: 'dg1', name: 'Senior Software Engineer' }, { id: 'e6', empId: 'EMP006', name: 'Lisa Anderson', email: 'lisa.a@nxtgauge.com', phone: '+1 234-567-8906', department: 'Operations', designation: 'Operations Manager', role: 'Operations Head', joiningDate: '2024-03-01', employmentType: 'Part Time', status: 'ACTIVE' },
role: { id: 'r1', name: 'Engineering Lead' }, { id: 'e7', empId: 'EMP007', name: 'James Taylor', email: 'james.t@nxtgauge.com', phone: '+1 234-567-8907', department: 'Customer Support', designation: 'Support Lead', role: 'Support Lead', joiningDate: '2024-03-10', employmentType: 'Full Time', status: 'ACTIVE' },
is_active: true, { id: 'e8', empId: 'EMP008', name: 'Jennifer Martinez', email: 'jennifer.m@nxtgauge.com', phone: '+1 234-567-8908', department: 'Product', designation: 'Product Designer', role: 'Product Owner', joiningDate: '2024-03-15', employmentType: 'Full Time', status: 'INACTIVE' },
},
{
id: 'EMP002',
full_name: 'Sarah Johnson',
email: 'sarah.j@nxtgauge.com',
department: { id: 'd2', name: 'Marketing' },
designation: { id: 'dg2', name: 'Marketing Manager' },
role: { id: 'r2', name: 'Marketing Manager' },
is_active: true,
},
{
id: 'EMP003',
full_name: 'Michael Brown',
email: 'michael.b@nxtgauge.com',
department: { id: 'd3', name: 'Sales' },
designation: { id: 'dg3', name: 'Sales Executive' },
role: { id: 'r3', name: 'Sales Director' },
status: 'PROBATION',
is_active: false,
},
{
id: 'EMP004',
full_name: 'Emily Davis',
email: 'emily.d@nxtgauge.com',
department: { id: 'd4', name: 'Human Resources' },
designation: { id: 'dg4', name: 'HR Specialist' },
role: { id: 'r4', name: 'HR Admin' },
is_active: true,
},
{
id: 'EMP005',
full_name: 'David Wilson',
email: 'david.w@nxtgauge.com',
department: { id: 'd5', name: 'Finance' },
designation: { id: 'dg5', name: 'Financial Analyst' },
role: { id: 'r5', name: 'Finance Controller' },
is_active: true,
},
]; ];
async function fetchEmployees(): Promise<Employee[]> { async function fetchEmployees(): Promise<Employee[]> {
@ -74,187 +44,262 @@ async function fetchEmployees(): Promise<Employee[]> {
if (!res.ok) throw new Error(); if (!res.ok) throw new Error();
const data = await res.json(); const data = await res.json();
const list = Array.isArray(data) ? data : (data.employees ?? data.users ?? []); const list = Array.isArray(data) ? data : (data.employees ?? data.users ?? []);
return list.length > 0 ? list : FALLBACK; return list.length > 0 ? list : FALLBACK_EMPLOYEES;
} catch { } catch {
return FALLBACK; return FALLBACK_EMPLOYEES;
} }
} }
function empName(e: Employee) { function getEmpId(e: Employee) { return e.empId || e.employeeId || e.id || '—'; }
return e.full_name || e.name || '—'; function getEmpName(e: Employee) { return e.full_name || e.name || '—'; }
} function getEmpDept(e: Employee) {
function empRole(e: Employee) {
if (!e.role) return e.role_name ?? '—';
if (typeof e.role === 'string') return e.role;
return e.role.name ?? '—';
}
function empDept(e: Employee) {
if (!e.department) return e.department_name ?? '—'; if (!e.department) return e.department_name ?? '—';
if (typeof e.department === 'string') return e.department; if (typeof e.department === 'string') return e.department;
return e.department.name ?? '—'; return e.department.name ?? '—';
} }
function empDesig(e: Employee) { function getEmpDesignation(e: Employee) {
if (!e.designation) return e.designation_name ?? '—'; if (!e.designation) return e.designation_name ?? '—';
if (typeof e.designation === 'string') return e.designation; if (typeof e.designation === 'string') return e.designation;
return e.designation.name ?? '—'; return e.designation.name ?? '—';
} }
function empStatus(e: Employee) { function getEmpRole(e: Employee) {
if (!e.role) return e.role_name ?? '—';
if (typeof e.role === 'string') return e.role;
return e.role.name ?? '—';
}
function getEmpJoiningDate(e: Employee) {
return e.joiningDate || e.joining_date || e.created_at?.slice(0, 10) || '—';
}
function getEmpEmploymentType(e: Employee) { return e.employmentType || e.employment_type || '—'; }
function getEmpStatus(e: Employee): 'ACTIVE' | 'INACTIVE' {
const raw = String(e.status || '').toUpperCase(); const raw = String(e.status || '').toUpperCase();
if (raw === 'PROBATION') return 'Probation'; if (e.is_active === false || raw === 'INACTIVE') return 'INACTIVE';
if (e.is_active === false || raw === 'INACTIVE') return 'Inactive'; return 'ACTIVE';
return 'Active';
} }
function StatusBadge(props: { status: 'Active' | 'Inactive' | 'Probation' }) { function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) {
const classes = () => { const active = () => props.status === 'ACTIVE';
if (props.status === 'Active') return 'border-[#B7E4C7] bg-[#DEF7E8] text-[#0B8A4A]';
if (props.status === 'Probation') return 'border-[#F6D78F] bg-[#FFF3D6] text-[#B7791F]';
return 'border-[#D1D5DB] bg-[#F3F4F6] text-[#6B7280]';
};
return ( return (
<span class={`inline-flex min-w-[68px] items-center justify-center border px-2 py-1 text-[11px] font-semibold ${classes()}`}> <span style={active()
{props.status} ? 'display:inline-flex;align-items:center;justify-content:center;min-width:68px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;border:1px solid #FFD8C2;background:#FFF1EB;color:#FF5E13'
: 'display:inline-flex;align-items:center;justify-content:center;min-width:68px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;border:1px solid #D1D5DB;background:#F3F4F6;color:#6B7280'}>
{active() ? 'Active' : 'Inactive'}
</span> </span>
); );
} }
function Toggle(props: { on: boolean; onChange: (val: boolean) => void }) {
return (
<div
onClick={() => props.onChange(!props.on)}
style={`width:40px;height:22px;border-radius:11px;background:${props.on ? '#FF5E13' : '#E5E7EB'};position:relative;cursor:pointer;flex-shrink:0;transition:background 0.2s`}
>
<div style={`position:absolute;width:18px;height:18px;border-radius:50%;background:white;top:2px;transition:left 0.2s;left:${props.on ? '20px' : '2px'}`} />
</div>
);
}
const INPUT_STYLE = 'width:100%;height:36px;padding:0 12px;font-size:13px;border:1px solid #E5E7EB;border-radius:8px;outline:none;color:#374151;background:white;box-sizing:border-box';
const LABEL_STYLE = 'display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:6px';
const DEPARTMENTS = ['Engineering', 'Marketing', 'Human Resources', 'Finance', 'Sales', 'Operations', 'Product', 'Customer Support'];
const DESIGNATIONS = ['Senior Software Engineer', 'Marketing Manager', 'Sales Executive', 'HR Specialist', 'Financial Analyst', 'Operations Manager', 'Support Lead', 'Product Designer'];
const EMPLOYMENT_TYPES = ['Full Time', 'Part Time', 'Contract', 'Internship'];
const STATUSES = ['ACTIVE', 'INACTIVE'];
const GENDERS = ['Male', 'Female', 'Other', 'Prefer not to say'];
const INTERNAL_ROLES = ['Engineering Lead', 'Marketing Manager', 'Sales Director', 'HR Admin', 'Finance Controller', 'Operations Head', 'Support Lead', 'Product Owner'];
const SHIFT_TYPES = ['Day Shift', 'Night Shift', 'Flexible'];
const EMPLOYEE_CATEGORIES = ['Permanent', 'Temporary', 'Probation'];
export default function EmployeesIndexPage() { export default function EmployeesIndexPage() {
const [employees] = createResource(fetchEmployees); const [employees] = createResource(fetchEmployees);
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [formTab, setFormTab] = createSignal<'basic' | 'work' | 'access' | 'docs'>('basic');
const [search, setSearch] = createSignal(''); const [search, setSearch] = createSignal('');
const [activeTab, setActiveTab] = createSignal<'list' | 'create'>('list'); const [deptFilter, setDeptFilter] = createSignal('');
const [designationFilter, setDesignationFilter] = createSignal('');
const [empTypeFilter, setEmpTypeFilter] = createSignal('');
const [statusFilter, setStatusFilter] = createSignal('');
const [page, setPage] = createSignal(1);
const [openActionId, setOpenActionId] = createSignal<string | null>(null);
// Access & Permissions toggles
const [allowAdminLogin, setAllowAdminLogin] = createSignal(false);
const [requirePasswordReset, setRequirePasswordReset] = createSignal(false);
const [enable2FA, setEnable2FA] = createSignal(false);
const [accountActive, setAccountActive] = createSignal(true);
const [allowMobileApp, setAllowMobileApp] = createSignal(false);
const [allowReports, setAllowReports] = createSignal(false);
const PAGE_SIZE = 10;
const filtered = () => { const filtered = () => {
const list = employees() ?? []; const list = employees() ?? [];
const q = search().trim().toLowerCase(); const q = search().trim().toLowerCase();
if (!q) return list; return list.filter((e) => {
const matchSearch = !q || [getEmpId(e), getEmpName(e), e.email, getEmpDept(e), getEmpDesignation(e)].join(' ').toLowerCase().includes(q);
return list.filter((e) => const matchDept = !deptFilter() || getEmpDept(e) === deptFilter();
[e.id, empName(e), e.email, empDept(e), empDesig(e), empRole(e)] const matchDesignation = !designationFilter() || getEmpDesignation(e) === designationFilter();
.join(' ') const matchEmpType = !empTypeFilter() || getEmpEmploymentType(e) === empTypeFilter();
.toLowerCase() const matchStatus = !statusFilter() || getEmpStatus(e) === statusFilter();
.includes(q), return matchSearch && matchDept && matchDesignation && matchEmpType && matchStatus;
); });
}; };
const totalPages = () => Math.max(1, Math.ceil(filtered().length / PAGE_SIZE));
const paged = () => filtered().slice((page() - 1) * PAGE_SIZE, page() * PAGE_SIZE);
function mainTabStyle(active: boolean) {
return active
? 'padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px'
: 'padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;color:#6B7280';
}
function formTabStyle(active: boolean) {
return active
? 'padding:14px 0;margin-right:24px;font-size:14px;font-weight:600;background:none;border:none;cursor:pointer;color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px'
: 'padding:14px 0;margin-right:24px;font-size:14px;font-weight:400;background:none;border:none;cursor:pointer;color:#6B7280';
}
return ( return (
<AdminShell> <AdminShell>
<div class="w-full space-y-4 pb-8"> <div style="width:100%;padding-bottom:32px">
{/* Page Header */}
<div> <div>
<h1 class="text-[48px] font-bold leading-[1.1] tracking-[-0.02em] text-[#0B1246]">Employee Management</h1> <h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Employee Management</h1>
<p class="mt-1 text-[15px] text-[#7E849F]">Manage internal employees and their information</p> <p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage internal employees and their information</p>
</div> </div>
<div class="rounded-none border border-[#D9DDE6] bg-white"> {/* Main Tabs */}
<div class="border-b border-[#D9DDE6] px-6"> <div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
<div class="flex items-end gap-10"> <button type="button" style={mainTabStyle(mainTab() === 'all')} onClick={() => setMainTab('all')}>
<button
type="button"
onClick={() => setActiveTab('list')}
class={`relative pb-3 pt-4 text-[36px] font-semibold ${
activeTab() === 'list' ? 'text-[#0B1246]' : 'text-[#7E849F]'
}`}
>
All Employees All Employees
<Show when={activeTab() === 'list'}>
<span class="absolute bottom-0 left-0 h-[4px] w-full bg-[#FF5E13]" />
</Show>
</button> </button>
<button type="button" style={mainTabStyle(mainTab() === 'create')} onClick={() => setMainTab('create')}>
<button
type="button"
onClick={() => setActiveTab('create')}
class={`relative pb-3 pt-4 text-[36px] font-semibold ${
activeTab() === 'create' ? 'text-[#0B1246]' : 'text-[#7E849F]'
}`}
>
Create Employee Create Employee
<Show when={activeTab() === 'create'}>
<span class="absolute bottom-0 left-0 h-[4px] w-full bg-[#FF5E13]" />
</Show>
</button> </button>
</div> </div>
</div>
<Show {/* All Employees View */}
when={activeTab() === 'list'} <Show when={mainTab() === 'all'}>
fallback={ <div style="margin-top:20px;margin-left:-24px;margin-right:-24px;border-radius:0;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white">
<div class="px-6 py-12 text-[14px] text-[#6B7280]"> {/* Filter Row 1 */}
Create Employee form will follow this new base design next. <div style="display:flex;align-items:center;gap:8px;padding:14px 20px 8px 20px">
</div>
}
>
<div class="border-b border-[#D9DDE6] px-6 py-5">
<div class="grid grid-cols-[1fr_160px_120px] gap-4">
<label class="flex h-[58px] items-center border border-[#D9DDE6] bg-[#F7F8FA] px-4 text-[#9CA3AF]">
<Search size={19} class="mr-3 text-[#9CA3AF]" />
<input <input
type="text" type="text"
placeholder="Search employees..." placeholder="Search employees"
value={search()} value={search()}
onInput={(e) => setSearch(e.currentTarget.value)} onInput={(e) => { setSearch(e.currentTarget.value); setPage(1); }}
class="w-full border-0 bg-transparent text-[34px] text-[#0B1246] placeholder:text-[#9CA3AF] outline-none" style={`height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:text;flex:1;max-width:240px;box-sizing:border-box`}
/> />
</label> <select
value={deptFilter()}
<div class="h-[58px] border border-[#D9DDE6] bg-white" /> onChange={(e) => { setDeptFilter(e.currentTarget.value); setPage(1); }}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:pointer"
<button
type="button"
class="h-[58px] border border-[#D9DDE6] bg-white text-[34px] font-medium text-[#0B1246] transition-colors hover:bg-[#F9FAFB]"
> >
<option value="">Department</option>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</select>
<select
value={designationFilter()}
onChange={(e) => { setDesignationFilter(e.currentTarget.value); setPage(1); }}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:pointer"
>
<option value="">Designation</option>
<For each={DESIGNATIONS}>{(d) => <option value={d}>{d}</option>}</For>
</select>
</div>
{/* Filter Row 2 */}
<div style="display:flex;align-items:center;gap:8px;padding:0 20px 14px 20px;border-bottom:1px solid #F3F4F6">
<select
value={empTypeFilter()}
onChange={(e) => { setEmpTypeFilter(e.currentTarget.value); setPage(1); }}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:pointer"
>
<option value="">Employment Type</option>
<For each={EMPLOYMENT_TYPES}>{(t) => <option value={t}>{t}</option>}</For>
</select>
<select
value={statusFilter()}
onChange={(e) => { setStatusFilter(e.currentTarget.value); setPage(1); }}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:pointer"
>
<option value="">Status</option>
<For each={STATUSES}>{(s) => <option value={s}>{s}</option>}</For>
</select>
<button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer">
Export Export
</button> </button>
</div> </div>
</div>
<div class="overflow-x-auto"> {/* Table */}
<table class="w-full min-w-[1400px] table-fixed border-collapse"> <div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;min-width:1100px">
<thead> <thead>
<tr class="bg-[#02033B] text-left"> <tr style="background:#0D0D2A">
{['ID', 'Name', 'Email', 'Department', 'Designation', 'Role', 'Status', 'Actions'].map((h) => ( <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Employee ID</th>
<th class="px-6 py-5 text-[31px] font-semibold text-white">{h}</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Employee Name</th>
))} <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Email</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Phone</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Department</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Designation</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Internal Role</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Joining Date</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Employment Type</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Status</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<Show when={employees.loading}> <Show when={employees.loading}>
<tr> <tr>
<td colspan="8" class="px-6 py-10 text-center text-[24px] text-[#9CA3AF]">Loading employees...</td> <td colspan="11" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">Loading employees...</td>
</tr> </tr>
</Show> </Show>
<Show when={!employees.loading && paged().length === 0}>
<Show when={!employees.loading && filtered().length === 0}>
<tr> <tr>
<td colspan="8" class="px-6 py-10 text-center text-[24px] text-[#9CA3AF]">No employees found.</td> <td colspan="11" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">No employees found.</td>
</tr> </tr>
</Show> </Show>
<For each={paged()}>
<For each={filtered()}>
{(emp) => ( {(emp) => (
<tr class="border-b border-[#D9DDE6] bg-white align-middle"> <tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA]">
<td class="px-6 py-4 text-[35px] italic text-[#303A67]">{emp.id}</td> <td style="padding:12px 20px">
<td class="px-6 py-4 text-[41px] font-semibold leading-[1.15] text-[#0B1246]">{empName(emp)}</td> <span style="font-size:12px;font-family:monospace;color:#6B7280">{getEmpId(emp)}</span>
<td class="px-6 py-4 text-[35px] text-[#59608A]">{emp.email}</td>
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empDept(emp)}</td>
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empDesig(emp)}</td>
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empRole(emp)}</td>
<td class="px-6 py-4">
<StatusBadge status={empStatus(emp) as 'Active' | 'Inactive' | 'Probation'} />
</td> </td>
<td class="px-6 py-4"> <td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827;white-space:nowrap">{getEmpName(emp)}</td>
<div class="flex items-center gap-4"> <td style="padding:12px 20px;font-size:13px;color:#374151">{emp.email}</td>
<button type="button" class="text-[#FF5E13] transition-colors hover:text-[#E04D0A]" aria-label="View"> <td style="padding:12px 20px;font-size:13px;color:#374151;white-space:nowrap">{emp.phone || '—'}</td>
<Eye size={23} /> <td style="padding:12px 20px;font-size:13px;color:#374151">{getEmpDept(emp)}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{getEmpDesignation(emp)}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{getEmpRole(emp)}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151;white-space:nowrap">{getEmpJoiningDate(emp)}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151;white-space:nowrap">{getEmpEmploymentType(emp)}</td>
<td style="padding:12px 20px">
<StatusBadge status={getEmpStatus(emp)} />
</td>
<td style="padding:12px 20px;position:relative">
<button
type="button"
onClick={() => setOpenActionId(openActionId() === emp.id ? null : emp.id)}
style="height:30px;padding:0 12px;font-size:12px;font-weight:500;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer"
>
Actions
</button> </button>
<button type="button" class="text-[#5B628D] transition-colors hover:text-[#303A67]" aria-label="Edit"> <Show when={openActionId() === emp.id}>
<SquarePen size={23} /> <div style="position:absolute;right:20px;top:44px;z-index:20;width:180px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.10)">
<button type="button" style="display:block;width:100%;text-align:left;padding:9px 12px;font-size:13px;color:#111827;background:none;border:none;cursor:pointer;border-radius:6px" class="hover:bg-[#F9FAFB]">
View Employee
</button> </button>
<button type="button" class="text-[#5B628D] transition-colors hover:text-[#303A67]" aria-label="Delete"> <button type="button" style="display:block;width:100%;text-align:left;padding:9px 12px;font-size:13px;color:#111827;background:none;border:none;cursor:pointer;border-radius:6px" class="hover:bg-[#F9FAFB]">
<Trash2 size={23} /> Edit Employee
</button>
<button type="button" style="display:block;width:100%;text-align:left;padding:9px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;border-radius:6px" class="hover:bg-[#FEF2F2]">
Deactivate
</button> </button>
</div> </div>
</Show>
</td> </td>
</tr> </tr>
)} )}
@ -262,9 +307,388 @@ export default function EmployeesIndexPage() {
</tbody> </tbody>
</table> </table>
</div> </div>
</Show>
{/* Pagination */}
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 20px">
<span style="font-size:13px;color:#6B7280">
Showing {Math.min((page() - 1) * PAGE_SIZE + 1, Math.max(filtered().length, 1))}{Math.min(page() * PAGE_SIZE, filtered().length)} of {filtered().length}
</span>
<div style="display:flex;gap:4px">
<button
type="button"
onClick={() => setPage(Math.max(1, page() - 1))}
disabled={page() === 1}
style={`height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer;opacity:${page() === 1 ? '0.4' : '1'}`}
>
</button>
<For each={Array.from({ length: totalPages() }, (_, i) => i + 1)}>
{(p) => (
<button
type="button"
onClick={() => setPage(p)}
style={p === page()
? 'height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #FF5E13;background:#FF5E13;color:white;border-radius:6px;cursor:pointer;font-weight:600'
: 'height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer'}
>
{p}
</button>
)}
</For>
<button
type="button"
onClick={() => setPage(Math.min(totalPages(), page() + 1))}
disabled={page() === totalPages()}
style={`height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer;opacity:${page() === totalPages() ? '0.4' : '1'}`}
>
</button>
</div> </div>
</div> </div>
</div>
</Show>
{/* Create Employee View */}
<Show when={mainTab() === 'create'}>
<div style="margin-top:20px;border-radius:16px;border:1px solid #E5E7EB;background:white;overflow:hidden">
{/* Form Sub-Tabs */}
<div style="display:flex;align-items:center;border-bottom:1px solid #E5E7EB;padding:0 24px">
<button type="button" style={formTabStyle(formTab() === 'basic')} onClick={() => setFormTab('basic')}>
Basic Information
</button>
<button type="button" style={formTabStyle(formTab() === 'work')} onClick={() => setFormTab('work')}>
Work Information
</button>
<button type="button" style={formTabStyle(formTab() === 'access')} onClick={() => setFormTab('access')}>
Access & Permissions
</button>
<button type="button" style={formTabStyle(formTab() === 'docs')} onClick={() => setFormTab('docs')}>
Documents
</button>
</div>
{/* Basic Information */}
<Show when={formTab() === 'basic'}>
<div style="padding:24px">
<p style="font-size:13px;color:#6B7280;margin:0 0 20px 0">Add employee personal and contact details</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div>
<label style={LABEL_STYLE}>First Name <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="Enter first name" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Last Name <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="Enter last name" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Employee ID <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="e.g., EMP001" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Email Address <span style="color:#EF4444">*</span></label>
<input type="email" placeholder="Enter email address" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Phone Number <span style="color:#EF4444">*</span></label>
<input type="tel" placeholder="+1 234-567-6900" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Alternate Phone Number</label>
<input type="tel" placeholder="+1 234-567-0900" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Date of Birth <span style="color:#EF4444">*</span></label>
<input type="date" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Gender <span style="color:#EF4444">*</span></label>
<select style={`${INPUT_STYLE};cursor:pointer`}>
<option value="">Select gender</option>
<For each={GENDERS}>{(g) => <option value={g}>{g}</option>}</For>
</select>
</div>
</div>
{/* Profile Photo Upload */}
<div style="margin-top:16px">
<label style={LABEL_STYLE}>Profile Photo</label>
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:40px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
<div style="display:flex;justify-content:center;margin-bottom:8px">
<Upload size={28} color="#FF5E13" />
</div>
<p style="font-size:13px;font-weight:500;color:#374151;margin:0 0 4px 0">Click to upload profile photo</p>
<p style="font-size:12px;color:#6B7280;margin:0">PNG, JPG up to 5MB</p>
</div>
</div>
<div style="margin-top:16px">
<label style={LABEL_STYLE}>Address Line 1 <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="Street address" style={INPUT_STYLE} />
</div>
<div style="margin-top:16px">
<label style={LABEL_STYLE}>Address Line 2</label>
<input type="text" placeholder="Apartment, suite, etc." style={INPUT_STYLE} />
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">
<div>
<label style={LABEL_STYLE}>City <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="Enter city" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>State <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="Enter state" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Country <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="Enter country" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Postal Code <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="Enter postal code" style={INPUT_STYLE} />
</div>
</div>
</div>
</Show>
{/* Work Information */}
<Show when={formTab() === 'work'}>
<div style="padding:24px">
<p style="font-size:13px;color:#6B7280;margin:0 0 20px 0">Map employee to the correct internal structure</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div>
<label style={LABEL_STYLE}>Department <span style="color:#EF4444">*</span></label>
<select style={`${INPUT_STYLE};cursor:pointer`}>
<option value="">Select department</option>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</select>
</div>
<div>
<label style={LABEL_STYLE}>Designation <span style="color:#EF4444">*</span></label>
<select style={`${INPUT_STYLE};cursor:pointer`}>
<option value="">Select designation</option>
<For each={DESIGNATIONS}>{(d) => <option value={d}>{d}</option>}</For>
</select>
</div>
<div>
<label style={LABEL_STYLE}>Internal Role <span style="color:#EF4444">*</span></label>
<select style={`${INPUT_STYLE};cursor:pointer`}>
<option value="">Select internal role</option>
<For each={INTERNAL_ROLES}>{(r) => <option value={r}>{r}</option>}</For>
</select>
</div>
<div>
<label style={LABEL_STYLE}>Reporting Manager</label>
<input type="text" placeholder="Select reporting manager" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Employment Type <span style="color:#EF4444">*</span></label>
<select style={`${INPUT_STYLE};cursor:pointer`}>
<option value="">Select employment type</option>
<For each={EMPLOYMENT_TYPES}>{(t) => <option value={t}>{t}</option>}</For>
</select>
</div>
<div>
<label style={LABEL_STYLE}>Joining Date <span style="color:#EF4444">*</span></label>
<input type="date" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Work Location</label>
<input type="text" placeholder="Enter work location" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Employee Category</label>
<select style={`${INPUT_STYLE};cursor:pointer`}>
<option value="">Select category</option>
<For each={EMPLOYEE_CATEGORIES}>{(c) => <option value={c}>{c}</option>}</For>
</select>
</div>
<div>
<label style={LABEL_STYLE}>Shift Type</label>
<select style={`${INPUT_STYLE};cursor:pointer`}>
<option value="">Select shift type</option>
<For each={SHIFT_TYPES}>{(s) => <option value={s}>{s}</option>}</For>
</select>
</div>
<div>
<label style={LABEL_STYLE}>Status <span style="color:#EF4444">*</span></label>
<select style={`${INPUT_STYLE};cursor:pointer`}>
<option value="ACTIVE">Active</option>
<option value="INACTIVE">Inactive</option>
</select>
</div>
</div>
</div>
</Show>
{/* Access & Permissions */}
<Show when={formTab() === 'access'}>
<div style="padding:24px">
<p style="font-size:13px;color:#6B7280;margin:0 0 20px 0">Configure admin panel access and employee account settings</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div>
<label style={LABEL_STYLE}>Username <span style="color:#EF4444">*</span></label>
<input type="text" placeholder="Enter username" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Password <span style="color:#EF4444">*</span></label>
<input type="password" placeholder="Enter password" style={INPUT_STYLE} />
</div>
<div>
<label style={LABEL_STYLE}>Confirm Password <span style="color:#EF4444">*</span></label>
<input type="password" placeholder="Confirm password" style={INPUT_STYLE} />
</div>
</div>
{/* Toggle rows */}
<div style="margin-top:20px">
{/* Allow Admin Panel Login */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
<div>
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Admin Panel Login</p>
<p style="font-size:13px;color:#6B7280;margin:0">Enable this employee to access the admin panel</p>
</div>
<Toggle on={allowAdminLogin()} onChange={setAllowAdminLogin} />
</div>
{/* Require Password Reset */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
<div>
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Require Password Reset on First Login</p>
<p style="font-size:13px;color:#6B7280;margin:0">Force employee to change password on first login</p>
</div>
<Toggle on={requirePasswordReset()} onChange={setRequirePasswordReset} />
</div>
{/* Enable 2FA */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
<div>
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Enable Two-Factor Authentication</p>
<p style="font-size:13px;color:#6B7280;margin:0">Require 2FA for enhanced security</p>
</div>
<Toggle on={enable2FA()} onChange={setEnable2FA} />
</div>
{/* Account Active */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
<div>
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Account Active / Inactive</p>
<p style="font-size:13px;color:#6B7280;margin:0">Control whether the account is active</p>
</div>
<Toggle on={accountActive()} onChange={setAccountActive} />
</div>
{/* Allow Mobile App */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
<div>
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Access from Mobile App</p>
<p style="font-size:13px;color:#6B7280;margin:0">Enable access from mobile devices</p>
</div>
<Toggle on={allowMobileApp()} onChange={setAllowMobileApp} />
</div>
{/* Allow Reports */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
<div>
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Access to Reports</p>
<p style="font-size:13px;color:#6B7280;margin:0">Grant access to view and download reports</p>
</div>
<Toggle on={allowReports()} onChange={setAllowReports} />
</div>
</div>
</div>
</Show>
{/* Documents */}
<Show when={formTab() === 'docs'}>
<div style="padding:24px">
<p style="font-size:13px;color:#6B7280;margin:0 0 20px 0">Upload and manage employee verification and employment documents</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
{/* ID Proof */}
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">ID Proof</p>
<div style="display:flex;justify-content:center;margin-bottom:6px">
<Upload size={24} color="#FF5E13" />
</div>
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
</div>
{/* Address Proof */}
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Address Proof</p>
<div style="display:flex;justify-content:center;margin-bottom:6px">
<Upload size={24} color="#FF5E13" />
</div>
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
</div>
{/* Offer Letter */}
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Offer Letter</p>
<div style="display:flex;justify-content:center;margin-bottom:6px">
<Upload size={24} color="#FF5E13" />
</div>
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
</div>
{/* Employment Contract */}
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Employment Contract</p>
<div style="display:flex;justify-content:center;margin-bottom:6px">
<Upload size={24} color="#FF5E13" />
</div>
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
</div>
{/* Education Certificate */}
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Education Certificate</p>
<div style="display:flex;justify-content:center;margin-bottom:6px">
<Upload size={24} color="#FF5E13" />
</div>
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
</div>
{/* Experience Certificate */}
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Experience Certificate</p>
<div style="display:flex;justify-content:center;margin-bottom:6px">
<Upload size={24} color="#FF5E13" />
</div>
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
</div>
</div>
{/* Other Supporting Documents */}
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px;text-align:center;margin-top:16px;cursor:pointer" class="hover:bg-[#FAFAFA]">
<div style="display:flex;justify-content:center;margin-bottom:8px">
<Upload size={28} color="#FF5E13" />
</div>
<p style="font-size:13px;font-weight:500;color:#374151;margin:0 0 4px 0">Click to upload additional documents</p>
<p style="font-size:12px;color:#6B7280;margin:0">PDF, DOC, DOCX, JPG, PNG up to 10MB</p>
</div>
</div>
</Show>
{/* Form Footer */}
<div style="display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #E5E7EB">
<button
type="button"
onClick={() => setMainTab('all')}
style="height:38px;padding:0 20px;font-size:14px;font-weight:500;background:white;color:#374151;border:1px solid #E5E7EB;border-radius:8px;cursor:pointer"
>
Cancel
</button>
<button
type="button"
style="background:#0D0D2A;color:white;height:38px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer"
>
Create Employee
</button>
</div>
</div>
</Show>
</div>
</AdminShell> </AdminShell>
); );
} }

View file

@ -1,537 +1,233 @@
import { createMemo, createSignal, For, onMount, Show } from 'solid-js'; import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
import { A } from '@solidjs/router';
import AdminShell from '~/components/AdminShell'; import AdminShell from '~/components/AdminShell';
const API = '/api/gateway'; const API = '/api/gateway';
const LEGACY_INTERNAL_PREVIEW_BASE = String(import.meta.env.VITE_LEGACY_ADMIN_PREVIEW_URL || 'http://localhost:3002').replace(/\/+$/, '');
// ---------- Types ---------- type InternalDashboard = {
type SidebarItem = { key: string; label: string; visible: boolean; order: number }; id: string;
type Field = { id: string; label: string; type: 'text' | 'number' | 'select' | 'date'; required: boolean; placeholder?: string }; name: string;
type Tab = { id: string; title: string; fields: Field[] }; department: string;
type Widget = { id: string; title: string; metric: string; description?: string }; designation: string;
type Section = { id: string; title: string; tabs: Tab[]; widgets: Widget[] }; role: string;
type Dashboard = { id: string; roleId: string; roleName: string; title: string; description?: string; status: string; version: number; sidebar: SidebarItem[]; sections: Section[] }; widgetsCount: number;
type InternalRole = { id: string; key: string; name: string }; status: string;
const FIELD_TYPES: Field['type'][] = ['text', 'number', 'select', 'date'];
const makeId = (prefix: string) => `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
function normalizeInternalDashboardFromConfig(base: Dashboard, configJson: any): Dashboard {
const sidebarRaw = Array.isArray(configJson?.sidebar) ? configJson.sidebar : [];
const sectionsRaw = Array.isArray(configJson?.sections) ? configJson.sections : [];
const legacyNav = Array.isArray(configJson?.nav) ? configJson.nav : [];
const sidebar: SidebarItem[] = sidebarRaw.length > 0
? sidebarRaw.map((item: any, index: number) => ({
key: String(item?.key || makeId('sb')),
label: String(item?.label || `Menu ${index + 1}`),
visible: item?.visible !== false,
order: Number(item?.order) || index + 1,
}))
: legacyNav.map((item: any, index: number) => ({
key: String(item?.key || makeId('sb')),
label: String(item?.label || `Menu ${index + 1}`),
visible: true,
order: index + 1,
}));
const sections: Section[] = sectionsRaw.map((section: any, sectionIndex: number) => ({
id: String(section?.id || `section_${sectionIndex + 1}`),
title: String(section?.title || `Section ${sectionIndex + 1}`),
tabs: Array.isArray(section?.tabs)
? section.tabs.map((tab: any, tabIndex: number) => ({
id: String(tab?.id || `tab_${tabIndex + 1}`),
title: String(tab?.title || `Tab ${tabIndex + 1}`),
fields: Array.isArray(tab?.fields)
? tab.fields.map((field: any, fieldIndex: number) => ({
id: String(field?.id || `field_${fieldIndex + 1}`),
label: String(field?.label || `Field ${fieldIndex + 1}`),
type: field?.type === 'number' || field?.type === 'select' || field?.type === 'date' ? field.type : 'text',
required: Boolean(field?.required),
placeholder: String(field?.placeholder || ''),
}))
: [],
}))
: [],
widgets: Array.isArray(section?.widgets)
? section.widgets.map((widget: any, widgetIndex: number) => ({
id: String(widget?.id || `widget_${widgetIndex + 1}`),
title: String(widget?.title || `Widget ${widgetIndex + 1}`),
metric: String(widget?.metric || '0'),
description: String(widget?.description || ''),
}))
: [],
}));
return {
...base,
title: String(configJson?.title || base.title || 'Internal Dashboard'),
description: String(configJson?.description || ''),
roleName: String(configJson?.roleName || base.roleName || ''),
sidebar,
sections,
}; };
}
// ---------- Preview ---------- const FALLBACK: InternalDashboard[] = [
function PreviewSection(props: { section: Section }) { { id: 'id1', name: 'Super Admin Dashboard', department: 'Administration', designation: 'Super Admin', role: 'Super Admin', widgetsCount: 12, status: 'ACTIVE' },
const [activeTabId, setActiveTabId] = createSignal(props.section.tabs[0]?.id || ''); { id: 'id2', name: 'Engineering Lead Dashboard', department: 'Engineering', designation: 'Engineering Lead', role: 'Engineering Lead', widgetsCount: 8, status: 'ACTIVE' },
const activeTab = createMemo(() => props.section.tabs.find((t) => t.id === activeTabId()) || props.section.tabs[0] || null); { id: 'id3', name: 'HR Manager Dashboard', department: 'Human Resources', designation: 'HR Manager', role: 'HR Admin', widgetsCount: 6, status: 'ACTIVE' },
return ( { id: 'id4', name: 'Finance Dashboard', department: 'Finance', designation: 'Finance Analyst', role: 'Finance Controller', widgetsCount: 5, status: 'DRAFT' },
<div class="preview-section"> ];
<h5 style="margin:0 0 4px;font-size:17px;font-weight:700;color:#0D0D2A">{props.section.title}</h5>
<p style="margin:0;font-size:13px;color:#64748b">Preview tabs, fields, and widgets.</p>
<Show when={props.section.tabs.length > 0}>
<div class="preview-tabs">
{props.section.tabs.map((tab) => (
<button type="button" class={`preview-tab-btn ${activeTabId() === tab.id ? 'active' : ''}`} onClick={() => setActiveTabId(tab.id)}>
{tab.title}
</button>
))}
</div>
</Show>
<Show when={activeTab()}>
<Show when={activeTab()!.fields.length > 0}>
<div class="preview-fields-grid">
{activeTab()!.fields.map((field) => (
<div class="preview-field">
<label>{field.label}{field.required ? <span style="color:#fd6216"> *</span> : null}</label>
{field.type === 'select'
? <select><option>{field.placeholder || `Select ${field.label}`}</option><option>Option A</option><option>Option B</option></select>
: <input type={field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'} placeholder={field.placeholder || `Enter ${field.label}`} />}
</div>
))}
</div>
</Show>
<Show when={activeTab()!.fields.length === 0}>
<div style="margin-top:16px;border:1px dashed #cbd5e1;border-radius:12px;padding:24px;text-align:center;font-size:13px;color:#94a3b8">Add fields to this tab to preview.</div>
</Show>
</Show>
<Show when={props.section.widgets.length > 0}>
<div class="preview-widget-grid">
{props.section.widgets.map((w) => (
<div class="preview-widget">
<div class="w-label">{w.title}</div>
<div class="w-metric">{w.metric}</div>
<div class="w-desc">{w.description || 'Widget description'}</div>
</div>
))}
</div>
</Show>
</div>
);
}
function DashboardPreview(props: { dashboard: Dashboard }) { async function loadDashboards(): Promise<InternalDashboard[]> {
const visibleSidebar = createMemo(() =>
[...props.dashboard.sidebar].filter((i) => i.visible).sort((a, b) => a.order - b.order)
);
const [activeSidebarKey, setActiveSidebarKey] = createSignal('');
const selectedLabel = createMemo(() => visibleSidebar().find((i) => i.key === activeSidebarKey())?.label || 'Overview');
return (
<div class="preview-shell">
<div class="preview-header">
<div>
<h3 style="margin:0;font-size:17px;font-weight:700;color:#0D0D2A">{props.dashboard.title}</h3>
<p style="margin:2px 0 0;font-size:13px;color:#64748b">{props.dashboard.roleName}</p>
</div>
<div style="display:flex;align-items:center;gap:12px">
<div style="width:36px;height:36px;border-radius:999px;background:#fff1e8;display:flex;align-items:center;justify-content:center;color:#c2410c;font-weight:700;font-size:14px">A</div>
</div>
</div>
<div class="preview-layout">
<aside class="preview-sidebar">
{visibleSidebar().map((item) => (
<button
type="button"
class={`preview-sidebar-item ${activeSidebarKey() === item.key ? 'active' : ''}`}
onClick={() => setActiveSidebarKey(item.key)}
>
<span style="width:16px;height:16px;border-radius:4px;background:#cbd5e1;flex-shrink:0;display:inline-block" />
{item.label}
</button>
))}
<Show when={visibleSidebar().length === 0}>
<p style="font-size:12px;color:#94a3b8;padding:8px">No sidebar items added yet.</p>
</Show>
</aside>
<div class="preview-content">
<p style="margin:0;font-size:11px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#fd6216">Dashboard Preview</p>
<h4 style="margin:4px 0 4px;font-size:22px;font-weight:700;color:#0D0D2A">{selectedLabel()}</h4>
<p style="margin:0 0 16px;font-size:13px;color:#64748b">{props.dashboard.description || 'Preview of the internal dashboard layout.'}</p>
<For each={props.dashboard.sections}>
{(section) => <PreviewSection section={section} />}
</For>
<Show when={props.dashboard.sections.length === 0}>
<div style="border:1px dashed #cbd5e1;border-radius:12px;padding:32px;text-align:center;font-size:13px;color:#94a3b8">Add sections, tabs, fields, and widgets to preview here.</div>
</Show>
</div>
</div>
</div>
);
}
// ---------- Main Page ----------
export default function InternalDashboardManagementPage() {
const [dashboards, setDashboards] = createSignal<Dashboard[]>([]);
const [roles, setRoles] = createSignal<InternalRole[]>([]);
const [selectedId, setSelectedId] = createSignal('');
const [activeTab, setActiveTab] = createSignal<'overview' | 'sidebar' | 'sections' | 'preview'>('overview');
const [previewMode, setPreviewMode] = createSignal<'configured' | 'live'>('configured');
const [loading, setLoading] = createSignal(true);
const [saving, setSaving] = createSignal(false);
const [creating, setCreating] = createSignal(false);
const [error, setError] = createSignal('');
onMount(async () => {
await Promise.all([loadDashboards(), loadRoles()]);
});
const loadDashboards = async () => {
try { try {
setLoading(true); const res = await fetch(`${API}/api/admin/internal-dashboards`);
setError(''); if (!res.ok) throw new Error('Failed');
const res = await fetch(`${API}/api/admin/dashboard-config?audience=INTERNAL`);
if (!res.ok) throw new Error('Failed to load internal dashboards');
const data = await res.json(); const data = await res.json();
const rows = (Array.isArray(data) ? data : (data.dashboards || [])) const rows = Array.isArray(data) ? data : (data.dashboards || data.templates || []);
.filter((item: any) => String(item?.audience || '').toUpperCase() === 'INTERNAL') if (!rows.length) return FALLBACK;
.map((item: any) => ({ return rows.map((item: any) => ({
id: String(item.id || ''), id: String(item.id || ''),
roleId: String(item.role_id || ''), name: String(item.name || item.title || 'Untitled Dashboard'),
roleName: '', department: String(item.department || item.assigned_department || ''),
title: 'Internal Dashboard', designation: String(item.designation || item.assigned_designation || ''),
description: '', role: String(item.role || item.role_name || item.assigned_role || ''),
status: item.is_active ? 'published' : 'draft', widgetsCount: Number(item.widgets_count || item.widgetsCount || 0),
version: Number(item.version) || 1, status: item.is_active ? 'ACTIVE' : (String(item.status || 'DRAFT').toUpperCase()),
sidebar: [],
sections: [],
})); }));
setDashboards(rows);
} catch (err: any) {
setError(err.message || 'Failed to load dashboards');
} finally {
setLoading(false);
}
};
const loadRoles = async () => {
try {
const res = await fetch(`${API}/api/admin/roles?audience=INTERNAL`);
if (!res.ok) return;
const data = await res.json();
setRoles(
(Array.isArray(data) ? data : (data.roles || []))
.filter((r: any) => String(r?.audience || '').toUpperCase() === 'INTERNAL')
.map((r: any) => ({ id: String(r.id || ''), key: String(r.key || ''), name: String(r.name || 'Internal Role') })),
);
} catch { setRoles([]); }
};
const hydrateDashboard = async (configId: string) => {
const base = dashboards().find((item) => item.id === configId);
if (!base || !base.roleId) return;
try {
const res = await fetch(`${API}/api/admin/dashboard-config/${base.roleId}?audience=INTERNAL`);
if (!res.ok) return;
const detail = await res.json();
const role = roles().find((item) => item.id === base.roleId);
const hydrated = normalizeInternalDashboardFromConfig(
{
...base,
roleName: role?.name || base.roleName,
status: detail?.is_active ? 'published' : base.status,
version: Number(detail?.version) || base.version,
},
detail?.config_json || {},
);
setDashboards((prev) => prev.map((item) => (item.id === configId ? hydrated : item)));
} catch { } catch {
// Keep list-summary data if detail fetch fails. return FALLBACK;
}
} }
};
const openDashboard = async (configId: string) => { function StatusBadge(props: { status: string }) {
setSelectedId(configId); const isActive = () => props.status === 'ACTIVE';
setActiveTab('overview'); const isDraft = () => props.status === 'DRAFT';
await hydrateDashboard(configId); const baseStyle = 'border-radius:9999px;padding:3px 10px;font-size:12px;font-weight:500;display:inline-block';
}; const activeStyle = `${baseStyle};background:#FFF1EB;color:#FF5E13;border:1px solid #FFD8C2`;
const draftStyle = `${baseStyle};background:#F3F4F6;color:#6B7280;border:1px solid #D1D5DB`;
const otherStyle = `${baseStyle};background:#F0FDF4;color:#166534;border:1px solid #BBF7D0`;
return (
<span style={isActive() ? activeStyle : isDraft() ? draftStyle : otherStyle}>
{props.status}
</span>
);
}
const selected = () => dashboards().find((d) => d.id === selectedId()) || null; export default function InternalDashboardManagementPage() {
const livePreviewUrl = createMemo(() => { const [dashboards, { refetch }] = createResource(loadDashboards);
const roleId = String(selected()?.roleId || '').trim(); const [search, setSearch] = createSignal('');
if (!roleId) return `${LEGACY_INTERNAL_PREVIEW_BASE}/internal-dashboard-management`; const [statusFilter, setStatusFilter] = createSignal('All');
return `${LEGACY_INTERNAL_PREVIEW_BASE}/internal-dashboard-management?roleId=${encodeURIComponent(roleId)}`; const [activeTab, setActiveTab] = createSignal<'dashboard' | 'preview'>('dashboard');
const filtered = createMemo(() => {
const q = search().toLowerCase();
const st = statusFilter();
return (dashboards() || FALLBACK).filter((d) => {
const matchesSearch = !q || d.name.toLowerCase().includes(q) || d.department.toLowerCase().includes(q) || d.role.toLowerCase().includes(q);
const matchesStatus = st === 'All' || d.status === st;
return matchesSearch && matchesStatus;
}); });
const livePreviewRoleLabel = createMemo(() => String(selected()?.roleName || '').trim() || 'Unlinked Role');
const update = (patch: Partial<Dashboard>) =>
setDashboards((prev) => prev.map((d) => d.id === selectedId() ? { ...d, ...patch } : d));
const updateSection = (sectionId: string, patch: Partial<Section>) =>
update({ sections: selected()!.sections.map((s) => s.id === sectionId ? { ...s, ...patch } : s) });
const updateTab = (sectionId: string, tabId: string, patch: Partial<Tab>) => {
const section = selected()!.sections.find((s) => s.id === sectionId)!;
updateSection(sectionId, { tabs: section.tabs.map((t) => t.id === tabId ? { ...t, ...patch } : t) });
};
const updateField = (sId: string, tId: string, fId: string, patch: Partial<Field>) => {
const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!;
updateTab(sId, tId, { fields: tab.fields.map((f) => f.id === fId ? { ...f, ...patch } : f) });
};
const updateWidget = (sId: string, wId: string, patch: Partial<Widget>) => {
const section = selected()!.sections.find((s) => s.id === sId)!;
updateSection(sId, { widgets: section.widgets.map((w) => w.id === wId ? { ...w, ...patch } : w) });
};
const addSidebarItem = () => {
const items = selected()!.sidebar;
update({ sidebar: [...items, { key: makeId('sb'), label: 'New Sidebar Item', visible: true, order: items.length + 1 }] });
};
const removeSidebarItem = (key: string) =>
update({ sidebar: selected()!.sidebar.filter((i) => i.key !== key).map((i, idx) => ({ ...i, order: idx + 1 })) });
const addSection = () =>
update({ sections: [...selected()!.sections, { id: makeId('sec'), title: 'New Section', tabs: [], widgets: [] }] });
const removeSection = (id: string) =>
update({ sections: selected()!.sections.filter((s) => s.id !== id) });
const addTab = (sId: string) => {
const section = selected()!.sections.find((s) => s.id === sId)!;
updateSection(sId, { tabs: [...section.tabs, { id: makeId('tab'), title: 'New Tab', fields: [] }] });
};
const removeTab = (sId: string, tId: string) => {
const section = selected()!.sections.find((s) => s.id === sId)!;
updateSection(sId, { tabs: section.tabs.filter((t) => t.id !== tId) });
};
const addField = (sId: string, tId: string) => {
const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!;
updateTab(sId, tId, { fields: [...tab.fields, { id: makeId('fld'), label: 'New Field', type: 'text', required: false, placeholder: 'Enter value' }] });
};
const removeField = (sId: string, tId: string, fId: string) => {
const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!;
updateTab(sId, tId, { fields: tab.fields.filter((f) => f.id !== fId) });
};
const addWidget = (sId: string) => {
const section = selected()!.sections.find((s) => s.id === sId)!;
updateSection(sId, { widgets: [...section.widgets, { id: makeId('wgt'), title: 'New Widget', metric: '0', description: 'Describe this widget.' }] });
};
const removeWidget = (sId: string, wId: string) => {
const section = selected()!.sections.find((s) => s.id === sId)!;
updateSection(sId, { widgets: section.widgets.filter((w) => w.id !== wId) });
};
const createDashboard = async () => {
try {
setCreating(true);
setError('');
let newId = makeId('local');
const defaultRole = roles()[0];
try {
if (defaultRole?.id) {
const res = await fetch(`${API}/api/admin/dashboard-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
role_id: defaultRole.id,
audience: 'INTERNAL',
config_json: { title: `${defaultRole.name} Dashboard`, description: '', roleName: defaultRole.name, version: 1, sidebar: [], sections: [], nav: [] },
}),
}); });
if (res.ok) {
const data = await res.json();
newId = data.id;
}
}
} catch { /* backend unavailable — use local draft */ }
const nd: Dashboard = {
id: newId,
roleId: defaultRole?.id || '',
roleName: defaultRole?.name || '',
title: `${defaultRole?.name || 'New Internal'} Dashboard`,
status: 'draft',
version: 1,
sidebar: [],
sections: [],
};
setDashboards((prev) => [nd, ...prev]);
setSelectedId(newId);
setActiveTab('overview');
} finally {
setCreating(false);
}
};
const saveSelected = async () => { const totalTemplates = createMemo(() => (dashboards() || FALLBACK).length);
const d = selected(); const activeTemplates = createMemo(() => (dashboards() || FALLBACK).filter((d) => d.status === 'ACTIVE').length);
if (!d) return; const draftTemplates = createMemo(() => (dashboards() || FALLBACK).filter((d) => d.status === 'DRAFT').length);
if (!d.roleId) { const assignedRoles = createMemo(() => {
setError('Please select an internal role before saving.'); const roles = new Set((dashboards() || FALLBACK).map((d) => d.role).filter(Boolean));
return; return roles.size;
}
try {
setSaving(true);
setError('');
const nav = d.sidebar
.filter((item) => item.visible)
.sort((a, b) => a.order - b.order)
.map((item) => ({
key: item.key,
label: item.label,
path: '/internal-dashboard-management',
}));
const res = await fetch(`${API}/api/admin/dashboard-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
role_id: d.roleId,
audience: 'INTERNAL',
config_json: { title: d.title, description: d.description, roleName: d.roleName, version: d.version, sidebar: d.sidebar, sections: d.sections, nav },
}),
}); });
if (!res.ok) throw new Error('Failed to save dashboard'); const unassignedRoles = createMemo(() => (dashboards() || FALLBACK).filter((d) => !d.role).length);
await loadDashboards();
const next = dashboards().find((item) => item.roleId === d.roleId); const pillActive = 'background:#0D0D2A;color:white;border:none;height:32px;border-radius:9999px;padding:0 16px;font-size:13px;font-weight:500;cursor:pointer';
if (next) await openDashboard(next.id); const pillInactive = 'background:transparent;color:#6B7280;border:1px solid #E5E7EB;height:32px;border-radius:9999px;padding:0 16px;font-size:13px;font-weight:500;cursor:pointer';
} catch (err: any) {
setError(err.message || 'Failed to save dashboard');
} finally {
setSaving(false);
}
};
// ---------- List view ----------
return ( return (
<AdminShell> <AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full"> <div style="width:100%;padding-bottom:32px">
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
<Show when={!selected()}> {/* Header */}
{/* Header & Title */} <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
<div> <div>
<h1 class="text-[32px] font-bold text-[#0D0D2A] leading-tight">Internal Dashboard Management</h1> <h1 style="font-size:28px;font-weight:700;color:#111827">Internal Dashboard Management</h1>
<p class="text-[15px] text-[#8087a0] mt-1">Configure dashboards for internal staff members</p> <p style="margin-top:4px;font-size:14px;color:#6B7280">Manage internal dashboard templates for departments, designations, and roles.</p>
</div> </div>
<div class="flex items-center gap-3"> <div style="display:flex;gap:8px;align-items:center;flex-shrink:0;margin-top:4px">
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0D0D2A] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]"> <button style={activeTab() === 'dashboard' ? pillActive : pillInactive} onClick={() => setActiveTab('dashboard')}>
Dashboard Management Dashboard Management
</button> </button>
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]"> <button style={activeTab() === 'preview' ? pillActive : pillInactive} onClick={() => setActiveTab('preview')}>
Role Preview Role Preview
</button> </button>
</div> </div>
</div> </div>
{/* 5 KPI Cards Row */} {/* Stats Row */}
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8"> <div style="display:flex;gap:16px;margin-top:24px">
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm"> <div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p class="text-[13px] text-[#8087a0] leading-snug">Total Dashboard<br/>Templates</p> <p style="font-size:13px;color:#6B7280;margin:0">Total Dashboard Templates</p>
<p class="mt-3 text-[32px] font-bold text-[#0D0D2A] leading-none">18</p> <p style="font-size:28px;font-weight:700;color:#111827;margin:4px 0 0">{dashboards.loading ? '—' : totalTemplates()}</p>
</div> </div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm"> <div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p class="text-[13px] text-[#8087a0] leading-snug">Active Templates</p> <p style="font-size:13px;color:#6B7280;margin:0">Active Templates</p>
<p class="mt-3 text-[32px] font-bold text-[#00c853] leading-none">14</p> <p style="font-size:28px;font-weight:700;color:#FF5E13;margin:4px 0 0">{dashboards.loading ? '—' : activeTemplates()}</p>
</div> </div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm"> <div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p class="text-[13px] text-[#8087a0] leading-snug">Draft Templates</p> <p style="font-size:13px;color:#6B7280;margin:0">Draft Templates</p>
<p class="mt-3 text-[32px] font-bold text-[#64748b] leading-none">2</p> <p style="font-size:28px;font-weight:700;color:#6B7280;margin:4px 0 0">{dashboards.loading ? '—' : draftTemplates()}</p>
</div> </div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm"> <div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p class="text-[13px] text-[#8087a0] leading-snug">Assigned Roles</p> <p style="font-size:13px;color:#6B7280;margin:0">Assigned Roles</p>
<p class="mt-3 text-[32px] font-bold text-[#2962ff] leading-none">9</p> <p style="font-size:28px;font-weight:700;color:#3730A3;margin:4px 0 0">{dashboards.loading ? '—' : assignedRoles()}</p>
</div> </div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm"> <div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p class="text-[13px] text-[#8087a0] leading-snug">Unassigned Roles</p> <p style="font-size:13px;color:#6B7280;margin:0">Unassigned Roles</p>
<p class="mt-3 text-[32px] font-bold text-[#ff6e30] leading-none">2</p> <p style="font-size:28px;font-weight:700;color:#111827;margin:4px 0 0">{dashboards.loading ? '—' : unassignedRoles()}</p>
</div> </div>
</div> </div>
{/* Main Table Section */} {/* Table Card */}
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full"> <div style="margin-top:24px;border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
<div class="rounded-[20px] bg-white p-5">
{/* Table Action Header */} {/* Card Header */}
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between mb-6"> <div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #F3F4F6">
<h2 class="text-[22px] font-bold text-[#0D0D2A]">Internal Dashboard<br/>Templates</h2> <h2 style="font-size:16px;font-weight:600;color:#111827;margin:0">Internal Dashboard Templates</h2>
<div class="flex items-center gap-3"> <div style="display:flex;gap:8px">
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]"> <button style="border:1px solid #E5E7EB;background:white;color:#374151;height:36px;border-radius:8px;padding:0 14px;font-size:13px;font-weight:500;cursor:pointer">
Import Layout Import Layout
</button> </button>
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]"> <button style="border:1px solid #E5E7EB;background:white;color:#374151;height:36px;border-radius:8px;padding:0 14px;font-size:13px;font-weight:500;cursor:pointer">
Export Config Export Config
</button> </button>
<button class="inline-flex h-[42px] items-center justify-center rounded-xl bg-[#0D0D2A] px-5 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" onClick={createDashboard} disabled={creating()}> <A
<span class="mr-2 text-lg leading-none">+</span> {creating() ? 'Creating...' : 'Create Dashboard Template'} href="/admin/internal-dashboard-management/new"
</button> style="background:#0D0D2A;color:white;border:none;height:36px;border-radius:8px;padding:0 16px;font-size:13px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;text-decoration:none"
>
<span>+</span> Create Dashboard Template
</A>
</div> </div>
</div> </div>
{/* Error Message */} {/* Filter Bar */}
<Show when={error()}> <div style="display:flex;gap:12px;align-items:center;padding:12px 20px;border-bottom:1px solid #F3F4F6">
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div> <div style="position:relative;flex:1;max-width:320px">
</Show> <svg style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#9CA3AF" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
{/* Filters Row */}
<div class="flex items-center gap-4 mb-6">
<div class="relative w-[320px]">
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input <input
type="text" type="text"
placeholder="Search templates..." placeholder="Search dashboards..."
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white" value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="height:36px;width:100%;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px 0 32px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
/> />
</div> </div>
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div> <select
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div> value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
style="height:36px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer"
>
<option value="All">Status: All</option>
<option value="ACTIVE">Active</option>
<option value="DRAFT">Draft</option>
</select>
<select style="height:36px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer">
<option>Department: All</option>
<option>Administration</option>
<option>Engineering</option>
<option>Human Resources</option>
<option>Finance</option>
</select>
</div> </div>
{/* Table */} {/* Table */}
<div class="overflow-x-auto"> <div style="overflow-x:auto">
<table class="w-full min-w-[1000px] border-collapse"> <table style="width:100%;border-collapse:collapse;min-width:900px">
<thead> <thead>
<tr class="bg-[#0D0D2A] text-left text-white"> <tr style="background:#0D0D2A">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl">DASHBOARD NAME</th> <th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">DASHBOARD NAME</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED DEPARTMENT</th> <th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ASSIGNED DEPARTMENT</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED DESIGNATION</th> <th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ASSIGNED DESIGNATION</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED INTERNAL ROLE</th> <th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ASSIGNED INTERNAL ROLE</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-center">WIDGETS COUNT</th> <th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">WIDGETS COUNT</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tr-xl text-center">STATUS</th> <th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">STATUS</th>
<th style="padding:12px 20px;text-align:right;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ACTIONS</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<Show when={loading()}> <Show when={dashboards.loading}>
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-sm">Loading templates...</td></tr> <tr>
<td colspan="7" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">Loading dashboards...</td>
</tr>
</Show> </Show>
<Show when={!loading() && dashboards().length === 0}> <Show when={!dashboards.loading && filtered().length === 0}>
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-sm">No dashboard templates found.</td></tr> <tr>
<td colspan="7" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">No dashboard templates found.</td>
</tr>
</Show> </Show>
<For each={dashboards()}> <For each={filtered()}>
{(d) => ( {(dashboard, index) => (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc] cursor-pointer" onClick={() => void openDashboard(d.id)}> <tr style={`border-bottom:1px solid #F3F4F6;background:${index() % 2 === 0 ? 'white' : '#FAFAFA'}`}>
<td class="px-6 py-4 text-[14px] font-bold text-[#0D0D2A]">{d.title}</td> <td style="padding:12px 20px;font-size:14px;color:#111827;font-weight:500">{dashboard.name}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Administration</td> <td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.department || '—'}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Super Admin</td> <td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.designation || '—'}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{d.roleName || 'Unassigned'}</td> <td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.role || '—'}</td>
<td class="px-6 py-4 text-[14px] text-[#475569] text-center">{d.sections.length * 3 || 12}</td> <td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.widgetsCount}</td>
<td class="px-6 py-4 text-center"> <td style="padding:12px 20px">
<span class={`inline-flex items-center justify-center rounded-lg px-3 py-1 text-[12px] font-bold ${ <StatusBadge status={dashboard.status} />
d.status === 'published' ? 'bg-[#e6f9ed] text-[#00c853]' : 'bg-[#f1f5f9] text-[#64748b]' </td>
}`}> <td style="padding:12px 20px;text-align:right">
{d.status === 'published' ? 'Active' : 'Draft'} <div style="display:inline-flex;gap:8px">
</span> <A
href={`/admin/internal-dashboard-management/${dashboard.id}`}
style="font-size:13px;color:#FF5E13;font-weight:500;text-decoration:none;border:1px solid #FFD8C2;border-radius:6px;padding:4px 10px;background:#FFF1EB"
>
Edit
</A>
<button style="font-size:13px;color:#6B7280;font-weight:500;border:1px solid #E5E7EB;border-radius:6px;padding:4px 10px;background:white;cursor:pointer">
Preview
</button>
</div>
</td> </td>
</tr> </tr>
)} )}
@ -539,267 +235,16 @@ export default function InternalDashboardManagementPage() {
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</section>
</Show>
{/* ---------- Builder view ---------- */} {/* Pagination */}
<Show when={selected()}> <div style="display:flex;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px;align-items:center">
<div class="builder-header"> <span style="font-size:13px;color:#6B7280">Showing {filtered().length} of {(dashboards() || FALLBACK).length} templates</span>
<div> <div style="display:flex;gap:4px">
<h2>Internal Dashboard Builder</h2> <button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:white;border-radius:6px;font-size:13px;color:#374151;cursor:pointer;padding:0 10px">Previous</button>
<p>Manage menu items, sections, tabs, form fields, and summary cards from one place.</p> <button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:#0D0D2A;border-radius:6px;font-size:13px;color:white;cursor:pointer;padding:0 10px">1</button>
</div> <button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:white;border-radius:6px;font-size:13px;color:#374151;cursor:pointer;padding:0 10px">Next</button>
<div class="builder-header-actions">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => setSelectedId('')}>Back to List</button>
<button class="btn-primary" onClick={saveSelected} disabled={saving()}>
{saving() ? 'Saving...' : 'Save Dashboard'}
</button>
</div> </div>
</div> </div>
<Show when={error()}><div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div></Show>
{/* Tab bar */}
<div class="builder-tab-bar">
{(['overview', 'sidebar', 'sections', 'preview'] as const).map((t) => (
<button
type="button"
class={`builder-tab-btn ${activeTab() === t ? 'active' : ''}`}
onClick={() => setActiveTab(t)}
>
{t === 'sections' ? 'Sections, Tabs & Fields' : t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
{/* Overview */}
<Show when={activeTab() === 'overview'}>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Team Role</label>
<select
value={selected()!.roleId}
onChange={(e) => {
const role = roles().find((r) => r.id === e.currentTarget.value);
update({ roleId: e.currentTarget.value, roleName: role?.name || '', title: role ? `${role.name} Dashboard` : selected()!.title });
}}
>
<option value="">Select team role</option>
{roles().map((r) => <option value={r.id}>{r.name}</option>)}
</select>
</div>
<div class="field">
<label>Dashboard Title</label>
<input value={selected()!.title} onInput={(e) => update({ title: e.currentTarget.value })} />
</div>
<div class="field">
<label>Short description</label>
<input value={selected()!.description || ''} onInput={(e) => update({ description: e.currentTarget.value })} />
</div>
<div class="info-box">
<p style="margin:0 0 4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#64748b">Linked Role</p>
<p style="margin:0;font-weight:600;color:#0f172a">{selected()!.roleName || 'No role selected yet'}</p>
<p style="margin:2px 0 0;font-size:12px;color:#64748b">{selected()!.roleId || 'Select a team role to connect this dashboard.'}</p>
</div>
</div>
</Show>
{/* Sidebar */}
<Show when={activeTab() === 'sidebar'}>
<div class="builder-section">
<div class="sub-card-header">
<h4>Menu</h4>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSidebarItem}>Add Menu Item</button>
</div>
<For each={selected()!.sidebar}>
{(item, idx) => (
<div class="builder-item builder-item-row-4">
<input
value={item.label}
placeholder="Menu label"
onInput={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, label: e.currentTarget.value } : i) })}
/>
<input
type="number"
value={item.order}
onInput={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, order: Number(e.currentTarget.value) || idx() + 1 } : i) })}
style="width:80px"
/>
<label class="checkbox-label" style="justify-content:center">
<input
type="checkbox"
checked={item.visible}
onChange={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, visible: e.currentTarget.checked } : i) })}
/>
Show
</label>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSidebarItem(item.key)}>Remove</button>
</div>
)}
</For>
<Show when={selected()!.sidebar.length === 0}>
<p class="notice">No menu items yet. Add your first menu item.</p>
</Show>
</div>
</Show>
{/* Sections, Tabs & Fields */}
<Show when={activeTab() === 'sections'}>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<h3 style="margin:0;font-size:15px;font-weight:700">Sections</h3>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSection}>Add Section</button>
</div>
<For each={selected()!.sections}>
{(section) => (
<div class="builder-section">
<div class="builder-section-header">
<input
value={section.title}
onInput={(e) => updateSection(section.id, { title: e.currentTarget.value })}
placeholder="Section title"
/>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSection(section.id)}>Remove</button>
</div>
{/* Tabs */}
<div class="sub-card">
<div class="sub-card-header">
<h4>Tabs and Form Fields</h4>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addTab(section.id)}>Add Tab</button>
</div>
<For each={section.tabs}>
{(tab) => (
<div class="nested-card">
<div class="nested-card-header">
<input
value={tab.title}
onInput={(e) => updateTab(section.id, tab.id, { title: e.currentTarget.value })}
placeholder="Tab title"
/>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeTab(section.id, tab.id)}>Remove</button>
</div>
<div style="display:flex;justify-content:flex-end;margin-bottom:8px">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addField(section.id, tab.id)}>Add Field</button>
</div>
<For each={tab.fields}>
{(field) => (
<div class="field-row">
<div>
<input
value={field.label}
onInput={(e) => updateField(section.id, tab.id, field.id, { label: e.currentTarget.value })}
placeholder="Field label"
style="width:100%;margin-bottom:4px"
/>
<input
value={field.placeholder || ''}
onInput={(e) => updateField(section.id, tab.id, field.id, { placeholder: e.currentTarget.value })}
placeholder="Help text inside the input"
style="width:100%"
/>
</div>
<select
value={field.type}
onChange={(e) => updateField(section.id, tab.id, field.id, { type: e.currentTarget.value as Field['type'] })}
>
{FIELD_TYPES.map((t) => <option value={t}>{t}</option>)}
</select>
<label class="checkbox-label" style="justify-content:center">
<input
type="checkbox"
checked={field.required}
onChange={(e) => updateField(section.id, tab.id, field.id, { required: e.currentTarget.checked })}
/>
Required
</label>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeField(section.id, tab.id, field.id)}>Remove</button>
</div>
)}
</For>
<Show when={tab.fields.length === 0}>
<p class="notice" style="text-align:center">No fields in this tab yet.</p>
</Show>
</div>
)}
</For>
<Show when={section.tabs.length === 0}>
<p class="notice">No tabs yet. Add a tab above.</p>
</Show>
</div>
{/* Widgets */}
<div class="sub-card" style="margin-top:8px">
<div class="sub-card-header">
<h4>Widgets</h4>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addWidget(section.id)}>Add Widget</button>
</div>
<For each={section.widgets}>
{(widget) => (
<div class="widget-item">
<input value={widget.title} onInput={(e) => updateWidget(section.id, widget.id, { title: e.currentTarget.value })} placeholder="Widget title" />
<input value={widget.metric} onInput={(e) => updateWidget(section.id, widget.id, { metric: e.currentTarget.value })} placeholder="Metric value e.g. 42" />
<textarea rows={2} value={widget.description || ''} onInput={(e) => updateWidget(section.id, widget.id, { description: e.currentTarget.value })} placeholder="Widget description" />
<div style="display:flex;justify-content:flex-end">
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" style="font-size:12px;padding:5px 10px" onClick={() => removeWidget(section.id, widget.id)}>Remove Widget</button>
</div>
</div>
)}
</For>
<Show when={section.widgets.length === 0}>
<p class="notice">No widgets yet.</p>
</Show>
</div>
</div>
)}
</For>
<Show when={selected()!.sections.length === 0}>
<div style="text-align:center;padding:32px;border:1px dashed #cbd5e1;border-radius:12px;color:#94a3b8;font-size:13px">No sections yet. Add the first section above.</div>
</Show>
</Show>
{/* Preview */}
<Show when={activeTab() === 'preview'}>
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:12px">
<p class="notice" style="margin:0">Preview this dashboard as a sample view or open the live version.</p>
<div class="admin-segmented" style="margin:0">
<button
type="button"
class={`admin-segment ${previewMode() === 'configured' ? 'active' : ''}`}
onClick={() => setPreviewMode('configured')}
>
Sample Preview
</button>
<button
type="button"
class={`admin-segment ${previewMode() === 'live' ? 'active' : ''}`}
onClick={() => setPreviewMode('live')}
>
Live Preview
</button>
</div>
</div>
<Show when={previewMode() === 'configured'}>
<DashboardPreview dashboard={selected()!} />
</Show>
<Show when={previewMode() === 'live'}>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px">
<span class="meta-chip">Role: {livePreviewRoleLabel()}</span>
<Show when={selected()!.roleId}>
<span class="meta-chip">Role ID: {selected()!.roleId}</span>
</Show>
<a class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={livePreviewUrl()} target="_blank" rel="noreferrer">Open Full Page Preview</a>
</div>
<iframe
src={livePreviewUrl()}
title="Live Internal Dashboard Preview"
style="width:100%;height:760px;border:1px solid #e2e8f0;border-radius:14px;background:#fff"
/>
</Show>
</Show>
</Show>
</div> </div>
</div> </div>
</AdminShell> </AdminShell>

View file

@ -1,13 +1,393 @@
import { onMount } from 'solid-js'; import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
import { useNavigate } from '@solidjs/router'; import { A } from '@solidjs/router';
import AdminShell from '~/components/AdminShell'; import AdminShell from '~/components/AdminShell';
import { MoreVertical } from 'lucide-solid';
const API = '/api/gateway';
type OnboardingFlow = {
id: string;
name: string;
userType: string;
totalSteps: number;
requiredDocs: number;
verificationType: string;
status: string;
lastUpdated: string;
};
const FALLBACK_FLOWS: OnboardingFlow[] = [
{ id: 'f1', name: 'Customer Service Onboarding', userType: 'Customer', totalSteps: 6, requiredDocs: 2, verificationType: 'Identity Verification', status: 'ACTIVE', lastUpdated: '2024-03-20' },
{ id: 'f2', name: 'Professional Photographer Onboarding', userType: 'Professional', totalSteps: 6, requiredDocs: 3, verificationType: 'Identity Verification', status: 'ACTIVE', lastUpdated: '2024-03-19' },
{ id: 'f3', name: 'Company Hiring Onboarding', userType: 'Company', totalSteps: 6, requiredDocs: 4, verificationType: 'Business Verification', status: 'ACTIVE', lastUpdated: '2024-03-18' },
{ id: 'f4', name: 'Jobseeker Profile Onboarding', userType: 'Jobseeker', totalSteps: 6, requiredDocs: 2, verificationType: 'Identity Verification', status: 'ACTIVE', lastUpdated: '2024-03-17' },
{ id: 'f5', name: 'Professional Developer Onboarding', userType: 'Professional', totalSteps: 6, requiredDocs: 2, verificationType: 'Identity Verification', status: 'DRAFT', lastUpdated: '2024-03-16' },
{ id: 'f6', name: 'Customer Requirements Collection', userType: 'Customer', totalSteps: 5, requiredDocs: 1, verificationType: 'No Verification', status: 'INACTIVE', lastUpdated: '2024-03-15' },
];
async function loadFlows(): Promise<OnboardingFlow[]> {
try {
const res = await fetch(`${API}/api/admin/onboarding-schemas`);
if (!res.ok) {
const res2 = await fetch(`${API}/api/admin/onboarding-flows`);
if (!res2.ok) throw new Error('Failed');
const data2 = await res2.json();
const rows2 = Array.isArray(data2) ? data2 : (data2.flows || data2.schemas || []);
if (!rows2.length) return FALLBACK_FLOWS;
return rows2.map((item: any) => ({
id: String(item.id || ''),
name: String(item.name || item.title || 'Untitled Flow'),
userType: String(item.user_type || item.userType || item.role_key || ''),
totalSteps: Number(item.step_count || item.steps || item.totalSteps || 0),
requiredDocs: Number(item.required_docs || item.requiredDocs || 0),
verificationType: String(item.verification_type || item.verificationType || ''),
status: item.is_active ? 'ACTIVE' : String(item.status || 'DRAFT').toUpperCase(),
lastUpdated: String(item.updated_at || item.lastUpdated || '').slice(0, 10),
}));
}
const data = await res.json();
const rows = Array.isArray(data) ? data : (data.flows || data.schemas || []);
if (!rows.length) return FALLBACK_FLOWS;
return rows.map((item: any) => ({
id: String(item.id || ''),
name: String(item.name || item.title || 'Untitled Flow'),
userType: String(item.user_type || item.userType || item.role_key || ''),
totalSteps: Number(item.step_count || item.steps || item.totalSteps || 0),
requiredDocs: Number(item.required_docs || item.requiredDocs || 0),
verificationType: String(item.verification_type || item.verificationType || ''),
status: item.is_active ? 'ACTIVE' : String(item.status || 'DRAFT').toUpperCase(),
lastUpdated: String(item.updated_at || item.lastUpdated || '').slice(0, 10),
}));
} catch {
return FALLBACK_FLOWS;
}
}
function StatusBadge(props: { status: string }) {
const s = props.status;
if (s === 'ACTIVE') {
return (
<span style="display:inline-flex;border-radius:9999px;border:1px solid #FFD8C2;background:#FFF1EB;color:#FF5E13;padding:2px 10px;font-size:12px;font-weight:500">
Active
</span>
);
}
if (s === 'DRAFT') {
return (
<span style="display:inline-flex;border-radius:9999px;border:1px solid #FDE68A;background:#FFFBEB;color:#D97706;padding:2px 10px;font-size:12px;font-weight:500">
Draft
</span>
);
}
return (
<span style="display:inline-flex;border-radius:9999px;border:1px solid #D1D5DB;background:#F3F4F6;color:#4B5563;padding:2px 10px;font-size:12px;font-weight:500">
Inactive
</span>
);
}
function VerificationBadge(props: { type: string }) {
const t = props.type;
if (t === 'Identity Verification') {
return (
<span style="display:inline-flex;border-radius:9999px;border:1px solid #BFDBFE;background:#EFF6FF;color:#1D4ED8;padding:2px 10px;font-size:12px;font-weight:500">
{t}
</span>
);
}
if (t === 'Business Verification') {
return (
<span style="display:inline-flex;border-radius:9999px;border:1px solid #E9D5FF;background:#F5F3FF;color:#7C3AED;padding:2px 10px;font-size:12px;font-weight:500">
{t}
</span>
);
}
return (
<span style="display:inline-flex;border-radius:9999px;border:1px solid #D1D5DB;background:#F3F4F6;color:#4B5563;padding:2px 10px;font-size:12px;font-weight:500">
{t || '—'}
</span>
);
}
function ActionsMenu(props: { flowId: string; status: string }) {
const [open, setOpen] = createSignal(false);
return (
<div style="position:relative;display:inline-block">
<button
onClick={() => setOpen((v) => !v)}
style="width:32px;height:32px;border-radius:6px;border:1px solid #E5E7EB;background:white;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:#6B7280"
>
<MoreVertical size={15} />
</button>
<Show when={open()}>
<div
style="position:absolute;right:0;top:36px;background:white;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.10);z-index:100;min-width:168px;padding:4px 0"
onMouseLeave={() => setOpen(false)}
>
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
View Flow
</button>
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Edit Flow
</button>
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
Duplicate Flow
</button>
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
<Show when={props.status !== 'ACTIVE'}>
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Activate Flow
</button>
</Show>
<Show when={props.status === 'ACTIVE'}>
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
Deactivate Flow
</button>
</Show>
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#EF4444;background:none;border:none;cursor:pointer;text-align:left">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
Delete Flow
</button>
</div>
</Show>
</div>
);
}
export default function OnboardingManagementPage() {
const [flows] = createResource(loadFlows);
const [search, setSearch] = createSignal('');
const [statusFilter, setStatusFilter] = createSignal('All');
const [userTypeFilter, setUserTypeFilter] = createSignal('All');
const [activeTab, setActiveTab] = createSignal<'flow' | 'preview'>('flow');
const allFlows = () => flows() || FALLBACK_FLOWS;
const filtered = createMemo(() => {
const q = search().toLowerCase();
const st = statusFilter();
const ut = userTypeFilter();
return allFlows().filter((f) => {
const matchesSearch = !q || f.name.toLowerCase().includes(q) || f.userType.toLowerCase().includes(q);
const matchesStatus = st === 'All' || f.status === st;
const matchesType = ut === 'All' || f.userType === ut;
return matchesSearch && matchesStatus && matchesType;
});
});
const totalFlows = () => allFlows().length;
const activeFlows = () => allFlows().filter((f) => f.status === 'ACTIVE').length;
const draftFlows = () => allFlows().filter((f) => f.status === 'DRAFT').length;
const requireVerification = () => allFlows().filter((f) => f.verificationType && f.verificationType !== 'No Verification').length;
const recentlyUpdated = () => allFlows().filter((f) => f.lastUpdated >= '2024-03-18').length;
const STATS = [
{ label: 'Total Flows', value: () => totalFlows(), color: '#111827' },
{ label: 'Active Flows', value: () => activeFlows(), color: '#10B981' },
{ label: 'Draft Flows', value: () => draftFlows(), color: '#F59E0B' },
{ label: 'Flows Requiring Verification', value: () => requireVerification(), color: '#FF5E13' },
{ label: 'Recently Updated', value: () => recentlyUpdated(), color: '#3B82F6' },
];
export default function OnboardingManagementAliasPage() {
const navigate = useNavigate();
onMount(() => navigate('/admin/onboarding-schemas', { replace: true }));
return ( return (
<AdminShell> <AdminShell>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to onboarding management...</p></div> <div style="width:100%;padding-bottom:32px">
{/* Page Header */}
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:24px">
<div>
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0 0 4px 0">External Onboarding Management</h1>
<p style="font-size:14px;color:#6B7280;margin:0">Create and manage onboarding flows for external users.</p>
</div>
<div style="display:flex;border-radius:10px;overflow:hidden;border:1px solid #E5E7EB">
<button
onClick={() => setActiveTab('flow')}
style={activeTab() === 'flow'
? 'background:#0D0D2A;color:white;padding:8px 16px;font-size:13px;font-weight:600;border:none;cursor:pointer;border-radius:0'
: 'background:white;color:#374151;padding:8px 16px;font-size:13px;font-weight:500;border-left:1px solid #E5E7EB;border:1px solid #E5E7EB;cursor:pointer'}
>
Flow Management
</button>
<button
onClick={() => setActiveTab('preview')}
style={activeTab() === 'preview'
? 'background:#0D0D2A;color:white;padding:8px 16px;font-size:13px;font-weight:600;border:none;cursor:pointer;border-radius:0'
: 'background:white;color:#374151;padding:8px 16px;font-size:13px;font-weight:500;border-left:1px solid #E5E7EB;border:1px solid #E5E7EB;cursor:pointer'}
>
User Preview
</button>
</div>
</div>
{/* Stats Cards */}
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:24px">
<For each={STATS}>
{(stat) => (
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px">
<p style="font-size:12px;color:#6B7280;margin:0 0 8px 0">{stat.label}</p>
<p style={`font-size:28px;font-weight:700;color:${stat.color};margin:0`}>
{flows.loading ? '—' : stat.value()}
</p>
</div>
)}
</For>
</div>
{/* Flow Management View */}
<Show when={activeTab() === 'flow'}>
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
{/* Section Header */}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0;padding:16px 20px;border-bottom:1px solid #F3F4F6">
<h2 style="font-size:18px;font-weight:700;color:#111827;margin:0">Onboarding Flow Management</h2>
<div style="display:flex;gap:8px;align-items:center">
<button style="height:34px;padding:0 14px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer">
Import Flow
</button>
<button style="height:34px;padding:0 14px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer">
Export Flow
</button>
<A
href="/admin/onboarding-management/new"
style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px;text-decoration:none"
>
+ Create Onboarding Flow
</A>
</div>
</div>
{/* Filter Bar */}
<div style="display:flex;gap:8px;margin-bottom:0;padding:12px 20px;border-bottom:1px solid #F3F4F6">
<div style="position:relative;flex:1;max-width:280px">
<svg style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#9CA3AF" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input
type="text"
placeholder="Search flows..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="height:34px;width:100%;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px 0 30px;font-size:13px;color:#111827;outline:none;box-sizing:border-box;background:white"
/>
</div>
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
style="height:34px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer"
>
<option value="All">Status: All</option>
<option value="ACTIVE">Active</option>
<option value="DRAFT">Draft</option>
<option value="INACTIVE">Inactive</option>
</select>
<select
value={userTypeFilter()}
onChange={(e) => setUserTypeFilter(e.currentTarget.value)}
style="height:34px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer"
>
<option value="All">User Type: All</option>
<option value="Customer">Customer</option>
<option value="Professional">Professional</option>
<option value="Company">Company</option>
<option value="Jobseeker">Jobseeker</option>
</select>
</div>
{/* Table */}
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;min-width:960px">
<thead>
<tr style="background:#0D0D2A">
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">FLOW NAME</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">USER TYPE</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">TOTAL STEPS</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">REQUIRED DOCUMENTS</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">VERIFICATION TYPE</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">STATUS</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">LAST UPDATED</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">ACTIONS</th>
</tr>
</thead>
<tbody>
<Show when={flows.loading}>
<tr>
<td colspan="8" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">Loading flows...</td>
</tr>
</Show>
<Show when={!flows.loading && filtered().length === 0}>
<tr>
<td colspan="8" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">No onboarding flows found.</td>
</tr>
</Show>
<For each={filtered()}>
{(flow) => (
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 20px;font-size:14px;color:#111827;font-weight:600">{flow.name}</td>
<td style="padding:12px 20px;font-size:14px;color:#374151">{flow.userType}</td>
<td style="padding:12px 20px;font-size:14px;color:#374151">{flow.totalSteps}</td>
<td style="padding:12px 20px;font-size:14px;color:#374151">{flow.requiredDocs}</td>
<td style="padding:12px 20px">
<VerificationBadge type={flow.verificationType} />
</td>
<td style="padding:12px 20px">
<StatusBadge status={flow.status} />
</td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{flow.lastUpdated || '—'}</td>
<td style="padding:12px 20px">
<ActionsMenu flowId={flow.id} status={flow.status} />
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
{/* Pagination */}
<div style="display:flex;justify-content:space-between;align-items:center;border-top:1px solid #F3F4F6;padding:12px 20px">
<span style="font-size:13px;color:#6B7280">
Showing {filtered().length} of {allFlows().length} flows
</span>
<div style="display:flex;gap:4px">
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:white;border-radius:6px;font-size:13px;color:#374151;cursor:pointer;padding:0 10px">
Previous
</button>
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:#0D0D2A;border-radius:6px;font-size:13px;color:white;cursor:pointer;padding:0 10px">
1
</button>
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:white;border-radius:6px;font-size:13px;color:#374151;cursor:pointer;padding:0 10px">
Next
</button>
</div>
</div>
</div>
</Show>
{/* User Preview View */}
<Show when={activeTab() === 'preview'}>
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:32px;text-align:center">
<div style="margin-bottom:24px">
<label style="font-size:13px;font-weight:500;color:#374151;display:block;margin-bottom:8px">
Select a flow to preview
</label>
<select style="height:36px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer;min-width:280px">
<option value=""> Choose a flow </option>
<For each={allFlows()}>
{(f) => <option value={f.id}>{f.name}</option>}
</For>
</select>
</div>
<div style="padding:48px 0;color:#9CA3AF">
<svg style="margin:0 auto 16px;display:block;color:#D1D5DB" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<p style="font-size:14px;color:#9CA3AF;margin:0">Select a flow above to see a user-facing preview.</p>
</div>
</div>
</Show>
</div>
</AdminShell> </AdminShell>
); );
} }

View file

@ -1,357 +1,463 @@
import { A, useNavigate } from '@solidjs/router'; import { For, Show, createSignal } from 'solid-js';
import { createResource, createSignal, For, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell'; import AdminShell from '~/components/AdminShell';
import { ArrowUpDown, Filter, Download, Eye, Pencil } from 'lucide-solid'; import { ChevronDown, SlidersHorizontal, Download, MoreVertical } from 'lucide-solid';
const API = '/api/gateway';
type Role = { type Role = {
id: string; id: string;
key: string;
name: string; name: string;
audience: string; department: string;
description?: string; usersAssigned: number;
department_name?: string; permissionsCount: number;
is_active: boolean; status: 'ACTIVE' | 'INACTIVE';
users_assigned: number; createdDate: string;
permissions_count: number;
created_at: string;
}; };
type ListResponse = { roles: Role[]; total: number; page: number; per_page: number };
const FALLBACK_ROLES: Role[] = [ const FALLBACK_ROLES: Role[] = [
{ id: 'r1', key: 'ADM-SYS-001', name: 'System Administrator', audience: 'INTERNAL', department_name: 'Information Technology', is_active: true, users_assigned: 12, permissions_count: 0, created_at: '2024-01-12' }, { id: 'r1', name: 'Engineering Lead', department: 'Engineering', usersAssigned: 12, permissionsCount: 28, status: 'ACTIVE', createdDate: '2026-01-15' },
{ id: 'r2', key: 'FIN-MGR-002', name: 'Finance Manager', audience: 'INTERNAL', department_name: 'Accounting & Finance', is_active: true, users_assigned: 4, permissions_count: 42, created_at: '2024-02-05' }, { id: 'r2', name: 'Marketing Manager', department: 'Marketing', usersAssigned: 8, permissionsCount: 18, status: 'ACTIVE', createdDate: '2026-01-20' },
{ id: 'r3', key: 'HR-COOR-003', name: 'HR Coordinator', audience: 'INTERNAL', department_name: 'Human Resources', is_active: false, users_assigned: 8, permissions_count: 28, created_at: '2024-03-18' }, { id: 'r3', name: 'Sales Director', department: 'Sales', usersAssigned: 15, permissionsCount: 32, status: 'ACTIVE', createdDate: '2026-02-01' },
{ id: 'r4', key: 'ENG-LEAD-004', name: 'Lead Developer', audience: 'INTERNAL', department_name: 'Engineering', is_active: true, users_assigned: 25, permissions_count: 64, created_at: '2024-04-02' }, { id: 'r4', name: 'HR Admin', department: 'Human Resources', usersAssigned: 5, permissionsCount: 24, status: 'ACTIVE', createdDate: '2026-02-05' },
{ id: 'r5', key: 'SLS-ASSC-005', name: 'Sales Associate', audience: 'INTERNAL', department_name: 'Growth & Sales', is_active: true, users_assigned: 48, permissions_count: 15, created_at: '2024-05-11' }, { id: 'r5', name: 'Finance Controller', department: 'Finance', usersAssigned: 6, permissionsCount: 20, status: 'ACTIVE', createdDate: '2026-02-10' },
{ id: 'r6', name: 'Operations Head', department: 'Operations', usersAssigned: 4, permissionsCount: 16, status: 'INACTIVE', createdDate: '2026-03-01' },
{ id: 'r7', name: 'Support Lead', department: 'Customer Support', usersAssigned: 9, permissionsCount: 16, status: 'ACTIVE', createdDate: '2026-03-05' },
{ id: 'r8', name: 'Product Owner', department: 'Product', usersAssigned: 7, permissionsCount: 26, status: 'ACTIVE', createdDate: '2026-03-10' },
]; ];
async function loadRoles(params: { q: string; page: number }): Promise<ListResponse> { const MODULES = [
try { 'Department Management',
const qs = new URLSearchParams({ audience: 'INTERNAL', q: params.q, page: String(params.page), per_page: '10' }); 'Designation Management',
const res = await fetch(`${API}/api/admin/roles?${qs}`); 'Internal Role Management',
if (!res.ok) throw new Error('Failed'); 'Employee Management',
const data = await res.json(); 'External Role Management',
if (data.roles?.length > 0) return data; 'External Onboarding Management',
throw new Error('empty'); 'Internal Dashboard Management',
} catch { 'External Dashboard Management',
const q = params.q.toLowerCase(); 'Verification Management',
const filtered = q ? FALLBACK_ROLES.filter((r) => r.name.toLowerCase().includes(q) || r.key.toLowerCase().includes(q)) : FALLBACK_ROLES; 'Approval Management',
return { roles: filtered, total: filtered.length, page: 1, per_page: 10 }; 'Users Management',
'Company Management',
'Candidate Management',
'Customer Management',
'Jobs Management',
'Leads Management',
'Pricing Management',
'Credit Management',
'Coupon Management',
'Discount Management',
'Tax Management',
'Order Management',
'Invoice Management',
'Review Management',
'Support Management',
'Report Management',
'Ledger Management',
];
type PermKey = 'view' | 'create' | 'update' | 'delete';
const PERM_KEYS: PermKey[] = ['view', 'create', 'update', 'delete'];
type ModulePerms = Record<PermKey, boolean>;
type PermissionsMap = Record<string, ModulePerms>;
function defaultPerms(): PermissionsMap {
const map: PermissionsMap = {};
for (const m of MODULES) {
map[m] = { view: false, create: false, update: false, delete: false };
} }
return map;
} }
function formatDate(iso: string) { function StatusBadge(props: { status: string }) {
try { const active = () => props.status === 'ACTIVE';
const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
} catch { return '—'; }
}
function StatusBadge(props: { active: boolean }) {
return ( return (
<span class={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-[12px] font-medium ${ <span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
props.active <span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
? 'border-[#FF5E13] bg-[#FFF3EE] text-[#FF5E13]' {active() ? 'Active' : 'Inactive'}
: 'border-[#D1D5DB] bg-[#F9FAFB] text-[#6B7280]'
}`}>
<span class={`h-1.5 w-1.5 rounded-full ${props.active ? 'bg-[#FF5E13]' : 'bg-[#9CA3AF]'}`} />
{props.active ? 'Active' : 'Inactive'}
</span> </span>
); );
} }
function UsersBadge(props: { count: number }) { function Toggle(props: { on: boolean; onChange: (v: boolean) => void }) {
return ( return (
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full bg-[#EEF2FF] text-[12px] font-semibold text-[#4F46E5]"> <button
{props.count > 99 ? '99+' : String(props.count).padStart(2, '0')} type="button"
</span> onClick={() => props.onChange(!props.on)}
style={`width:40px;height:22px;border-radius:11px;background:${props.on ? '#FF5E13' : '#E5E7EB'};position:relative;cursor:pointer;border:none;padding:0;transition:background 0.2s`}
>
<span style={`position:absolute;width:18px;height:18px;border-radius:50%;background:white;top:2px;transition:left 0.2s;left:${props.on ? '20px' : '2px'}`} />
</button>
); );
} }
const TABS = [ function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string }) {
{ label: 'All Roles', href: '/admin/roles' }, return (
{ label: 'Create Role', href: '/admin/roles/create' }, <label style="display:block">
{ label: 'View Role', href: '/admin/roles/view' }, <span style="font-size:13px;font-weight:600;color:#374151">
{ label: 'Edit Role', href: '/admin/roles/edit' }, {props.label}{props.required && <span style="color:#FF5E13;margin-left:2px">*</span>}
]; </span>
<input
type="text"
value={props.value}
onInput={(e) => 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;padding:0 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white"
/>
</label>
);
}
export default function InternalRolesListPage() { export default function RolesPage() {
const navigate = useNavigate(); const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [formTab, setFormTab] = createSignal<'general' | 'access' | 'settings'>('general');
// All Roles state
const [search, setSearch] = createSignal(''); const [search, setSearch] = createSignal('');
const [debouncedSearch, setDebouncedSearch] = createSignal('');
const [page, setPage] = createSignal(1);
let debounceTimer: ReturnType<typeof setTimeout>; // Create Role state
const handleSearch = (val: string) => { const [roleName, setRoleName] = createSignal('');
setSearch(val); const [roleCode, setRoleCode] = createSignal('');
clearTimeout(debounceTimer); const [department, setDepartment] = createSignal('');
debounceTimer = setTimeout(() => { setDebouncedSearch(val); setPage(1); }, 300); const [description, setDescription] = createSignal('');
}; const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
const [approveRequests, setApproveRequests] = createSignal(true);
const [manageSettings, setManageSettings] = createSignal(false);
const [permissions, setPermissions] = createSignal<PermissionsMap>(defaultPerms());
const [data] = createResource( const filteredRoles = () => {
() => ({ q: debouncedSearch(), page: page() }), const q = search().toLowerCase();
loadRoles, if (!q) return FALLBACK_ROLES;
return FALLBACK_ROLES.filter(
(r) => r.name.toLowerCase().includes(q) || r.department.toLowerCase().includes(q)
); );
const totalPages = () => {
const d = data();
if (!d || d.per_page === 0) return 1;
return Math.max(1, Math.ceil(d.total / d.per_page));
}; };
const roles = () => data()?.roles ?? []; const togglePerm = (mod: string, key: PermKey) => {
setPermissions((prev) => ({
...prev,
[mod]: { ...prev[mod], [key]: !prev[mod][key] },
}));
};
const toggleSelectAll = (mod: string) => {
const p = permissions()[mod];
const allOn = PERM_KEYS.every((k) => p[k]);
setPermissions((prev) => ({
...prev,
[mod]: { view: !allOn, create: !allOn, update: !allOn, delete: !allOn },
}));
};
const formatDate = (d: string) => {
if (!d) return '—';
const dt = new Date(d);
return dt.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
};
const shown = () => filteredRoles();
return ( return (
<AdminShell> <AdminShell>
<div class="w-full space-y-6 pb-8"> <div style="padding:24px">
{/* Page Header */}
{/* Page header */}
<div> <div>
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Internal Role Management</h1> <h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Internal Role Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Define and manage organizational access levels with granular permission control.</p> <p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage internal roles and permissions</p>
</div> </div>
{/* Tabs */} {/* Main Tabs */}
<div class="flex items-center gap-6 border-b border-[#E5E7EB]"> <div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
<For each={TABS}> <button
{(tab) => { type="button"
const active = () => tab.href === '/admin/roles'; onClick={() => setMainTab('all')}
return ( style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${mainTab() === 'all' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:none'}`}
<A
href={tab.href}
class={`pb-3 text-[14px] font-medium transition-colors ${
active()
? 'border-b-2 border-[#FF5E13] text-[#FF5E13]'
: 'text-[#6B7280] hover:text-[#111827]'
}`}
> >
{tab.label} All Roles
</A> </button>
); <button
}} type="button"
</For> onClick={() => setMainTab('create')}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${mainTab() === 'create' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:none'}`}
>
Create Role
</button>
</div> </div>
{/* Table card */} {/* ── ALL ROLES TAB ── */}
<div class="overflow-hidden rounded-2xl border border-[#E5E7EB] bg-white shadow-sm"> <Show when={mainTab() === 'all'}>
{/* Edge-to-edge card */}
{/* Filter bar */} <div style="margin-top:20px;margin-left:-24px;margin-right:-24px;border-radius:0;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white">
<div class="flex items-center gap-2 px-5 py-4"> {/* Filter Bar */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input <input
type="text" type="text"
placeholder="Filter by role name or code..." placeholder="Search roles..."
value={search()} value={search()}
onInput={(e) => handleSearch(e.currentTarget.value)} onInput={(e) => setSearch(e.currentTarget.value)}
class="h-[34px] flex-1 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.08)] transition-colors" style="height:34px;flex:1;max-width:240px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827"
/> />
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[12px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors"> <button type="button" style="height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer;display:inline-flex;align-items:center;gap:4px">
<ArrowUpDown size={13} /> <ChevronDown size={14} />
Sort Sort
</button> </button>
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[12px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors"> <button type="button" style="height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer;display:inline-flex;align-items:center;gap:4px">
<Filter size={13} /> <SlidersHorizontal size={14} />
Filters Filters
</button> </button>
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg bg-[#0D0D2A] px-3 text-[12px] font-semibold text-white hover:bg-[#1a1a3e] transition-colors"> <button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px">
<Download size={13} /> <Download size={14} />
Export Export
</button> </button>
</div> </div>
{/* Table */} {/* Table */}
<div class="overflow-x-auto"> <div style="overflow-x:auto">
<table class="min-w-full"> <table style="width:100%;border-collapse:collapse">
<thead> <thead>
<tr class="bg-[#0D0D2A] text-left"> <tr style="background:#0D0D2A">
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Role Name</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Role Name</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Role Code</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Department</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Department</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Users Assigned</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Users Assigned</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Permissions Count</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Permissions</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Status</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Status</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Created Date</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Created Date</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap"></th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">AC</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-[#F3F4F6]"> <tbody>
<Show when={data.loading}> <For each={shown()}>
<For each={[0, 1, 2, 3, 4]}>
{() => (
<tr class="animate-pulse">
<td class="px-5 py-3.5"><div class="h-4 w-36 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-20 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-28 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-7 w-7 rounded-full bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-20 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-6 w-20 rounded-full bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-24 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-12 rounded-lg bg-[#F3F4F6]" /></td>
</tr>
)}
</For>
</Show>
<Show when={!data.loading && roles().length === 0}>
<tr>
<td colspan="8" class="px-6 py-16 text-center">
<p class="text-[15px] font-semibold text-[#111827]">No roles found</p>
<p class="mt-1 text-[13px] text-[#6B7280]">Create your first internal role to control admin access.</p>
<A href="/admin/roles/create" class="mt-4 inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-4 py-2 text-[13px] font-semibold text-white">
Create Role
</A>
</td>
</tr>
</Show>
<Show when={!data.loading}>
<For each={roles()}>
{(role) => ( {(role) => (
<tr class="hover:bg-[#FAFAFA] transition-colors"> <tr style="border-bottom:1px solid #F3F4F6">
<td class="px-5 py-3.5"> <td style="padding:12px 20px">
<p class="text-[14px] font-bold text-[#111827]">{role.name}</p> <span style="font-size:14px;font-weight:600;color:#111827">{role.name}</span>
</td> </td>
<td class="px-5 py-3.5"> <td style="padding:12px 20px">
<div class="text-[11px] font-mono leading-[1.6] text-[#9CA3AF]"> <span style="font-size:14px;color:#374151">{role.department}</span>
{role.key.split('-').map((seg, i, arr) => (
<span class="block">{seg}{i < arr.length - 1 ? '-' : ''}</span>
))}
</div>
</td> </td>
<td class="px-5 py-3.5 text-[13px] text-[#374151]">{role.department_name || '—'}</td> <td style="padding:12px 20px">
<td class="px-5 py-3.5"> <span style="font-size:14px;font-weight:600;color:#111827">{role.usersAssigned}</span>
<UsersBadge count={role.users_assigned} />
</td> </td>
<td class="px-5 py-3.5"> <td style="padding:12px 20px">
<Show when={role.permissions_count === 0} fallback={ <span style="display:inline-flex;border-radius:8px;background:#FFF1EB;border:1px solid #FFD8C2;color:#FF5E13;padding:2px 10px;font-size:12px;font-weight:600">
<span class="text-[13px] text-[#374151]">{role.permissions_count} Controls</span> {role.permissionsCount} Permissions
}> </span>
<span class="text-[13px] font-semibold text-[#FF5E13]">All Access</span>
</Show>
</td> </td>
<td class="px-5 py-3.5"> <td style="padding:12px 20px">
<StatusBadge active={role.is_active} /> <StatusBadge status={role.status} />
</td> </td>
<td class="px-5 py-3.5 text-[13px] text-[#6B7280]">{formatDate(role.created_at)}</td> <td style="padding:12px 20px">
<td class="px-5 py-3.5"> <span style="font-size:13px;color:#6B7280">{formatDate(role.createdDate)}</span>
<div class="flex items-center gap-3"> </td>
<button <td style="padding:12px 20px;text-align:center">
type="button" <button type="button" style="background:none;border:none;cursor:pointer;color:#6B7280;display:inline-flex;align-items:center">
onClick={() => navigate(`/admin/roles/${role.id}`)} <MoreVertical size={16} />
class="text-[#9CA3AF] hover:text-[#374151] transition-colors"
aria-label="View role"
>
<Eye size={17} />
</button> </button>
<button
type="button"
onClick={() => navigate(`/admin/roles/${role.id}/edit`)}
class="text-[#9CA3AF] hover:text-[#FF5E13] transition-colors"
aria-label="Edit role"
>
<Pencil size={16} />
</button>
</div>
</td> </td>
</tr> </tr>
)} )}
</For> </For>
</Show>
</tbody> </tbody>
</table> </table>
</div> </div>
{/* Pagination */} {/* Pagination */}
<Show when={!data.loading && roles().length > 0}> <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-top:1px solid #F3F4F6">
<div class="flex items-center justify-between border-t border-[#F3F4F6] px-6 py-4"> <span style="font-size:13px;color:#6B7280">
<p class="text-[13px] text-[#6B7280]"> Showing 1{shown().length} of {shown().length} roles
Showing <span class="font-semibold text-[#111827]">{roles().length}</span> of <span class="font-semibold text-[#111827]">{data()?.total ?? 0}</span> roles </span>
</p> <div style="display:flex;align-items:center;gap:4px">
<div class="flex items-center gap-1.5"> <button type="button" style="height:30px;min-width:30px;padding:0 10px;border-radius:6px;border:1px solid #E5E7EB;background:#FF5E13;color:white;font-size:13px;font-weight:600;cursor:pointer">1</button>
<button </div>
type="button"
disabled={page() === 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-[16px]"
></button>
<For each={Array.from({ length: Math.min(totalPages(), 5) }, (_, i) => i + 1)}>
{(p) => (
<button
type="button"
onClick={() => setPage(p)}
class={`inline-flex h-8 w-8 items-center justify-center rounded-lg text-[13px] font-medium transition-colors ${
page() === p
? 'bg-[#FF5E13] text-white'
: 'border border-[#E5E7EB] text-[#374151] hover:bg-[#F9FAFB]'
}`}
>{p}</button>
)}
</For>
<button
type="button"
disabled={page() >= totalPages()}
onClick={() => setPage((p) => Math.min(totalPages(), p + 1))}
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-[16px]"
></button>
</div> </div>
</div> </div>
</Show> </Show>
</div> {/* ── CREATE ROLE TAB ── */}
<Show when={mainTab() === 'create'}>
{/* Bottom stats row */} <div style="margin-top:20px;border-radius:16px;border:1px solid #E5E7EB;background:white;overflow:hidden">
<div class="grid grid-cols-2 gap-5"> {/* Form Sub-tabs */}
<div style="display:flex;align-items:center;gap:0;border-bottom:1px solid #E5E7EB;padding:0 24px">
{/* Distribution card */} {(
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-sm"> [
<div class="flex items-center justify-between mb-5"> { key: 'general', label: 'General Information' },
<p class="text-[15px] font-semibold text-[#111827]">Distribution</p> { key: 'access', label: 'Module Access' },
<button type="button" class="text-[#9CA3AF] hover:text-[#374151]"> { key: 'settings', label: 'Role Settings' },
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> ] as const
).map(({ key, label }) => (
<button
type="button"
onClick={() => setFormTab(key)}
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;cursor:pointer;${formTab() === key ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;font-weight:400'}`}
>
{label}
</button> </button>
</div>
<div class="flex items-end gap-3 h-[100px]">
{[
{ h: 55, color: '#FFD4C2' },
{ h: 80, color: '#FF5E13' },
{ h: 45, color: '#FFD4C2' },
{ h: 65, color: '#FFD4C2' },
{ h: 38, color: '#FFD4C2' },
].map((bar) => (
<div class="flex-1 rounded-t-md" style={{ height: `${bar.h}%`, background: bar.color }} />
))} ))}
</div> </div>
<p class="mt-4 text-[12px] text-[#6B7280]">
Most users are concentrated in <span class="font-semibold text-[#FF5E13]">Sales</span> and{' '}
<span class="font-semibold text-[#FF5E13]">Engineering</span> departments.
</p>
</div>
{/* Audit Readiness Score card */} {/* ── General Information ── */}
<div class="flex items-center justify-between rounded-2xl bg-[#0D0D2A] p-8 shadow-sm"> <Show when={formTab() === 'general'}>
<div class="flex-1 pr-8"> <div style="padding:24px">
<p class="text-[18px] font-bold text-white">Audit Readiness Score</p> <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<p class="mt-2 text-[13px] leading-relaxed text-[#8AACC8]"> <FormInput
Your organizational permissions currently align with 94% of compliance standards. Review inactive roles to reach 100%. label="Role Name"
required
value={roleName()}
onInput={setRoleName}
placeholder="e.g. Engineering Lead"
/>
<FormInput
label="Role Code"
required
value={roleCode()}
onInput={setRoleCode}
placeholder="e.g. ENG_LEAD"
/>
</div>
<div style="margin-top:20px">
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
Department<span style="color:#FF5E13;margin-left:2px">*</span>
</span>
<input
type="text"
value={department()}
onInput={(e) => setDepartment(e.currentTarget.value)}
placeholder="e.g. Engineering"
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;padding:0 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white"
/>
</label>
</div>
<div style="margin-top:20px">
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
<textarea
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Describe this role's responsibilities..."
style="display:block;margin-top:6px;height:100px;width:100%;border-radius:10px;border:1px solid #E5E7EB;padding:10px 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white;resize:vertical;font-family:inherit"
/>
</label>
</div>
</div>
</Show>
{/* ── Module Access ── */}
<Show when={formTab() === 'access'}>
<div style="padding:24px">
<p style="font-size:13px;color:#6B7280;margin-top:0;margin-bottom:16px">
Configure module access permissions for this role.
</p> </p>
<button type="button" class="mt-5 inline-flex h-10 items-center rounded-xl bg-[#FF5E13] px-5 text-[13px] font-semibold text-white hover:bg-[#e54d0a] transition-colors"> <div style="overflow-x:auto">
Review Audit Log <table style="width:100%;border-collapse:collapse;border-radius:12px;overflow:hidden;border:1px solid #E5E7EB">
<thead>
<tr style="background:#0D0D2A">
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:left">Module Name</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">View</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Create</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Update</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Delete</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Select All</th>
</tr>
</thead>
<tbody>
<For each={MODULES}>
{(mod) => {
const p = () => permissions()[mod];
const allOn = () => PERM_KEYS.every((k) => p()[k]);
return (
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 16px;font-size:13px;color:#111827;font-weight:500;text-align:left">{mod}</td>
<For each={PERM_KEYS}>
{(key) => (
<td style="text-align:center;padding:12px">
<input
type="checkbox"
checked={p()[key]}
onChange={() => togglePerm(mod, key)}
style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer"
/>
</td>
)}
</For>
<td style="text-align:center;padding:12px">
<input
type="checkbox"
checked={allOn()}
onChange={() => toggleSelectAll(mod)}
style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer"
/>
</td>
</tr>
);
}}
</For>
</tbody>
</table>
</div>
</div>
</Show>
{/* ── Role Settings ── */}
<Show when={formTab() === 'settings'}>
<div style="padding:24px;display:flex;flex-direction:column;gap:24px">
{/* Role Status */}
<div>
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 10px 0">Role Status</p>
<div style="display:flex;gap:8px">
<button
type="button"
onClick={() => setStatus('ACTIVE')}
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${status() === 'ACTIVE' ? 'border:1px solid #FF5E13;background:#FF5E13;color:white' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
>
Active
</button>
<button
type="button"
onClick={() => setStatus('INACTIVE')}
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${status() === 'INACTIVE' ? 'border:1px solid #6B7280;background:#6B7280;color:white' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
>
Inactive
</button> </button>
</div> </div>
<div class="relative flex h-[100px] w-[100px] shrink-0 items-center justify-center">
<svg class="h-full w-full -rotate-90" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="42" fill="none" stroke="#1E2D3D" stroke-width="10" />
<circle cx="50" cy="50" r="42" fill="none" stroke="#FF5E13" stroke-width="10"
stroke-dasharray={`${2 * Math.PI * 42 * 0.94} ${2 * Math.PI * 42}`}
stroke-linecap="round" />
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-[22px] font-bold text-white">94%</span>
</div>
</div>
</div> </div>
{/* Toggle: Approve Requests */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
<div>
<p style="font-size:14px;font-weight:600;color:#111827;margin:0">Allow Role to Approve Requests</p>
<p style="font-size:13px;color:#6B7280;margin:4px 0 0 0">Grant this role the ability to approve or reject submitted requests.</p>
</div> </div>
<Toggle on={approveRequests()} onChange={setApproveRequests} />
</div>
{/* Toggle: Manage Settings */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
<div>
<p style="font-size:14px;font-weight:600;color:#111827;margin:0">Allow Role to Manage System Settings</p>
<p style="font-size:13px;color:#6B7280;margin:4px 0 0 0">Grant this role access to configure and modify system-wide settings.</p>
</div>
<Toggle on={manageSettings()} onChange={setManageSettings} />
</div>
</div>
</Show>
{/* Form Footer */}
<div style="display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #E5E7EB">
<button
type="button"
onClick={() => setMainTab('all')}
style="height:38px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;border:1px solid #E5E7EB;background:white;color:#374151;cursor:pointer"
>
Cancel
</button>
<button
type="button"
style="background:#0D0D2A;color:white;height:38px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer"
>
Create Role
</button>
</div>
</div>
</Show>
</div> </div>
</AdminShell> </AdminShell>
); );

View file

@ -1,173 +1,318 @@
import { A } from '@solidjs/router'; import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
import { createMemo, createResource, For, Show } from 'solid-js';
import { Search } from 'lucide-solid';
import AdminShell from '~/components/AdminShell'; import AdminShell from '~/components/AdminShell';
import { Clock } from 'lucide-solid';
const API = '/api/gateway'; const API = '/api/gateway';
type ApprovalRow = { type VerificationStatus = 'PENDING' | 'VERIFIED' | 'FLAGGED' | 'RE_UPLOAD';
type VerificationRow = {
id: string; id: string;
requestType?: string; verificationId?: string;
type?: string; name?: string;
requestStatus?: string;
status?: string;
requester?: { name?: string; email?: string };
requesterName?: string; requesterName?: string;
requesterEmail?: string; requester?: { name?: string; email?: string };
userType?: string;
verificationType?: string;
type?: string;
submittedDate?: string;
createdAt?: string; createdAt?: string;
created_at?: string; created_at?: string;
documents?: number;
status?: string;
requestStatus?: string;
}; };
async function fetchApprovals(): Promise<ApprovalRow[]> { const FALLBACK: VerificationRow[] = [
{ id: 'v1', verificationId: 'VER-2024-001', name: 'Rajesh Kumar', userType: 'Professional', verificationType: 'Identity Verification', submittedDate: '2024-03-20', documents: 3, status: 'PENDING' },
{ id: 'v2', verificationId: 'VER-2024-002', name: 'Priya Sharma', userType: 'Company', verificationType: 'Business Verification', submittedDate: '2024-03-19', documents: 5, status: 'VERIFIED' },
{ id: 'v3', verificationId: 'VER-2024-003', name: 'Anil Patel', userType: 'Customer', verificationType: 'Profile Verification', submittedDate: '2024-03-18', documents: 2, status: 'PENDING' },
{ id: 'v4', verificationId: 'VER-2024-004', name: 'Meera Singh', userType: 'Jobseeker', verificationType: 'Document Verification', submittedDate: '2024-03-17', documents: 4, status: 'RE_UPLOAD' },
{ id: 'v5', verificationId: 'VER-2024-005', name: 'Vikram Reddy', userType: 'Professional', verificationType: 'Mixed Verification', submittedDate: '2024-03-16', documents: 6, status: 'FLAGGED' },
];
const FALLBACK_RULES = [
{ id: 'r1', name: 'Professional Identity Verification', userType: 'Professional', verificationType: 'Identity Verification', requiredDocs: 3, checklistItems: 8, status: 'ACTIVE' },
{ id: 'r2', name: 'Company Business Verification', userType: 'Company', verificationType: 'Business Verification', requiredDocs: 5, checklistItems: 12, status: 'ACTIVE' },
{ id: 'r3', name: 'Customer Basic Verification', userType: 'Customer', verificationType: 'Profile Verification', requiredDocs: 2, checklistItems: 6, status: 'ACTIVE' },
];
const STATS = [
{ label: 'Total Pending', value: '42', color: '#6B7280' },
{ label: 'Identity Verification', value: '18', color: '#3B82F6' },
{ label: 'Business Verification', value: '12', color: '#8B5CF6' },
{ label: 'Re-upload Review', value: '8', color: '#FF5E13' },
{ label: 'Verified Today', value: '15', color: '#10B981' },
{ label: 'Flagged Cases', value: '4', color: '#EF4444' },
];
const PREVIEW_STATES = [
{ key: 'pending', label: 'Pending Verification' },
{ key: 'review', label: 'Under Review' },
{ key: 'reupload', label: 'Re-upload Requested' },
{ key: 'completed', label: 'Completed' },
{ key: 'rejected', label: 'Rejected' },
] as const;
async function fetchVerifications(): Promise<VerificationRow[]> {
try { try {
const res = await fetch(`${API}/api/admin/approvals`); const res = await fetch(`${API}/api/admin/verifications`);
if (!res.ok) return []; if (!res.ok) throw new Error();
const data = await res.json(); const data = await res.json();
return Array.isArray(data) ? data : (data.approvals || []); const list = Array.isArray(data) ? data : (data.verifications ?? data.approvals ?? []);
return list.length > 0 ? list : FALLBACK;
} catch { } catch {
return []; return FALLBACK;
} }
} }
function rowName(r: VerificationRow) {
return r.name || r.requester?.name || r.requesterName || '—';
}
function rowVerifId(r: VerificationRow) {
return r.verificationId || r.id || '—';
}
function rowStatus(r: VerificationRow): VerificationStatus {
const raw = String(r.status || r.requestStatus || '').toUpperCase();
if (raw === 'VERIFIED' || raw === 'APPROVED') return 'VERIFIED';
if (raw === 'FLAGGED' || raw === 'REJECTED') return 'FLAGGED';
if (raw === 'RE_UPLOAD' || raw === 'CHANGES_REQUESTED') return 'RE_UPLOAD';
return 'PENDING';
}
function rowDate(r: VerificationRow) {
return r.submittedDate || (r.createdAt ? r.createdAt.split('T')[0] : r.created_at ? r.created_at.split('T')[0] : '—');
}
function StatusBadge(props: { status: VerificationStatus }) {
const styles: Record<VerificationStatus, string> = {
PENDING: 'display:inline-flex;align-items:center;justify-content:center;min-width:72px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;border:1px solid #FFD8C2;background:#FFF1EB;color:#FF5E13',
VERIFIED: 'display:inline-flex;align-items:center;justify-content:center;min-width:72px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;background:#ECFDF5;color:#059669;border:1px solid #A7F3D0',
FLAGGED: 'display:inline-flex;align-items:center;justify-content:center;min-width:72px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;background:#FEF2F2;color:#DC2626;border:1px solid #FECACA',
RE_UPLOAD: 'display:inline-flex;align-items:center;justify-content:center;min-width:72px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;background:#EFF6FF;color:#3B82F6;border:1px solid #BFDBFE',
};
const labels: Record<VerificationStatus, string> = {
PENDING: 'Pending',
VERIFIED: 'Verified',
FLAGGED: 'Flagged',
RE_UPLOAD: 'Re-upload',
};
return <span style={styles[props.status]}>{labels[props.status]}</span>;
}
export default function VerificationStatusPage() { export default function VerificationStatusPage() {
const [rows] = createResource(fetchApprovals); const [rows] = createResource(fetchVerifications);
const [search, setSearch] = createSignal('');
const [userTypeFilter, setUserTypeFilter] = createSignal('');
const [verifTypeFilter, setVerifTypeFilter] = createSignal('');
const [statusFilter, setStatusFilter] = createSignal('');
const [activeView, setActiveView] = createSignal<'queue' | 'rules' | 'preview'>('queue');
const [previewState, setPreviewState] = createSignal('review');
const [page, setPage] = createSignal(1);
const [openActionId, setOpenActionId] = createSignal<string | null>(null);
const [rulesSearch, setRulesSearch] = createSignal('');
const [rulesUserType, setRulesUserType] = createSignal('');
const normalized = createMemo(() => (rows() || []).map((r) => ({ const PAGE_SIZE = 10;
...r,
status: (r.requestStatus || r.status || 'UNKNOWN').toUpperCase(),
type: (r.requestType || r.type || 'PROFILE').toUpperCase(),
requesterName: r.requester?.name || r.requesterName || 'Unknown',
requesterEmail: r.requester?.email || r.requesterEmail || '—',
createdAt: r.createdAt || r.created_at || '',
})));
function statusDoc(status: string) { const filtered = createMemo(() => {
if (status === 'APPROVED') return 'Verified'; const list = rows() ?? [];
if (status === 'REJECTED') return 'Flagged'; const q = search().trim().toLowerCase();
if (status === 'PENDING') return 'Review'; const sf = statusFilter();
return 'Pending'; const ut = userTypeFilter();
const vt = verifTypeFilter();
return list.filter((r) => {
const matchSearch = !q || [rowVerifId(r), rowName(r), r.userType, r.verificationType].join(' ').toLowerCase().includes(q);
const matchStatus = !sf || rowStatus(r) === sf;
const matchUserType = !ut || (r.userType || '').toLowerCase() === ut.toLowerCase();
const matchVerifType = !vt || (r.verificationType || '').toLowerCase().includes(vt.toLowerCase());
return matchSearch && matchStatus && matchUserType && matchVerifType;
});
});
const filteredRules = createMemo(() => {
const q = rulesSearch().trim().toLowerCase();
const ut = rulesUserType();
return FALLBACK_RULES.filter((r) => {
const matchSearch = !q || r.name.toLowerCase().includes(q) || r.userType.toLowerCase().includes(q);
const matchUserType = !ut || r.userType.toLowerCase() === ut.toLowerCase();
return matchSearch && matchUserType;
});
});
const totalPages = () => Math.max(1, Math.ceil(filtered().length / PAGE_SIZE));
const paged = () => filtered().slice((page() - 1) * PAGE_SIZE, page() * PAGE_SIZE);
function viewBtnStyle(view: 'queue' | 'rules' | 'preview') {
const isActive = activeView() === view;
const isFirst = view === 'queue';
return isActive
? `background:#0D0D2A;color:white;padding:8px 16px;font-size:13px;font-weight:600;border:none;cursor:pointer${!isFirst ? ';border-left:1px solid #E5E7EB' : ''}`
: `background:white;color:#374151;padding:8px 16px;font-size:13px;font-weight:500;border:none;cursor:pointer${!isFirst ? ';border-left:1px solid #E5E7EB' : ''}`;
} }
return ( return (
<AdminShell> <AdminShell>
<div class="space-y-6 max-w-[1600px]"> <div style="width:100%;padding-bottom:32px">
{/* Header Configuration */}
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between px-1"> {/* Page Header */}
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:24px">
<div> <div>
<div class="flex items-center gap-4"> <h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Verification Management</h1>
<h1 class="text-[32px] font-bold leading-tight text-[#0A1128]">Verification Management</h1> <p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Review and verify user submissions</p>
<div class="hidden items-center gap-3 md:flex"> </div>
<button class="inline-flex h-10 items-center justify-center rounded-xl bg-[#0A1128] px-5 text-[14px] font-semibold text-white hover:bg-[#1E293B] transition-colors"> {/* Toggle button group */}
<div style="display:flex;align-items:center;gap:0;border-radius:10px;overflow:hidden;border:1px solid #E5E7EB">
<button type="button" onClick={() => setActiveView('queue')} style={viewBtnStyle('queue')}>
Verification Queue Verification Queue
</button> </button>
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-5 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC] transition-colors"> <button type="button" onClick={() => setActiveView('rules')} style={viewBtnStyle('rules')}>
Verification Rules Verification Rules
</button> </button>
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-5 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC] transition-colors"> <button type="button" onClick={() => setActiveView('preview')} style={viewBtnStyle('preview')}>
User Preview User Preview
</button> </button>
</div> </div>
</div> </div>
<p class="mt-1 text-[15px] text-[#64748B]">Review and verify user submissions</p>
{/* ─── VERIFICATION QUEUE VIEW ─── */}
<Show when={activeView() === 'queue'}>
{/* Stats Cards */}
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:16px;margin-bottom:24px">
<For each={STATS}>
{(stat) => (
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px;">
<p style="font-size:12px;color:#6B7280;font-weight:500;margin:0 0 8px 0">{stat.label}</p>
<p style={`font-size:28px;font-weight:700;color:${stat.color};margin:0`}>{stat.value}</p>
</div> </div>
)}
</For>
</div> </div>
{/* 6 KPI Cards Grid */} {/* Section Header */}
<section class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm"> <span style="font-size:18px;font-weight:700;color:#111827">Verification Cases</span>
<p class="text-[14px] leading-tight text-[#64748B]">Total<br/>Pending</p> <div style="display:flex;gap:8px">
<p class="mt-4 text-[32px] font-bold leading-none text-[#0A1128]">42</p> <button type="button" style="height:34px;padding:0 14px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer">
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Identity<br/>Verification</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#2563EB]">18</p>
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Business<br/>Verification</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#7C3AED]">12</p>
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Re-upload<br/>Review</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FF5E13]">8</p>
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Verified<br/>Today</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#16A34A]">15</p>
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Flagged<br/>Cases</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FF5E13]">4</p>
</div>
</section>
{/* Table Container */}
<section class="rounded-2xl border border-[#E2E8F0] bg-white p-6 shadow-sm">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-6">
<h2 class="text-[20px] font-bold text-[#0A1128]">Verification Cases</h2>
<div class="flex items-center gap-3">
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] px-4 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC]">
Export Queue Export Queue
</button> </button>
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] px-4 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC]"> <button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer">
Bulk Actions Bulk Actions
</button> </button>
</div> </div>
</div> </div>
<div class="flex flex-col gap-4 md:flex-row md:items-center mb-6"> {/* Filter Bar */}
<div class="relative w-full max-w-sm"> <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap">
<Search size={18} class="absolute left-3.5 top-1/2 -translate-y-1/2 text-[#94A3B8]" />
<input <input
type="text" type="text"
placeholder="Search by name or ID..." placeholder="Search by name or ID..."
class="h-11 w-full rounded-xl border border-[#E2E8F0] bg-[#F8FAFC] pl-10 pr-4 text-[14px] outline-none transition-colors focus:border-[#CBD5E1] focus:bg-white" value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); setPage(1); }}
style="height:34px;flex:1;max-width:280px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827;background:white;box-sizing:border-box"
/> />
</div> <select
<div class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white" /> value={userTypeFilter()}
<div class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white" /> onChange={(e) => { setUserTypeFilter(e.currentTarget.value); setPage(1); }}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;color:#374151;background:white;cursor:pointer"
>
<option value="">User Type</option>
<option value="Professional">Professional</option>
<option value="Company">Company</option>
<option value="Customer">Customer</option>
<option value="Jobseeker">Jobseeker</option>
</select>
<select
value={verifTypeFilter()}
onChange={(e) => { setVerifTypeFilter(e.currentTarget.value); setPage(1); }}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;color:#374151;background:white;cursor:pointer"
>
<option value="">Verification Type</option>
<option value="Identity Verification">Identity Verification</option>
<option value="Business Verification">Business Verification</option>
<option value="Profile Verification">Profile Verification</option>
<option value="Document Verification">Document Verification</option>
</select>
<select
value={statusFilter()}
onChange={(e) => { setStatusFilter(e.currentTarget.value); setPage(1); }}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;color:#374151;background:white;cursor:pointer"
>
<option value="">Status</option>
<option value="PENDING">Pending</option>
<option value="VERIFIED">Verified</option>
<option value="FLAGGED">Flagged</option>
<option value="RE_UPLOAD">Re-upload</option>
</select>
</div> </div>
<div class="overflow-x-auto rounded-xl border border-[#E2E8F0]"> {/* Table Card */}
<table class="w-full min-w-[1000px] border-collapse bg-white text-left"> <div style="border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;min-width:900px">
<thead> <thead>
<tr class="bg-[#0A1128] text-white"> <tr style="background:#0D0D2A">
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERIFICATION ID</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Verification ID</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">APPLICANT NAME</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Applicant Name</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">USER TYPE</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">User Type</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERIFICATION TYPE</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Verification Type</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">SUBMITTED DATE</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Submitted Date</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">DOCUMENT STATUS</th> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Documents</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Status</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<Show when={rows.loading}> <Show when={rows.loading}>
<tr><td colspan="6" class="text-center py-12 text-[#64748B] text-[14px]">Loading verification cases...</td></tr> <tr>
<td colspan="8" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">Loading verification cases...</td>
</tr>
</Show> </Show>
<Show when={!rows.loading && normalized().length === 0}> <Show when={!rows.loading && paged().length === 0}>
<tr><td colspan="6" class="text-center py-12 text-[#64748B] text-[14px]">No pending verification cases found.</td></tr> <tr>
<td colspan="8" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">No verification cases found.</td>
</tr>
</Show> </Show>
<For each={normalized()}> <For each={paged()}>
{(item) => ( {(row) => (
<tr class="border-b border-[#E2E8F0] transition-colors hover:bg-[#F8FAFC]"> <tr style="border-bottom:1px solid #F3F4F6">
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">VER2024{item.id.slice(0, 3)}</td> <td style="padding:12px 20px;font-size:13px">
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">{item.requesterName}</td> <span style="font-size:12px;font-family:monospace;color:#6B7280">{rowVerifId(row)}</span>
<td class="px-6 py-4 text-[14px] text-[#475569]">Professional</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Identity Verification</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">
{item.createdAt ? new Date(item.createdAt).toISOString().split('T')[0] : '2024-03-20'}
</td> </td>
<td class="px-6 py-4"> <td style="padding:12px 20px;font-size:13px;font-weight:600;color:#111827">{rowName(row)}</td>
<A <td style="padding:12px 20px;font-size:13px;color:#374151">{row.userType || '—'}</td>
href={`/admin/verification-status/${item.id}`} <td style="padding:12px 20px;font-size:13px;color:#374151">{row.verificationType || row.type || '—'}</td>
class={`inline-flex items-center rounded-lg px-3 py-1.5 text-[13px] font-bold transition-opacity hover:opacity-80 ${ <td style="padding:12px 20px;font-size:13px;color:#6B7280">{rowDate(row)}</td>
statusDoc(item.status) === 'Verified' ? 'bg-[#DCFCE7] text-[#16A34A]' : <td style="padding:12px 20px;font-size:13px;color:#374151">{row.documents ?? '—'}</td>
statusDoc(item.status) === 'Flagged' ? 'bg-[#FEE2E2] text-[#EF4444]' : <td style="padding:12px 20px">
'bg-[#F1F5F9] text-[#64748B]' <StatusBadge status={rowStatus(row)} />
}`} </td>
<td style="padding:12px 20px;position:relative">
<button
type="button"
onClick={() => setOpenActionId(openActionId() === row.id ? null : row.id)}
style="height:30px;padding:0 12px;font-size:12px;font-weight:500;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer"
> >
{statusDoc(item.status)} Actions
</A> </button>
<Show when={openActionId() === row.id}>
<div style="position:absolute;right:20px;top:44px;z-index:20;width:210px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
<button type="button" style="display:block;width:100%;text-align:left;padding:10px 12px;font-size:13px;color:#111827;background:none;border:none;cursor:pointer;border-radius:6px" onMouseEnter={(e) => (e.currentTarget.style.background = '#F9FAFB')} onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}>
View Details
</button>
<button type="button" style="display:block;width:100%;text-align:left;padding:10px 12px;font-size:13px;color:#059669;background:none;border:none;cursor:pointer;border-radius:6px" onMouseEnter={(e) => (e.currentTarget.style.background = '#ECFDF5')} onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}>
Approve
</button>
<button type="button" style="display:block;width:100%;text-align:left;padding:10px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;border-radius:6px" onMouseEnter={(e) => (e.currentTarget.style.background = '#FEF2F2')} onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}>
Reject / Flag
</button>
<button type="button" style="display:block;width:100%;text-align:left;padding:10px 12px;font-size:13px;color:#D97706;background:none;border:none;cursor:pointer;border-radius:6px" onMouseEnter={(e) => (e.currentTarget.style.background = '#FFFBEB')} onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}>
Request Re-upload
</button>
</div>
</Show>
</td> </td>
</tr> </tr>
)} )}
@ -175,7 +320,195 @@ export default function VerificationStatusPage() {
</tbody> </tbody>
</table> </table>
</div> </div>
</section>
{/* Pagination */}
<div style="display:flex;justify-content:space-between;align-items:center;border-top:1px solid #F3F4F6;padding:12px 20px">
<span style="font-size:13px;color:#6B7280">
Showing {Math.min((page() - 1) * PAGE_SIZE + 1, filtered().length)}{Math.min(page() * PAGE_SIZE, filtered().length)} of {filtered().length}
</span>
<div style="display:flex;gap:4px">
<button
type="button"
onClick={() => setPage(Math.max(1, page() - 1))}
disabled={page() === 1}
style="height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer"
>
</button>
<For each={Array.from({ length: totalPages() }, (_, i) => i + 1)}>
{(p) => (
<button
type="button"
onClick={() => setPage(p)}
style={p === page()
? 'height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #FF5E13;background:#FF5E13;color:white;border-radius:6px;cursor:pointer;font-weight:600'
: 'height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer'}
>
{p}
</button>
)}
</For>
<button
type="button"
onClick={() => setPage(Math.min(totalPages(), page() + 1))}
disabled={page() === totalPages()}
style="height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer"
>
</button>
</div>
</div>
</div>
</Show>
{/* ─── VERIFICATION RULES VIEW ─── */}
<Show when={activeView() === 'rules'}>
{/* Section Header */}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<span style="font-size:18px;font-weight:700;color:#111827">Verification Rules</span>
<button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer">
+ Create Verification Rule
</button>
</div>
{/* Filter */}
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px">
<input
type="text"
placeholder="Search rules..."
value={rulesSearch()}
onInput={(e) => setRulesSearch(e.currentTarget.value)}
style="height:34px;flex:1;max-width:280px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827;background:white;box-sizing:border-box"
/>
<select
value={rulesUserType()}
onChange={(e) => setRulesUserType(e.currentTarget.value)}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;color:#374151;background:white;cursor:pointer"
>
<option value="">User Type</option>
<option value="Professional">Professional</option>
<option value="Company">Company</option>
<option value="Customer">Customer</option>
<option value="Jobseeker">Jobseeker</option>
</select>
</div>
{/* Rules Table Card */}
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;min-width:800px">
<thead>
<tr style="background:#0D0D2A">
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Rule Name</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">User Type</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Verification Type</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Required Documents</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Checklist Items</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Status</th>
</tr>
</thead>
<tbody>
<Show when={filteredRules().length === 0}>
<tr>
<td colspan="6" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">No rules found.</td>
</tr>
</Show>
<For each={filteredRules()}>
{(rule) => (
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 20px;font-size:13px;font-weight:600;color:#111827">{rule.name}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{rule.userType}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{rule.verificationType}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{rule.requiredDocs}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{rule.checklistItems}</td>
<td style="padding:12px 20px">
<span style="display:inline-flex;align-items:center;justify-content:center;min-width:64px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;background:#ECFDF5;color:#059669;border:1px solid #A7F3D0">
{rule.status}
</span>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
</Show>
{/* ─── USER PREVIEW VIEW ─── */}
<Show when={activeView() === 'preview'}>
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:24px">
<p style="font-size:16px;font-weight:600;color:#111827;margin:0 0 16px 0">Select Verification State</p>
{/* State Selector Buttons */}
<div style="display:flex;align-items:center;gap:12px;margin-bottom:32px;flex-wrap:wrap">
<For each={PREVIEW_STATES}>
{(state) => (
<button
type="button"
onClick={() => setPreviewState(state.key)}
style={previewState() === state.key
? 'border:2px solid #FF5E13;background:white;color:#FF5E13;padding:8px 16px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer'
: 'border:1px solid #E5E7EB;background:white;color:#374151;padding:8px 16px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer'}
>
{state.label}
</button>
)}
</For>
</div>
{/* Preview Card */}
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:48px;text-align:center">
{/* Clock icon */}
<div style="display:flex;justify-content:center">
<Clock size={48} color="#FF5E13" />
</div>
<p style="font-size:20px;font-weight:700;color:#111827;margin-top:16px;margin-bottom:0">Verification Status</p>
<p style="font-size:14px;color:#6B7280;margin-top:8px;max-width:400px;margin-left:auto;margin-right:auto">
Your submitted information and documents are currently under review.
</p>
{/* Progress Row */}
<div style="display:flex;align-items:center;justify-content:center;gap:0;margin-top:32px">
{/* Step 1: Submitted */}
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
<div style="width:32px;height:32px;border-radius:50%;background:#10B981;display:flex;align-items:center;justify-content:center">
<span style="color:white;font-size:16px;font-weight:700;line-height:1"></span>
</div>
<span style="font-size:12px;font-weight:500;color:#10B981">Submitted</span>
</div>
{/* Connector */}
<div style="width:80px;height:2px;background:#FF5E13;margin-bottom:20px" />
{/* Step 2: Under Review */}
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
<div style="width:32px;height:32px;border-radius:50%;background:#FF5E13;display:flex;align-items:center;justify-content:center">
<Clock size={16} color="white" />
</div>
<span style="font-size:12px;font-weight:500;color:#FF5E13">Under Review</span>
</div>
{/* Connector */}
<div style="width:80px;height:2px;background:#E5E7EB;margin-bottom:20px" />
{/* Step 3: Verification Result */}
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
<div style="width:32px;height:32px;border-radius:50%;background:#E5E7EB;display:flex;align-items:center;justify-content:center">
<span style="color:#9CA3AF;font-size:14px;font-weight:700;line-height:1">?</span>
</div>
<span style="font-size:12px;font-weight:500;color:#9CA3AF">Verification Result</span>
</div>
</div>
<button type="button" style="height:40px;padding:0 24px;border-radius:10px;background:#0D0D2A;color:white;font-size:14px;font-weight:600;border:none;cursor:pointer;margin-top:24px">
Back to Dashboard
</button>
</div>
</div>
</Show>
</div> </div>
</AdminShell> </AdminShell>
); );