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 AdminShell from '~/components/AdminShell';
import { updateModuleRecord, deleteModuleRecord } from '~/lib/admin/client';
import type { CrudRecord } from '~/lib/admin/types';
import { ChevronDown, SlidersHorizontal, Download, MoreVertical } from 'lucide-solid';
type DesignationRecord = CrudRecord & {
code?: string;
department?: string;
level?: string;
type DesignationRecord = {
id: string;
name: string;
code: string;
department: string;
level: string;
description?: string;
totalEmployees?: number;
createdDate?: string;
totalEmployees: number;
status: 'ACTIVE' | 'INACTIVE';
createdDate: string;
canManageTeam?: boolean;
canApprove?: boolean;
};
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: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-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: '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: 'z5', name: 'Product Manager', code: 'PM-005', department: 'Product', level: 'Manager', totalEmployees: 6, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-10' },
{ 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: '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', createdDate: '2024-01-20' },
{ 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', createdDate: '2024-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: '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 DEPARTMENTS = ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance', 'Operations', 'Product', 'Customer Success'];
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 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 }) {
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 }) {
return (
<label style="display:block">
@ -71,32 +106,32 @@ function FormSelect(props: { label: string; required?: boolean; value: string; o
}
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 [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
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 [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [editingId, setEditingId] = createSignal<string | null>(null);
// form state
const [name, setName] = createSignal('');
const [code, setCode] = createSignal('');
const [department, setDepartment] = createSignal('');
const [level, setLevel] = 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 [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 () => {
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) {
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 : [];
@ -110,7 +145,6 @@ export default function DesignationManagementPage() {
description: String(item.description ?? ''),
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? 0),
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: String(item.updatedAt ?? item.updated_at ?? ''),
createdDate: String(item.createdDate ?? item.created_at ?? ''),
})));
return;
@ -123,206 +157,148 @@ export default function DesignationManagementPage() {
onMount(() => void load());
const filteredRows = createMemo(() => {
let r = rows();
if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase());
if (deptFilter() !== 'all') r = r.filter((d) => d.department === deptFilter());
const q = search().toLowerCase();
if (q) r = r.filter((d) => d.name.toLowerCase().includes(q) || String(d.code ?? '').toLowerCase().includes(q));
return r;
if (!q) return rows();
return rows().filter((d) =>
d.name.toLowerCase().includes(q) || d.code.toLowerCase().includes(q) || d.department.toLowerCase().includes(q)
);
});
const resetForm = () => {
setEditingId(null); setName(''); setCode(''); setDepartment('');
setLevel(''); setDescription(''); setStatus('ACTIVE');
setCanManageTeam(false); setCanApprove(false); setFormTab('general'); setError('');
setName(''); setCode(''); setDepartment(''); setLevel('');
setDescription(''); setFormStatus('ACTIVE');
setCanManageTeam(false); setCanApprove(false);
setPermViewEmp(false); setPermCreateEmp(false);
setPermEditEmp(false); setPermDeleteEmp(false);
setPermAssignRoles(false);
setFormTab('general');
};
const openCreate = () => { resetForm(); setView('form'); };
const openEdit = (row: DesignationRecord) => {
setEditingId(row.id);
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 handleTabChange = (tab: 'all' | 'create') => {
setMainTab(tab);
if (tab === 'create') resetForm();
setOpenMenuId(null);
};
const save = async () => {
if (!name().trim()) { setError('Designation name is required.'); setFormTab('general'); return; }
setIsSaving(true); setError('');
const formatDate = (d: string) => {
if (!d) return '—';
try {
const payload: Partial<DesignationRecord> = {
name: name().trim(), code: code().trim() || undefined,
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 new Date(d).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
} catch { return d; }
};
return (
<AdminShell>
<div style="width:100%;padding-bottom:32px">
<div style="padding:24px">
{/* Page header */}
<div style="margin-bottom:24px">
<h1 style="font-size:28px;font-weight:700;color:#111827;line-height:1.2">Designation Management</h1>
<p style="margin-top:4px;font-size:14px;color:#6B7280">Manage all job designations and position levels</p>
<div>
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Designation Management</h1>
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage all designations and job positions</p>
</div>
{/* ── LIST VIEW ── */}
<Show when={view() === 'list'}>
<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) => (
{/* Main tabs */}
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
<button
type="button"
onClick={tab.action}
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'}`}
onClick={() => handleTabChange('all')}
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>
))}
</div>
{/* Table card */}
<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)">
{/* All Designations view */}
<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 */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
type="text"
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
type="button"
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
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"
>
<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
</button>
<Show when={filterMenuOpen()}>
<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)">
{(['all', 'ACTIVE', 'INACTIVE'] as const).map((s) => (
<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'}
</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>
<button
type="button"
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"
>
<Download size={14} />
Export
</button>
</div>
{/* Table */}
<div style="overflow-x:auto">
<table style="min-width:100%">
<table style="width:100%;border-collapse:collapse">
<thead>
<tr style="background:#0D0D2A;text-align:left">
<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">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">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">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">Actions</th>
<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;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;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;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;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;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;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;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;text-align:left">Actions</th>
</tr>
</thead>
<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()}>
{(row) => (
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
<td style="padding:12px 20px">
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
</td>
<td style="padding:12px 20px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">{String(row.code || '—')}</span>
</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{String(row.department || '—')}</td>
<td style="padding:12px 20px">
<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">
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 20px;font-size:14px;color:#111827;font-weight:600">{row.name}</td>
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.code}</td>
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.department}</td>
<td style="padding:12px 20px;font-size:14px;color:#111827">{levelBadge(row.level)}</td>
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.totalEmployees}</td>
<td style="padding:12px 20px;font-size:14px;color:#111827"><StatusBadge status={row.status} /></td>
<td style="padding:12px 20px;font-size:14px;color:#111827">{formatDate(row.createdDate)}</td>
<td style="padding:12px 20px;font-size:14px;color:#111827;position:relative">
<button
type="button"
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"
aria-label="More actions"
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"
>
<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>
<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)">
<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">
<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>
Edit Designation
<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={() => { 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 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">
<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>
{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}
</button>
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
<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>
<button
type="button"
onClick={() => { setOpenMenuId(null); }}
style="display:block;width:100%;text-align:left;padding:9px 14px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer"
class="hover:bg-[#FFF5F5]"
>
Delete
</button>
</div>
@ -331,185 +307,214 @@ export default function DesignationManagementPage() {
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
{/* Pagination */}
<Show when={filteredRows().length > 0}>
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
<p style="font-size:13px;color:#6B7280">
Showing <strong style="font-weight:600;color:#111827">1{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> designations
</p>
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-top:1px solid #F3F4F6">
<span style="font-size:13px;color:#6B7280">
Showing 1-{filteredRows().length} of {filteredRows().length} designations
</span>
<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
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}
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"
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
<button
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>
);
})}
</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>
{/* General Information */}
<Show when={formTab() === 'general'}>
<div style="display:flex;flex-direction:column;gap:20px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<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" />
{/* Create Designation 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;gap:0;border-bottom:1px solid #E5E7EB;padding:0 24px">
<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>
{/* General Information tab */}
<Show when={formTab() === 'general'}>
<div style="padding:24px">
<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>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</FormSelect>
<FormSelect label="Designation Level" required value={level()} onChange={setLevel}>
<FormSelect
label="Designation Level"
required
value={level()}
onChange={setLevel}
>
<option value="">Select level</option>
<For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For>
</FormSelect>
</div>
<div style="grid-column:1 / -1">
<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="Brief description of this designation's responsibilities..."
rows="3"
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
placeholder="Describe the designation role and responsibilities..."
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;box-sizing:border-box;resize:vertical;font-family:inherit"
/>
</label>
</div>
</div>
</div>
</Show>
{/* Designation Settings */}
{/* Designation Settings tab */}
<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>
<p style="font-size:14px;font-weight:600;color:#111827">Designation Status</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">Set whether this designation is currently active</p>
<div style="margin-top:12px;display:flex;gap:10px">
{(['ACTIVE', 'INACTIVE'] as const).map((s) => (
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 10px 0">Designation Status</p>
<div style="display:flex;gap:10px">
<button
type="button"
onClick={() => setStatus(s)}
style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? '#FF5E13' : '#E5E7EB'};background:${status() === s ? '#FFF3EE' : 'white'};color:${status() === s ? '#FF5E13' : '#6B7280'}`}
onClick={() => setFormStatus('ACTIVE')}
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>
))}
</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>
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage Team</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can manage team members</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="font-size:12px;color:#6B7280;margin:0">Enable this to allow team member management capabilities</p>
</div>
<button
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>
<Toggle on={canManageTeam()} onChange={setCanManageTeam} />
</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>
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can approve employee requests</p>
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Approval Permissions</p>
<p style="font-size:12px;color:#6B7280;margin:0">Enable this to allow approving requests and actions</p>
</div>
<button
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>
<Toggle on={canApprove()} onChange={setCanApprove} />
</div>
</div>
</Show>
{/* Permissions */}
{/* Permissions tab */}
<Show when={formTab() === 'permissions'}>
<div style="display:flex;flex-direction:column;gap:16px">
<p style="font-size:13px;color:#6B7280">Select the permissions available to employees with this designation.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
{['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees', 'Assign Roles', 'Approve Requests', 'Manage Team Members'].map((item) => (
<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" />
<span style="font-size:13px;font-weight:500;color:#374151">{item}</span>
</label>
<div style="padding:24px">
{/* Employee Management */}
<p style="font-size:14px;font-weight:600;color:#111827;margin:0 0 12px 0">Employee Management</p>
<div style="display:flex;flex-direction:column;gap:0">
{[
{ label: 'View Employees', sig: permViewEmp, set: setPermViewEmp },
{ label: 'Create Employees', sig: permCreateEmp, set: setPermCreateEmp },
{ 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>
{/* 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>
</Show>
</div>
{/* Form actions */}
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
{/* Form footer */}
<div style="display:flex;align-items:center;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #E5E7EB">
<button
type="button"
onClick={() => { setView('list'); resetForm(); }}
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
onClick={() => { resetForm(); setMainTab('all'); }}
style="height:38px;padding:0 20px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:14px;cursor:pointer"
>
Cancel
</button>
<button
type="button"
onClick={() => void save()}
disabled={isSaving()}
style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer"
onClick={() => {
if (!name().trim()) { setFormTab('general'); return; }
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>
</div>
</div>
</Show>
</div>
</AdminShell>
);

View file

@ -1,71 +1,41 @@
import { createResource, createSignal, For, Show } from 'solid-js';
import { Eye, SquarePen, Search, Trash2 } from 'lucide-solid';
import AdminShell from '~/components/AdminShell';
import { Upload } from 'lucide-solid';
const API = '/api/gateway';
type Employee = {
id: string;
empId?: string;
employeeId?: string;
full_name?: string;
name?: string;
email: string;
role?: { id: string; name: string } | string;
role_name?: string;
phone?: string;
department?: { id: string; name: string } | string;
department_name?: string;
designation?: { id: string; name: string } | 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;
status?: string;
};
const FALLBACK: Employee[] = [
{
id: 'EMP001',
full_name: 'John Smith',
email: 'john.smith@nxtgauge.com',
department: { id: 'd1', name: 'Engineering' },
designation: { id: 'dg1', name: 'Senior Software Engineer' },
role: { id: 'r1', name: 'Engineering Lead' },
is_active: true,
},
{
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,
},
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: '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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
];
async function fetchEmployees(): Promise<Employee[]> {
@ -74,187 +44,262 @@ async function fetchEmployees(): Promise<Employee[]> {
if (!res.ok) throw new Error();
const data = await res.json();
const list = Array.isArray(data) ? data : (data.employees ?? data.users ?? []);
return list.length > 0 ? list : FALLBACK;
return list.length > 0 ? list : FALLBACK_EMPLOYEES;
} catch {
return FALLBACK;
return FALLBACK_EMPLOYEES;
}
}
function empName(e: Employee) {
return e.full_name || e.name || '—';
}
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) {
function getEmpId(e: Employee) { return e.empId || e.employeeId || e.id || '—'; }
function getEmpName(e: Employee) { return e.full_name || e.name || '—'; }
function getEmpDept(e: Employee) {
if (!e.department) return e.department_name ?? '—';
if (typeof e.department === 'string') return e.department;
return e.department.name ?? '—';
}
function empDesig(e: Employee) {
function getEmpDesignation(e: Employee) {
if (!e.designation) return e.designation_name ?? '—';
if (typeof e.designation === 'string') return e.designation;
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();
if (raw === 'PROBATION') return 'Probation';
if (e.is_active === false || raw === 'INACTIVE') return 'Inactive';
return 'Active';
if (e.is_active === false || raw === 'INACTIVE') return 'INACTIVE';
return 'ACTIVE';
}
function StatusBadge(props: { status: 'Active' | 'Inactive' | 'Probation' }) {
const classes = () => {
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]';
};
function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) {
const active = () => props.status === 'ACTIVE';
return (
<span class={`inline-flex min-w-[68px] items-center justify-center border px-2 py-1 text-[11px] font-semibold ${classes()}`}>
{props.status}
<span style={active()
? '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>
);
}
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() {
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 [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 list = employees() ?? [];
const q = search().trim().toLowerCase();
if (!q) return list;
return list.filter((e) =>
[e.id, empName(e), e.email, empDept(e), empDesig(e), empRole(e)]
.join(' ')
.toLowerCase()
.includes(q),
);
return list.filter((e) => {
const matchSearch = !q || [getEmpId(e), getEmpName(e), e.email, getEmpDept(e), getEmpDesignation(e)].join(' ').toLowerCase().includes(q);
const matchDept = !deptFilter() || getEmpDept(e) === deptFilter();
const matchDesignation = !designationFilter() || getEmpDesignation(e) === designationFilter();
const matchEmpType = !empTypeFilter() || getEmpEmploymentType(e) === empTypeFilter();
const matchStatus = !statusFilter() || getEmpStatus(e) === statusFilter();
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 (
<AdminShell>
<div class="w-full space-y-4 pb-8">
<div style="width:100%;padding-bottom:32px">
{/* Page Header */}
<div>
<h1 class="text-[48px] font-bold leading-[1.1] tracking-[-0.02em] text-[#0B1246]">Employee Management</h1>
<p class="mt-1 text-[15px] text-[#7E849F]">Manage internal employees and their information</p>
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Employee Management</h1>
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage internal employees and their information</p>
</div>
<div class="rounded-none border border-[#D9DDE6] bg-white">
<div class="border-b border-[#D9DDE6] px-6">
<div class="flex items-end gap-10">
<button
type="button"
onClick={() => setActiveTab('list')}
class={`relative pb-3 pt-4 text-[36px] font-semibold ${
activeTab() === 'list' ? 'text-[#0B1246]' : 'text-[#7E849F]'
}`}
>
{/* Main Tabs */}
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
<button type="button" style={mainTabStyle(mainTab() === 'all')} onClick={() => setMainTab('all')}>
All Employees
<Show when={activeTab() === 'list'}>
<span class="absolute bottom-0 left-0 h-[4px] w-full bg-[#FF5E13]" />
</Show>
</button>
<button
type="button"
onClick={() => setActiveTab('create')}
class={`relative pb-3 pt-4 text-[36px] font-semibold ${
activeTab() === 'create' ? 'text-[#0B1246]' : 'text-[#7E849F]'
}`}
>
<button type="button" style={mainTabStyle(mainTab() === 'create')} onClick={() => setMainTab('create')}>
Create Employee
<Show when={activeTab() === 'create'}>
<span class="absolute bottom-0 left-0 h-[4px] w-full bg-[#FF5E13]" />
</Show>
</button>
</div>
</div>
<Show
when={activeTab() === 'list'}
fallback={
<div class="px-6 py-12 text-[14px] text-[#6B7280]">
Create Employee form will follow this new base design next.
</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]" />
{/* All Employees View */}
<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 Row 1 */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px 8px 20px">
<input
type="text"
placeholder="Search employees..."
placeholder="Search employees"
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
class="w-full border-0 bg-transparent text-[34px] text-[#0B1246] placeholder:text-[#9CA3AF] outline-none"
onInput={(e) => { setSearch(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:text;flex:1;max-width:240px;box-sizing:border-box`}
/>
</label>
<div class="h-[58px] border border-[#D9DDE6] bg-white" />
<button
type="button"
class="h-[58px] border border-[#D9DDE6] bg-white text-[34px] font-medium text-[#0B1246] transition-colors hover:bg-[#F9FAFB]"
<select
value={deptFilter()}
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"
>
<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
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full min-w-[1400px] table-fixed border-collapse">
{/* Table */}
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;min-width:1100px">
<thead>
<tr class="bg-[#02033B] text-left">
{['ID', 'Name', 'Email', 'Department', 'Designation', 'Role', 'Status', 'Actions'].map((h) => (
<th class="px-6 py-5 text-[31px] font-semibold text-white">{h}</th>
))}
<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;white-space:nowrap">Employee 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;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>
</thead>
<tbody>
<Show when={employees.loading}>
<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>
</Show>
<Show when={!employees.loading && filtered().length === 0}>
<Show when={!employees.loading && paged().length === 0}>
<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>
</Show>
<For each={filtered()}>
<For each={paged()}>
{(emp) => (
<tr class="border-b border-[#D9DDE6] bg-white align-middle">
<td class="px-6 py-4 text-[35px] italic text-[#303A67]">{emp.id}</td>
<td class="px-6 py-4 text-[41px] font-semibold leading-[1.15] text-[#0B1246]">{empName(emp)}</td>
<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'} />
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA]">
<td style="padding:12px 20px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">{getEmpId(emp)}</span>
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-4">
<button type="button" class="text-[#FF5E13] transition-colors hover:text-[#E04D0A]" aria-label="View">
<Eye size={23} />
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827;white-space:nowrap">{getEmpName(emp)}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{emp.email}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151;white-space:nowrap">{emp.phone || '—'}</td>
<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 type="button" class="text-[#5B628D] transition-colors hover:text-[#303A67]" aria-label="Edit">
<SquarePen size={23} />
<Show when={openActionId() === emp.id}>
<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 type="button" class="text-[#5B628D] transition-colors hover:text-[#303A67]" aria-label="Delete">
<Trash2 size={23} />
<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]">
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>
</div>
</Show>
</td>
</tr>
)}
@ -262,9 +307,388 @@ export default function EmployeesIndexPage() {
</tbody>
</table>
</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>
</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>
);
}

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';
const API = '/api/gateway';
const LEGACY_INTERNAL_PREVIEW_BASE = String(import.meta.env.VITE_LEGACY_ADMIN_PREVIEW_URL || 'http://localhost:3002').replace(/\/+$/, '');
// ---------- Types ----------
type SidebarItem = { key: string; label: string; visible: boolean; order: number };
type Field = { id: string; label: string; type: 'text' | 'number' | 'select' | 'date'; required: boolean; placeholder?: string };
type Tab = { id: string; title: string; fields: Field[] };
type Widget = { id: string; title: string; metric: string; description?: string };
type Section = { id: string; title: string; tabs: Tab[]; widgets: Widget[] };
type Dashboard = { id: string; roleId: string; roleName: string; title: string; description?: string; status: string; version: number; sidebar: SidebarItem[]; sections: Section[] };
type InternalRole = { id: string; key: string; name: string };
type InternalDashboard = {
id: string;
name: string;
department: string;
designation: string;
role: string;
widgetsCount: number;
status: string;
};
const FIELD_TYPES: Field['type'][] = ['text', 'number', 'select', 'date'];
const makeId = (prefix: string) => `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
const FALLBACK: InternalDashboard[] = [
{ id: 'id1', name: 'Super Admin Dashboard', department: 'Administration', designation: 'Super Admin', role: 'Super Admin', widgetsCount: 12, status: 'ACTIVE' },
{ id: 'id2', name: 'Engineering Lead Dashboard', department: 'Engineering', designation: 'Engineering Lead', role: 'Engineering Lead', widgetsCount: 8, status: 'ACTIVE' },
{ id: 'id3', name: 'HR Manager Dashboard', department: 'Human Resources', designation: 'HR Manager', role: 'HR Admin', widgetsCount: 6, status: 'ACTIVE' },
{ id: 'id4', name: 'Finance Dashboard', department: 'Finance', designation: 'Finance Analyst', role: 'Finance Controller', widgetsCount: 5, status: 'DRAFT' },
];
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 ----------
function PreviewSection(props: { section: Section }) {
const [activeTabId, setActiveTabId] = createSignal(props.section.tabs[0]?.id || '');
const activeTab = createMemo(() => props.section.tabs.find((t) => t.id === activeTabId()) || props.section.tabs[0] || null);
return (
<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 }) {
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 () => {
async function loadDashboards(): Promise<InternalDashboard[]> {
try {
setLoading(true);
setError('');
const res = await fetch(`${API}/api/admin/dashboard-config?audience=INTERNAL`);
if (!res.ok) throw new Error('Failed to load internal dashboards');
const res = await fetch(`${API}/api/admin/internal-dashboards`);
if (!res.ok) throw new Error('Failed');
const data = await res.json();
const rows = (Array.isArray(data) ? data : (data.dashboards || []))
.filter((item: any) => String(item?.audience || '').toUpperCase() === 'INTERNAL')
.map((item: any) => ({
const rows = Array.isArray(data) ? data : (data.dashboards || data.templates || []);
if (!rows.length) return FALLBACK;
return rows.map((item: any) => ({
id: String(item.id || ''),
roleId: String(item.role_id || ''),
roleName: '',
title: 'Internal Dashboard',
description: '',
status: item.is_active ? 'published' : 'draft',
version: Number(item.version) || 1,
sidebar: [],
sections: [],
name: String(item.name || item.title || 'Untitled Dashboard'),
department: String(item.department || item.assigned_department || ''),
designation: String(item.designation || item.assigned_designation || ''),
role: String(item.role || item.role_name || item.assigned_role || ''),
widgetsCount: Number(item.widgets_count || item.widgetsCount || 0),
status: item.is_active ? 'ACTIVE' : (String(item.status || 'DRAFT').toUpperCase()),
}));
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 {
// Keep list-summary data if detail fetch fails.
return FALLBACK;
}
};
}
const openDashboard = async (configId: string) => {
setSelectedId(configId);
setActiveTab('overview');
await hydrateDashboard(configId);
};
function StatusBadge(props: { status: string }) {
const isActive = () => props.status === 'ACTIVE';
const isDraft = () => props.status === 'DRAFT';
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;
const livePreviewUrl = createMemo(() => {
const roleId = String(selected()?.roleId || '').trim();
if (!roleId) return `${LEGACY_INTERNAL_PREVIEW_BASE}/internal-dashboard-management`;
return `${LEGACY_INTERNAL_PREVIEW_BASE}/internal-dashboard-management?roleId=${encodeURIComponent(roleId)}`;
export default function InternalDashboardManagementPage() {
const [dashboards, { refetch }] = createResource(loadDashboards);
const [search, setSearch] = createSignal('');
const [statusFilter, setStatusFilter] = createSignal('All');
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 d = selected();
if (!d) return;
if (!d.roleId) {
setError('Please select an internal role before saving.');
return;
}
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 },
}),
const totalTemplates = createMemo(() => (dashboards() || FALLBACK).length);
const activeTemplates = createMemo(() => (dashboards() || FALLBACK).filter((d) => d.status === 'ACTIVE').length);
const draftTemplates = createMemo(() => (dashboards() || FALLBACK).filter((d) => d.status === 'DRAFT').length);
const assignedRoles = createMemo(() => {
const roles = new Set((dashboards() || FALLBACK).map((d) => d.role).filter(Boolean));
return roles.size;
});
if (!res.ok) throw new Error('Failed to save dashboard');
await loadDashboards();
const next = dashboards().find((item) => item.roleId === d.roleId);
if (next) await openDashboard(next.id);
} catch (err: any) {
setError(err.message || 'Failed to save dashboard');
} finally {
setSaving(false);
}
};
const unassignedRoles = createMemo(() => (dashboards() || FALLBACK).filter((d) => !d.role).length);
const pillActive = 'background:#0D0D2A;color:white;border:none;height:32px;border-radius:9999px;padding:0 16px;font-size:13px;font-weight:500;cursor:pointer';
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';
// ---------- List view ----------
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
<Show when={!selected()}>
{/* Header & Title */}
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
<div style="width:100%;padding-bottom:32px">
{/* Header */}
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px">
<div>
<h1 class="text-[32px] font-bold text-[#0D0D2A] leading-tight">Internal Dashboard Management</h1>
<p class="text-[15px] text-[#8087a0] mt-1">Configure dashboards for internal staff members</p>
<h1 style="font-size:28px;font-weight:700;color:#111827">Internal Dashboard Management</h1>
<p style="margin-top:4px;font-size:14px;color:#6B7280">Manage internal dashboard templates for departments, designations, and roles.</p>
</div>
<div class="flex items-center gap-3">
<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]">
<div style="display:flex;gap:8px;align-items:center;flex-shrink:0;margin-top:4px">
<button style={activeTab() === 'dashboard' ? pillActive : pillInactive} onClick={() => setActiveTab('dashboard')}>
Dashboard Management
</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
</button>
</div>
</div>
{/* 5 KPI Cards Row */}
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Total Dashboard<br/>Templates</p>
<p class="mt-3 text-[32px] font-bold text-[#0D0D2A] leading-none">18</p>
{/* Stats Row */}
<div style="display:flex;gap:16px;margin-top:24px">
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p style="font-size:13px;color:#6B7280;margin:0">Total Dashboard Templates</p>
<p style="font-size:28px;font-weight:700;color:#111827;margin:4px 0 0">{dashboards.loading ? '—' : totalTemplates()}</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Active Templates</p>
<p class="mt-3 text-[32px] font-bold text-[#00c853] leading-none">14</p>
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p style="font-size:13px;color:#6B7280;margin:0">Active Templates</p>
<p style="font-size:28px;font-weight:700;color:#FF5E13;margin:4px 0 0">{dashboards.loading ? '—' : activeTemplates()}</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Draft Templates</p>
<p class="mt-3 text-[32px] font-bold text-[#64748b] leading-none">2</p>
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p style="font-size:13px;color:#6B7280;margin:0">Draft Templates</p>
<p style="font-size:28px;font-weight:700;color:#6B7280;margin:4px 0 0">{dashboards.loading ? '—' : draftTemplates()}</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Assigned Roles</p>
<p class="mt-3 text-[32px] font-bold text-[#2962ff] leading-none">9</p>
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p style="font-size:13px;color:#6B7280;margin:0">Assigned Roles</p>
<p style="font-size:28px;font-weight:700;color:#3730A3;margin:4px 0 0">{dashboards.loading ? '—' : assignedRoles()}</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Unassigned Roles</p>
<p class="mt-3 text-[32px] font-bold text-[#ff6e30] leading-none">2</p>
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
<p style="font-size:13px;color:#6B7280;margin:0">Unassigned Roles</p>
<p style="font-size:28px;font-weight:700;color:#111827;margin:4px 0 0">{dashboards.loading ? '—' : unassignedRoles()}</p>
</div>
</div>
{/* Main Table Section */}
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
<div class="rounded-[20px] bg-white p-5">
{/* Table Action Header */}
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between mb-6">
<h2 class="text-[22px] font-bold text-[#0D0D2A]">Internal Dashboard<br/>Templates</h2>
<div class="flex items-center gap-3">
<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]">
{/* Table Card */}
<div style="margin-top:24px;border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
{/* Card Header */}
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #F3F4F6">
<h2 style="font-size:16px;font-weight:600;color:#111827;margin:0">Internal Dashboard Templates</h2>
<div style="display:flex;gap:8px">
<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
</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
</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()}>
<span class="mr-2 text-lg leading-none">+</span> {creating() ? 'Creating...' : 'Create Dashboard Template'}
</button>
<A
href="/admin/internal-dashboard-management/new"
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>
{/* Error Message */}
<Show when={error()}>
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
{/* 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>
{/* Filter Bar */}
<div style="display:flex;gap:12px;align-items:center;padding:12px 20px;border-bottom:1px solid #F3F4F6">
<div style="position:relative;flex:1;max-width:320px">
<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>
<input
type="text"
placeholder="Search templates..."
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"
placeholder="Search dashboards..."
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 class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
<select
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>
{/* Table */}
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;min-width:900px">
<thead>
<tr class="bg-[#0D0D2A] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl">DASHBOARD NAME</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED DEPARTMENT</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED DESIGNATION</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED INTERNAL ROLE</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-center">WIDGETS COUNT</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tr-xl text-center">STATUS</th>
<tr style="background:#0D0D2A">
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">DASHBOARD NAME</th>
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ASSIGNED DEPARTMENT</th>
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ASSIGNED DESIGNATION</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 style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">WIDGETS COUNT</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>
</thead>
<tbody>
<Show when={loading()}>
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-sm">Loading templates...</td></tr>
<Show when={dashboards.loading}>
<tr>
<td colspan="7" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">Loading dashboards...</td>
</tr>
</Show>
<Show when={!loading() && dashboards().length === 0}>
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-sm">No dashboard templates found.</td></tr>
<Show when={!dashboards.loading && filtered().length === 0}>
<tr>
<td colspan="7" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">No dashboard templates found.</td>
</tr>
</Show>
<For each={dashboards()}>
{(d) => (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc] cursor-pointer" onClick={() => void openDashboard(d.id)}>
<td class="px-6 py-4 text-[14px] font-bold text-[#0D0D2A]">{d.title}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Administration</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Super Admin</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{d.roleName || 'Unassigned'}</td>
<td class="px-6 py-4 text-[14px] text-[#475569] text-center">{d.sections.length * 3 || 12}</td>
<td class="px-6 py-4 text-center">
<span class={`inline-flex items-center justify-center rounded-lg px-3 py-1 text-[12px] font-bold ${
d.status === 'published' ? 'bg-[#e6f9ed] text-[#00c853]' : 'bg-[#f1f5f9] text-[#64748b]'
}`}>
{d.status === 'published' ? 'Active' : 'Draft'}
</span>
<For each={filtered()}>
{(dashboard, index) => (
<tr style={`border-bottom:1px solid #F3F4F6;background:${index() % 2 === 0 ? 'white' : '#FAFAFA'}`}>
<td style="padding:12px 20px;font-size:14px;color:#111827;font-weight:500">{dashboard.name}</td>
<td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.department || '—'}</td>
<td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.designation || '—'}</td>
<td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.role || '—'}</td>
<td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.widgetsCount}</td>
<td style="padding:12px 20px">
<StatusBadge status={dashboard.status} />
</td>
<td style="padding:12px 20px;text-align:right">
<div style="display:inline-flex;gap:8px">
<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>
</tr>
)}
@ -539,267 +235,16 @@ export default function InternalDashboardManagementPage() {
</tbody>
</table>
</div>
</div>
</section>
</Show>
{/* ---------- Builder view ---------- */}
<Show when={selected()}>
<div class="builder-header">
<div>
<h2>Internal Dashboard Builder</h2>
<p>Manage menu items, sections, tabs, form fields, and summary cards from one place.</p>
</div>
<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>
{/* Pagination */}
<div style="display:flex;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px;align-items:center">
<span style="font-size:13px;color:#6B7280">Showing {filtered().length} of {(dashboards() || FALLBACK).length} templates</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>
<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>
</AdminShell>

View file

@ -1,13 +1,393 @@
import { onMount } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
import { A } from '@solidjs/router';
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 (
<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>
);
}

View file

@ -1,357 +1,463 @@
import { A, useNavigate } from '@solidjs/router';
import { createResource, createSignal, For, Show } from 'solid-js';
import { For, Show, createSignal } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { ArrowUpDown, Filter, Download, Eye, Pencil } from 'lucide-solid';
const API = '/api/gateway';
import { ChevronDown, SlidersHorizontal, Download, MoreVertical } from 'lucide-solid';
type Role = {
id: string;
key: string;
name: string;
audience: string;
description?: string;
department_name?: string;
is_active: boolean;
users_assigned: number;
permissions_count: number;
created_at: string;
department: string;
usersAssigned: number;
permissionsCount: number;
status: 'ACTIVE' | 'INACTIVE';
createdDate: string;
};
type ListResponse = { roles: Role[]; total: number; page: number; per_page: number };
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: '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: '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: '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: '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: 'r1', name: 'Engineering Lead', department: 'Engineering', usersAssigned: 12, permissionsCount: 28, status: 'ACTIVE', createdDate: '2026-01-15' },
{ id: 'r2', name: 'Marketing Manager', department: 'Marketing', usersAssigned: 8, permissionsCount: 18, status: 'ACTIVE', createdDate: '2026-01-20' },
{ id: 'r3', name: 'Sales Director', department: 'Sales', usersAssigned: 15, permissionsCount: 32, status: 'ACTIVE', createdDate: '2026-02-01' },
{ id: 'r4', name: 'HR Admin', department: 'Human Resources', usersAssigned: 5, permissionsCount: 24, status: 'ACTIVE', createdDate: '2026-02-05' },
{ 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> {
try {
const qs = new URLSearchParams({ audience: 'INTERNAL', q: params.q, page: String(params.page), per_page: '10' });
const res = await fetch(`${API}/api/admin/roles?${qs}`);
if (!res.ok) throw new Error('Failed');
const data = await res.json();
if (data.roles?.length > 0) return data;
throw new Error('empty');
} catch {
const q = params.q.toLowerCase();
const filtered = q ? FALLBACK_ROLES.filter((r) => r.name.toLowerCase().includes(q) || r.key.toLowerCase().includes(q)) : FALLBACK_ROLES;
return { roles: filtered, total: filtered.length, page: 1, per_page: 10 };
const MODULES = [
'Department Management',
'Designation Management',
'Internal Role Management',
'Employee Management',
'External Role Management',
'External Onboarding Management',
'Internal Dashboard Management',
'External Dashboard Management',
'Verification Management',
'Approval Management',
'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) {
try {
const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
} catch { return '—'; }
}
function StatusBadge(props: { active: boolean }) {
function StatusBadge(props: { status: string }) {
const active = () => props.status === 'ACTIVE';
return (
<span class={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-[12px] font-medium ${
props.active
? 'border-[#FF5E13] bg-[#FFF3EE] text-[#FF5E13]'
: '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 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`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
{active() ? 'Active' : 'Inactive'}
</span>
);
}
function UsersBadge(props: { count: number }) {
function Toggle(props: { on: boolean; onChange: (v: boolean) => void }) {
return (
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full bg-[#EEF2FF] text-[12px] font-semibold text-[#4F46E5]">
{props.count > 99 ? '99+' : String(props.count).padStart(2, '0')}
</span>
<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;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 = [
{ label: 'All Roles', href: '/admin/roles' },
{ label: 'Create Role', href: '/admin/roles/create' },
{ label: 'View Role', href: '/admin/roles/view' },
{ label: 'Edit Role', href: '/admin/roles/edit' },
];
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string }) {
return (
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{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() {
const navigate = useNavigate();
export default function RolesPage() {
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [formTab, setFormTab] = createSignal<'general' | 'access' | 'settings'>('general');
// All Roles state
const [search, setSearch] = createSignal('');
const [debouncedSearch, setDebouncedSearch] = createSignal('');
const [page, setPage] = createSignal(1);
let debounceTimer: ReturnType<typeof setTimeout>;
const handleSearch = (val: string) => {
setSearch(val);
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => { setDebouncedSearch(val); setPage(1); }, 300);
};
// Create Role state
const [roleName, setRoleName] = createSignal('');
const [roleCode, setRoleCode] = createSignal('');
const [department, setDepartment] = createSignal('');
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(
() => ({ q: debouncedSearch(), page: page() }),
loadRoles,
const filteredRoles = () => {
const q = search().toLowerCase();
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 (
<AdminShell>
<div class="w-full space-y-6 pb-8">
{/* Page header */}
<div style="padding:24px">
{/* Page Header */}
<div>
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Internal Role Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Define and manage organizational access levels with granular permission control.</p>
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Internal Role Management</h1>
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage internal roles and permissions</p>
</div>
{/* Tabs */}
<div class="flex items-center gap-6 border-b border-[#E5E7EB]">
<For each={TABS}>
{(tab) => {
const active = () => tab.href === '/admin/roles';
return (
<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]'
}`}
{/* Main Tabs */}
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
<button
type="button"
onClick={() => setMainTab('all')}
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'}`}
>
{tab.label}
</A>
);
}}
</For>
All Roles
</button>
<button
type="button"
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>
{/* Table card */}
<div class="overflow-hidden rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
{/* Filter bar */}
<div class="flex items-center gap-2 px-5 py-4">
{/* ── ALL ROLES TAB ── */}
<Show when={mainTab() === 'all'}>
{/* Edge-to-edge card */}
<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 */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
type="text"
placeholder="Filter by role name or code..."
placeholder="Search roles..."
value={search()}
onInput={(e) => handleSearch(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"
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"
/>
<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">
<ArrowUpDown size={13} />
<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">
<ChevronDown size={14} />
Sort
</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">
<Filter size={13} />
<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">
<SlidersHorizontal size={14} />
Filters
</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">
<Download size={13} />
<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={14} />
Export
</button>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="min-w-full">
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse">
<thead>
<tr class="bg-[#0D0D2A] text-left">
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Role Name</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Role Code</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Department</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Users Assigned</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Permissions</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Status</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Created Date</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">AC</th>
<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;white-space:nowrap">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">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 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 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">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>
</tr>
</thead>
<tbody class="divide-y divide-[#F3F4F6]">
<Show when={data.loading}>
<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()}>
<tbody>
<For each={shown()}>
{(role) => (
<tr class="hover:bg-[#FAFAFA] transition-colors">
<td class="px-5 py-3.5">
<p class="text-[14px] font-bold text-[#111827]">{role.name}</p>
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 20px">
<span style="font-size:14px;font-weight:600;color:#111827">{role.name}</span>
</td>
<td class="px-5 py-3.5">
<div class="text-[11px] font-mono leading-[1.6] text-[#9CA3AF]">
{role.key.split('-').map((seg, i, arr) => (
<span class="block">{seg}{i < arr.length - 1 ? '-' : ''}</span>
))}
</div>
<td style="padding:12px 20px">
<span style="font-size:14px;color:#374151">{role.department}</span>
</td>
<td class="px-5 py-3.5 text-[13px] text-[#374151]">{role.department_name || '—'}</td>
<td class="px-5 py-3.5">
<UsersBadge count={role.users_assigned} />
<td style="padding:12px 20px">
<span style="font-size:14px;font-weight:600;color:#111827">{role.usersAssigned}</span>
</td>
<td class="px-5 py-3.5">
<Show when={role.permissions_count === 0} fallback={
<span class="text-[13px] text-[#374151]">{role.permissions_count} Controls</span>
}>
<span class="text-[13px] font-semibold text-[#FF5E13]">All Access</span>
</Show>
<td style="padding:12px 20px">
<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">
{role.permissionsCount} Permissions
</span>
</td>
<td class="px-5 py-3.5">
<StatusBadge active={role.is_active} />
<td style="padding:12px 20px">
<StatusBadge status={role.status} />
</td>
<td class="px-5 py-3.5 text-[13px] text-[#6B7280]">{formatDate(role.created_at)}</td>
<td class="px-5 py-3.5">
<div class="flex items-center gap-3">
<button
type="button"
onClick={() => navigate(`/admin/roles/${role.id}`)}
class="text-[#9CA3AF] hover:text-[#374151] transition-colors"
aria-label="View role"
>
<Eye size={17} />
<td style="padding:12px 20px">
<span style="font-size:13px;color:#6B7280">{formatDate(role.createdDate)}</span>
</td>
<td style="padding:12px 20px;text-align:center">
<button type="button" style="background:none;border:none;cursor:pointer;color:#6B7280;display:inline-flex;align-items:center">
<MoreVertical size={16} />
</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>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
{/* Pagination */}
<Show when={!data.loading && roles().length > 0}>
<div class="flex items-center justify-between border-t border-[#F3F4F6] px-6 py-4">
<p class="text-[13px] text-[#6B7280]">
Showing <span class="font-semibold text-[#111827]">{roles().length}</span> of <span class="font-semibold text-[#111827]">{data()?.total ?? 0}</span> roles
</p>
<div class="flex items-center gap-1.5">
<button
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 style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-top:1px solid #F3F4F6">
<span style="font-size:13px;color:#6B7280">
Showing 1{shown().length} of {shown().length} roles
</span>
<div style="display:flex;align-items:center;gap:4px">
<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>
</div>
</div>
</div>
</Show>
</div>
{/* Bottom stats row */}
<div class="grid grid-cols-2 gap-5">
{/* Distribution card */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
<div class="flex items-center justify-between mb-5">
<p class="text-[15px] font-semibold text-[#111827]">Distribution</p>
<button type="button" class="text-[#9CA3AF] hover:text-[#374151]">
<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>
{/* ── CREATE ROLE TAB ── */}
<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;gap:0;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(
[
{ key: 'general', label: 'General Information' },
{ key: 'access', label: 'Module Access' },
{ key: 'settings', label: 'Role Settings' },
] 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>
</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>
<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 */}
<div class="flex items-center justify-between rounded-2xl bg-[#0D0D2A] p-8 shadow-sm">
<div class="flex-1 pr-8">
<p class="text-[18px] font-bold text-white">Audit Readiness Score</p>
<p class="mt-2 text-[13px] leading-relaxed text-[#8AACC8]">
Your organizational permissions currently align with 94% of compliance standards. Review inactive roles to reach 100%.
{/* ── General Information ── */}
<Show when={formTab() === 'general'}>
<div style="padding:24px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput
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>
<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">
Review Audit Log
<div style="overflow-x:auto">
<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>
</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>
{/* 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>
<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>
</AdminShell>
);

View file

@ -1,173 +1,318 @@
import { A } from '@solidjs/router';
import { createMemo, createResource, For, Show } from 'solid-js';
import { Search } from 'lucide-solid';
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { Clock } from 'lucide-solid';
const API = '/api/gateway';
type ApprovalRow = {
type VerificationStatus = 'PENDING' | 'VERIFIED' | 'FLAGGED' | 'RE_UPLOAD';
type VerificationRow = {
id: string;
requestType?: string;
type?: string;
requestStatus?: string;
status?: string;
requester?: { name?: string; email?: string };
verificationId?: string;
name?: string;
requesterName?: string;
requesterEmail?: string;
requester?: { name?: string; email?: string };
userType?: string;
verificationType?: string;
type?: string;
submittedDate?: string;
createdAt?: 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 {
const res = await fetch(`${API}/api/admin/approvals`);
if (!res.ok) return [];
const res = await fetch(`${API}/api/admin/verifications`);
if (!res.ok) throw new Error();
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 {
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() {
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) => ({
...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 || '',
})));
const PAGE_SIZE = 10;
function statusDoc(status: string) {
if (status === 'APPROVED') return 'Verified';
if (status === 'REJECTED') return 'Flagged';
if (status === 'PENDING') return 'Review';
return 'Pending';
const filtered = createMemo(() => {
const list = rows() ?? [];
const q = search().trim().toLowerCase();
const sf = statusFilter();
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 (
<AdminShell>
<div class="space-y-6 max-w-[1600px]">
{/* Header Configuration */}
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between px-1">
<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>
<div class="flex items-center gap-4">
<h1 class="text-[32px] font-bold leading-tight text-[#0A1128]">Verification Management</h1>
<div class="hidden items-center gap-3 md:flex">
<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">
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Verification Management</h1>
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Review and verify user submissions</p>
</div>
{/* 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
</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
</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
</button>
</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>
)}
</For>
</div>
{/* 6 KPI Cards Grid */}
<section class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
<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]">Total<br/>Pending</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#0A1128]">42</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]">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]">
{/* 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 Cases</span>
<div style="display:flex;gap:8px">
<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">
Export Queue
</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
</button>
</div>
</div>
<div class="flex flex-col gap-4 md:flex-row md:items-center mb-6">
<div class="relative w-full max-w-sm">
<Search size={18} class="absolute left-3.5 top-1/2 -translate-y-1/2 text-[#94A3B8]" />
{/* Filter Bar */}
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap">
<input
type="text"
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>
<div class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white" />
<div class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white" />
<select
value={userTypeFilter()}
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 class="overflow-x-auto rounded-xl border border-[#E2E8F0]">
<table class="w-full min-w-[1000px] border-collapse bg-white text-left">
{/* 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:900px">
<thead>
<tr class="bg-[#0A1128] text-white">
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERIFICATION ID</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">APPLICANT NAME</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">USER TYPE</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERIFICATION TYPE</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">SUBMITTED DATE</th>
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">DOCUMENT STATUS</th>
<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">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">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">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">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">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>
</thead>
<tbody>
<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 when={!rows.loading && normalized().length === 0}>
<tr><td colspan="6" class="text-center py-12 text-[#64748B] text-[14px]">No pending verification cases found.</td></tr>
<Show when={!rows.loading && paged().length === 0}>
<tr>
<td colspan="8" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">No verification cases found.</td>
</tr>
</Show>
<For each={normalized()}>
{(item) => (
<tr class="border-b border-[#E2E8F0] transition-colors hover:bg-[#F8FAFC]">
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">VER2024{item.id.slice(0, 3)}</td>
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">{item.requesterName}</td>
<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'}
<For each={paged()}>
{(row) => (
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 20px;font-size:13px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">{rowVerifId(row)}</span>
</td>
<td class="px-6 py-4">
<A
href={`/admin/verification-status/${item.id}`}
class={`inline-flex items-center rounded-lg px-3 py-1.5 text-[13px] font-bold transition-opacity hover:opacity-80 ${
statusDoc(item.status) === 'Verified' ? 'bg-[#DCFCE7] text-[#16A34A]' :
statusDoc(item.status) === 'Flagged' ? 'bg-[#FEE2E2] text-[#EF4444]' :
'bg-[#F1F5F9] text-[#64748B]'
}`}
<td style="padding:12px 20px;font-size:13px;font-weight:600;color:#111827">{rowName(row)}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{row.userType || '—'}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{row.verificationType || row.type || '—'}</td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{rowDate(row)}</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{row.documents ?? '—'}</td>
<td style="padding:12px 20px">
<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)}
</A>
Actions
</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>
</tr>
)}
@ -175,7 +320,195 @@ export default function VerificationStatusPage() {
</tbody>
</table>
</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>
</AdminShell>
);