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:
parent
fd4f6ceba8
commit
a95c955ad4
7 changed files with 2965 additions and 2900 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,30 +1,53 @@
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
import { updateModuleRecord, deleteModuleRecord } from '~/lib/admin/client';
|
import { ChevronDown, SlidersHorizontal, Download, MoreVertical } from 'lucide-solid';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
|
||||||
|
|
||||||
type DesignationRecord = CrudRecord & {
|
type DesignationRecord = {
|
||||||
code?: string;
|
id: string;
|
||||||
department?: string;
|
name: string;
|
||||||
level?: string;
|
code: string;
|
||||||
|
department: string;
|
||||||
|
level: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
totalEmployees?: number;
|
totalEmployees: number;
|
||||||
createdDate?: string;
|
status: 'ACTIVE' | 'INACTIVE';
|
||||||
|
createdDate: string;
|
||||||
canManageTeam?: boolean;
|
canManageTeam?: boolean;
|
||||||
canApprove?: boolean;
|
canApprove?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FALLBACK_DESIGNATIONS: DesignationRecord[] = [
|
const FALLBACK_DESIGNATIONS: DesignationRecord[] = [
|
||||||
{ id: 'z1', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-15' },
|
{ id: 'z1', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', createdDate: '2024-01-15' },
|
||||||
{ id: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-20' },
|
{ id: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', createdDate: '2024-01-20' },
|
||||||
{ id: 'z3', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-01' },
|
{ id: 'z3', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', createdDate: '2024-02-01' },
|
||||||
{ id: 'z4', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-05' },
|
{ id: 'z4', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', createdDate: '2024-02-10' },
|
||||||
{ id: 'z5', name: 'Product Manager', code: 'PM-005', department: 'Product', level: 'Manager', totalEmployees: 6, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-10' },
|
{ id: 'z5', name: 'Financial Analyst', code: 'FA-005', department: 'Finance', level: 'Analyst', totalEmployees: 6, status: 'ACTIVE', createdDate: '2024-02-15' },
|
||||||
{ id: 'z6', name: 'Finance Analyst', code: 'FA-006', department: 'Finance', level: 'Analyst', totalEmployees: 9, status: 'INACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-15' },
|
{ id: 'z6', name: 'Operations Manager', code: 'OM-006', department: 'Operations', level: 'Manager', totalEmployees: 4, status: 'INACTIVE', createdDate: '2024-03-01' },
|
||||||
|
{ id: 'z7', name: 'Customer Support Lead', code: 'CSL-007', department: 'Customer Support', level: 'Lead', totalEmployees: 9, status: 'ACTIVE', createdDate: '2024-03-05' },
|
||||||
|
{ id: 'z8', name: 'Product Designer', code: 'PD-008', department: 'Product', level: 'Designer', totalEmployees: 7, status: 'ACTIVE', createdDate: '2024-03-10' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level', 'Executive', 'Specialist', 'Analyst'];
|
const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level', 'Executive', 'Specialist', 'Analyst', 'Designer'];
|
||||||
const DEPARTMENTS = ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance', 'Operations', 'Product', 'Customer Success'];
|
const DEPARTMENTS = ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance', 'Operations', 'Product', 'Customer Support'];
|
||||||
|
|
||||||
|
function levelBadge(level: string) {
|
||||||
|
const map: Record<string, { bg: string; color: string; border: string }> = {
|
||||||
|
Senior: { bg: '#EFF6FF', color: '#1D4ED8', border: '#BFDBFE' },
|
||||||
|
Manager: { bg: '#F5F3FF', color: '#7C3AED', border: '#DDD6FE' },
|
||||||
|
Executive: { bg: '#ECFDF5', color: '#059669', border: '#A7F3D0' },
|
||||||
|
Specialist: { bg: '#F0FDFA', color: '#0D9488', border: '#99F6E4' },
|
||||||
|
Analyst: { bg: '#FFF7ED', color: '#EA580C', border: '#FED7AA' },
|
||||||
|
Lead: { bg: '#EFF6FF', color: '#2563EB', border: '#BFDBFE' },
|
||||||
|
Designer: { bg: '#FDF4FF', color: '#A21CAF', border: '#F0ABFC' },
|
||||||
|
Director: { bg: '#FFF1F2', color: '#BE123C', border: '#FECDD3' },
|
||||||
|
};
|
||||||
|
const s = map[level] ?? { bg: '#F3F4F6', color: '#4B5563', border: '#D1D5DB' };
|
||||||
|
return (
|
||||||
|
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${s.border};background:${s.bg};color:${s.color};padding:2px 10px;font-size:12px;font-weight:500`}>
|
||||||
|
{level}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StatusBadge(props: { status: string }) {
|
function StatusBadge(props: { status: string }) {
|
||||||
const active = () => props.status === 'ACTIVE';
|
const active = () => props.status === 'ACTIVE';
|
||||||
|
|
@ -36,6 +59,18 @@ function StatusBadge(props: { status: string }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Toggle(props: { on: boolean; onChange: (v: boolean) => void }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => props.onChange(!props.on)}
|
||||||
|
style={`width:40px;height:22px;border-radius:11px;background:${props.on ? '#FF5E13' : '#E5E7EB'};position:relative;cursor:pointer;border:none;padding:0;flex-shrink:0`}
|
||||||
|
>
|
||||||
|
<span style={`position:absolute;width:18px;height:18px;border-radius:50%;background:white;top:2px;transition:left 0.2s;left:${props.on ? '20px' : '2px'}`} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
|
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
|
||||||
return (
|
return (
|
||||||
<label style="display:block">
|
<label style="display:block">
|
||||||
|
|
@ -71,32 +106,32 @@ function FormSelect(props: { label: string; required?: boolean; value: string; o
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DesignationManagementPage() {
|
export default function DesignationManagementPage() {
|
||||||
const [view, setView] = createSignal<'list' | 'form'>('list');
|
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
|
||||||
const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
|
const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
|
||||||
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
|
|
||||||
const [search, setSearch] = createSignal('');
|
const [search, setSearch] = createSignal('');
|
||||||
const [deptFilter, setDeptFilter] = createSignal('all');
|
|
||||||
const [statusFilter, setStatusFilter] = createSignal('all');
|
|
||||||
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
|
||||||
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
|
||||||
const [rows, setRows] = createSignal<DesignationRecord[]>([]);
|
const [rows, setRows] = createSignal<DesignationRecord[]>([]);
|
||||||
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
||||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
|
||||||
|
|
||||||
|
// form state
|
||||||
const [name, setName] = createSignal('');
|
const [name, setName] = createSignal('');
|
||||||
const [code, setCode] = createSignal('');
|
const [code, setCode] = createSignal('');
|
||||||
const [department, setDepartment] = createSignal('');
|
const [department, setDepartment] = createSignal('');
|
||||||
const [level, setLevel] = createSignal('');
|
const [level, setLevel] = createSignal('');
|
||||||
const [description, setDescription] = createSignal('');
|
const [description, setDescription] = createSignal('');
|
||||||
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
|
const [formStatus, setFormStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
|
||||||
const [canManageTeam, setCanManageTeam] = createSignal(false);
|
const [canManageTeam, setCanManageTeam] = createSignal(false);
|
||||||
const [canApprove, setCanApprove] = createSignal(false);
|
const [canApprove, setCanApprove] = createSignal(false);
|
||||||
const [isSaving, setIsSaving] = createSignal(false);
|
|
||||||
const [error, setError] = createSignal('');
|
// permissions toggles
|
||||||
|
const [permViewEmp, setPermViewEmp] = createSignal(false);
|
||||||
|
const [permCreateEmp, setPermCreateEmp] = createSignal(false);
|
||||||
|
const [permEditEmp, setPermEditEmp] = createSignal(false);
|
||||||
|
const [permDeleteEmp, setPermDeleteEmp] = createSignal(false);
|
||||||
|
const [permAssignRoles, setPermAssignRoles] = createSignal(false);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/gateway/api/admin/designations?page=1&limit=100&q=${encodeURIComponent(search().trim())}`);
|
const res = await fetch(`/api/gateway/api/admin/designations?page=1&limit=100`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const payload = await res.json().catch(() => null);
|
const payload = await res.json().catch(() => null);
|
||||||
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : [];
|
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : [];
|
||||||
|
|
@ -110,7 +145,6 @@ export default function DesignationManagementPage() {
|
||||||
description: String(item.description ?? ''),
|
description: String(item.description ?? ''),
|
||||||
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? 0),
|
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? 0),
|
||||||
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
|
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
|
||||||
updatedAt: String(item.updatedAt ?? item.updated_at ?? ''),
|
|
||||||
createdDate: String(item.createdDate ?? item.created_at ?? ''),
|
createdDate: String(item.createdDate ?? item.created_at ?? ''),
|
||||||
})));
|
})));
|
||||||
return;
|
return;
|
||||||
|
|
@ -123,206 +157,148 @@ export default function DesignationManagementPage() {
|
||||||
onMount(() => void load());
|
onMount(() => void load());
|
||||||
|
|
||||||
const filteredRows = createMemo(() => {
|
const filteredRows = createMemo(() => {
|
||||||
let r = rows();
|
|
||||||
if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase());
|
|
||||||
if (deptFilter() !== 'all') r = r.filter((d) => d.department === deptFilter());
|
|
||||||
const q = search().toLowerCase();
|
const q = search().toLowerCase();
|
||||||
if (q) r = r.filter((d) => d.name.toLowerCase().includes(q) || String(d.code ?? '').toLowerCase().includes(q));
|
if (!q) return rows();
|
||||||
return r;
|
return rows().filter((d) =>
|
||||||
|
d.name.toLowerCase().includes(q) || d.code.toLowerCase().includes(q) || d.department.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setEditingId(null); setName(''); setCode(''); setDepartment('');
|
setName(''); setCode(''); setDepartment(''); setLevel('');
|
||||||
setLevel(''); setDescription(''); setStatus('ACTIVE');
|
setDescription(''); setFormStatus('ACTIVE');
|
||||||
setCanManageTeam(false); setCanApprove(false); setFormTab('general'); setError('');
|
setCanManageTeam(false); setCanApprove(false);
|
||||||
|
setPermViewEmp(false); setPermCreateEmp(false);
|
||||||
|
setPermEditEmp(false); setPermDeleteEmp(false);
|
||||||
|
setPermAssignRoles(false);
|
||||||
|
setFormTab('general');
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCreate = () => { resetForm(); setView('form'); };
|
const handleTabChange = (tab: 'all' | 'create') => {
|
||||||
|
setMainTab(tab);
|
||||||
const openEdit = (row: DesignationRecord) => {
|
if (tab === 'create') resetForm();
|
||||||
setEditingId(row.id);
|
setOpenMenuId(null);
|
||||||
setName(row.name || ''); setCode(String(row.code || ''));
|
|
||||||
setDepartment(String(row.department || '')); setLevel(String(row.level || ''));
|
|
||||||
setDescription(String(row.description || ''));
|
|
||||||
setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE');
|
|
||||||
setCanManageTeam(Boolean(row.canManageTeam)); setCanApprove(Boolean(row.canApprove));
|
|
||||||
setFormTab('general'); setView('form'); setOpenMenuId(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async () => {
|
const formatDate = (d: string) => {
|
||||||
if (!name().trim()) { setError('Designation name is required.'); setFormTab('general'); return; }
|
if (!d) return '—';
|
||||||
setIsSaving(true); setError('');
|
|
||||||
try {
|
try {
|
||||||
const payload: Partial<DesignationRecord> = {
|
return new Date(d).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||||
name: name().trim(), code: code().trim() || undefined,
|
} catch { return d; }
|
||||||
department: department().trim(), level: level().trim(),
|
|
||||||
description: description().trim(), status: status(),
|
|
||||||
canManageTeam: canManageTeam(), canApprove: canApprove(),
|
|
||||||
};
|
|
||||||
if (editingId()) {
|
|
||||||
await updateModuleRecord<DesignationRecord>('designation', editingId()!, payload);
|
|
||||||
} else {
|
|
||||||
const res = await fetch('/api/gateway/api/admin/designations', {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
||||||
}
|
|
||||||
setView('list'); resetForm(); await load();
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err?.message || 'Failed to save designation.');
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (v?: string) => {
|
|
||||||
const s = v || '';
|
|
||||||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
|
|
||||||
return s.slice(0, 10) || '—';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div style="width:100%;padding-bottom:32px">
|
<div style="padding:24px">
|
||||||
|
|
||||||
{/* Page header */}
|
{/* Page header */}
|
||||||
<div style="margin-bottom:24px">
|
<div>
|
||||||
<h1 style="font-size:28px;font-weight:700;color:#111827;line-height:1.2">Designation Management</h1>
|
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Designation Management</h1>
|
||||||
<p style="margin-top:4px;font-size:14px;color:#6B7280">Manage all job designations and position levels</p>
|
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage all designations and job positions</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── LIST VIEW ── */}
|
{/* Main tabs */}
|
||||||
<Show when={view() === 'list'}>
|
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
|
||||||
<div>
|
|
||||||
{/* Tabs */}
|
|
||||||
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
||||||
{([
|
|
||||||
{ key: 'all', label: 'All Designations', action: () => { setListTab('all'); setStatusFilter('all'); void load(); } },
|
|
||||||
{ key: 'create', label: 'Create Designation', action: () => { setListTab('create'); openCreate(); } },
|
|
||||||
{ key: 'view', label: 'View Designation', action: () => setListTab('view') },
|
|
||||||
] as const).map((tab) => (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={tab.action}
|
onClick={() => handleTabChange('all')}
|
||||||
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${mainTab() === 'all' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px'}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
All Designations
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleTabChange('create')}
|
||||||
|
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${mainTab() === 'create' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px'}`}
|
||||||
|
>
|
||||||
|
Create Designation
|
||||||
</button>
|
</button>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table card */}
|
{/* All Designations view */}
|
||||||
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
<Show when={mainTab() === 'all'}>
|
||||||
|
<div style="margin-top:20px;margin-left:-24px;margin-right:-24px;border-radius:0;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white">
|
||||||
{/* Filter bar */}
|
{/* Filter bar */}
|
||||||
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
<input
|
<input
|
||||||
value={search()}
|
type="text"
|
||||||
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
|
|
||||||
placeholder="Search designations..."
|
placeholder="Search designations..."
|
||||||
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
value={search()}
|
||||||
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
style="height:34px;flex:1;max-width:240px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827;background:white"
|
||||||
/>
|
/>
|
||||||
<select
|
|
||||||
value={deptFilter()}
|
|
||||||
onChange={(e) => setDeptFilter(e.currentTarget.value)}
|
|
||||||
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151;outline:none"
|
|
||||||
>
|
|
||||||
<option value="all">All Departments</option>
|
|
||||||
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
|
|
||||||
</select>
|
|
||||||
<div style="position:relative">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
style="display:inline-flex;align-items:center;height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;gap:6px;cursor:pointer"
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
||||||
>
|
>
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
<ChevronDown size={14} />
|
||||||
|
Sort
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style="display:inline-flex;align-items:center;height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;gap:6px;cursor:pointer"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal size={14} />
|
||||||
Filters
|
Filters
|
||||||
</button>
|
</button>
|
||||||
<Show when={filterMenuOpen()}>
|
<button
|
||||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
type="button"
|
||||||
{(['all', 'ACTIVE', 'INACTIVE'] as const).map((s) => (
|
style="display:inline-flex;align-items:center;height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer;gap:6px"
|
||||||
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
>
|
||||||
{s === 'all' ? 'All Status' : s === 'ACTIVE' ? 'Active' : 'Inactive'}
|
<Download size={14} />
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div style="overflow-x:auto">
|
<div style="overflow-x:auto">
|
||||||
<table style="min-width:100%">
|
<table style="width:100%;border-collapse:collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background:#0D0D2A;text-align:left">
|
<tr style="background:#0D0D2A">
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Designation Name</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Designation Name</th>
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Code</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Designation Code</th>
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Department</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Department</th>
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Level</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Level</th>
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Employees</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Total Employees</th>
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Status</th>
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Created Date</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Created Date</th>
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:left">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<Show
|
|
||||||
when={filteredRows().length > 0}
|
|
||||||
fallback={
|
|
||||||
<tr>
|
|
||||||
<td colspan="8" style="padding:64px 24px;text-align:center">
|
|
||||||
<p style="font-size:15px;font-weight:600;color:#111827">No designations found</p>
|
|
||||||
<p style="margin-top:4px;font-size:13px;color:#6B7280">Create your first designation to get started.</p>
|
|
||||||
<button type="button" onClick={openCreate} style="margin-top:16px;display:inline-flex;align-items:center;gap:8px;border-radius:12px;background:#0D0D2A;padding:8px 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">
|
|
||||||
Create Designation
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={filteredRows()}>
|
<For each={filteredRows()}>
|
||||||
{(row) => (
|
{(row) => (
|
||||||
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
<tr style="border-bottom:1px solid #F3F4F6">
|
||||||
<td style="padding:12px 20px">
|
<td style="padding:12px 20px;font-size:14px;color:#111827;font-weight:600">{row.name}</td>
|
||||||
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
|
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.code}</td>
|
||||||
</td>
|
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.department}</td>
|
||||||
<td style="padding:12px 20px">
|
<td style="padding:12px 20px;font-size:14px;color:#111827">{levelBadge(row.level)}</td>
|
||||||
<span style="font-size:12px;font-family:monospace;color:#6B7280">{String(row.code || '—')}</span>
|
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.totalEmployees}</td>
|
||||||
</td>
|
<td style="padding:12px 20px;font-size:14px;color:#111827"><StatusBadge status={row.status} /></td>
|
||||||
<td style="padding:12px 20px;font-size:13px;color:#374151">{String(row.department || '—')}</td>
|
<td style="padding:12px 20px;font-size:14px;color:#111827">{formatDate(row.createdDate)}</td>
|
||||||
<td style="padding:12px 20px">
|
<td style="padding:12px 20px;font-size:14px;color:#111827;position:relative">
|
||||||
<span style="display:inline-flex;border-radius:9999px;background:#EFF6FF;color:#2563EB;padding:2px 10px;font-size:12px;font-weight:500">{String(row.level || '—')}</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">{Number(row.totalEmployees || 0)}</td>
|
|
||||||
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
|
|
||||||
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
|
|
||||||
<td style="padding:12px 20px;position:relative">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)}
|
onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)}
|
||||||
style="display:inline-flex;width:32px;height:32px;align-items:center;justify-content:center;border-radius:8px;border:none;background:none;color:#9CA3AF;cursor:pointer"
|
style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;border:1px solid #E5E7EB;background:white;cursor:pointer;color:#374151"
|
||||||
aria-label="More actions"
|
|
||||||
>
|
>
|
||||||
<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
<MoreVertical size={16} />
|
||||||
</button>
|
</button>
|
||||||
<Show when={openMenuId() === row.id}>
|
<Show when={openMenuId() === row.id}>
|
||||||
<div style="position:absolute;right:20px;top:44px;z-index:20;width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
<div style="position:absolute;right:20px;top:44px;z-index:50;background:white;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);min-width:140px;overflow:hidden">
|
||||||
<button type="button" onClick={() => openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer">
|
<button
|
||||||
<svg style="width:16px;height:16px;color:#FF5E13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z"/></svg>
|
type="button"
|
||||||
Edit Designation
|
onClick={() => { setOpenMenuId(null); }}
|
||||||
|
style="display:block;width:100%;text-align:left;padding:9px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer"
|
||||||
|
class="hover:bg-[#FAFAFA]"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={async () => { await updateModuleRecord<DesignationRecord>('designation', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer">
|
<button
|
||||||
<svg style="width:16px;height:16px;color:#FF5E13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
|
type="button"
|
||||||
{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}
|
onClick={() => { setOpenMenuId(null); }}
|
||||||
</button>
|
style="display:block;width:100%;text-align:left;padding:9px 14px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer"
|
||||||
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
|
class="hover:bg-[#FFF5F5]"
|
||||||
<button type="button" onClick={async () => { if (!window.confirm(`Delete designation "${row.name}"?`)) return; await deleteModuleRecord('designation', row.id); setOpenMenuId(null); await load(); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#DC2626;background:none;border:none;cursor:pointer">
|
>
|
||||||
<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6"/></svg>
|
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -331,185 +307,214 @@ export default function DesignationManagementPage() {
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<Show when={filteredRows().length > 0}>
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-top:1px solid #F3F4F6">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
<span style="font-size:13px;color:#6B7280">
|
||||||
<p style="font-size:13px;color:#6B7280">
|
Showing 1-{filteredRows().length} of {filteredRows().length} designations
|
||||||
Showing <strong style="font-weight:600;color:#111827">1–{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> designations
|
</span>
|
||||||
</p>
|
|
||||||
<div style="display:flex;align-items:center;gap:4px">
|
<div style="display:flex;align-items:center;gap:4px">
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">‹</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">›</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* ── FORM VIEW ── */}
|
|
||||||
<Show when={view() === 'form'}>
|
|
||||||
{/* Top tabs */}
|
|
||||||
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
||||||
<button type="button" onClick={() => { setView('list'); resetForm(); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
|
|
||||||
All Designations
|
|
||||||
</button>
|
|
||||||
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">
|
|
||||||
{editingId() ? 'Edit Designation' : 'Create Designation'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
||||||
{/* Sub-tabs */}
|
|
||||||
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
|
|
||||||
{(['general', 'settings', 'permissions'] as const).map((tab, i) => {
|
|
||||||
const labels = ['General Information', 'Designation Settings', 'Permissions'];
|
|
||||||
const active = () => formTab() === tab;
|
|
||||||
return (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFormTab(tab)}
|
style="display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;border:1px solid #E5E7EB;background:white;color:#374151;cursor:pointer;font-size:13px"
|
||||||
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}
|
|
||||||
>
|
>
|
||||||
{labels[i]}
|
‹
|
||||||
<Show when={active()}>
|
</button>
|
||||||
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
|
<button
|
||||||
</Show>
|
type="button"
|
||||||
|
style="display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;background:#FF5E13;color:white;border:none;cursor:pointer;font-size:13px;font-weight:500"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style="display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;border:1px solid #E5E7EB;background:white;color:#374151;cursor:pointer;font-size:13px"
|
||||||
|
>
|
||||||
|
›
|
||||||
</button>
|
</button>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div style="padding:24px">
|
|
||||||
<Show when={error()}>
|
|
||||||
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
|
||||||
{error()}
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* General Information */}
|
{/* Create Designation view */}
|
||||||
<Show when={formTab() === 'general'}>
|
<Show when={mainTab() === 'create'}>
|
||||||
<div style="display:flex;flex-direction:column;gap:20px">
|
<div style="margin-top:20px;border-radius:16px;border:1px solid #E5E7EB;background:white;overflow:hidden">
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
{/* Form sub-tabs */}
|
||||||
<FormInput label="Designation Name" required value={name()} onInput={setName} placeholder="e.g. Senior Software Engineer" />
|
<div style="display:flex;align-items:center;gap:0;border-bottom:1px solid #E5E7EB;padding:0 24px">
|
||||||
<FormInput label="Designation Code" required value={code()} onInput={setCode} placeholder="e.g. SSE-001" />
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormTab('general')}
|
||||||
|
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${formTab() === 'general' ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px;font-weight:500'}`}
|
||||||
|
>
|
||||||
|
General Information
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormTab('settings')}
|
||||||
|
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${formTab() === 'settings' ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px;font-weight:500'}`}
|
||||||
|
>
|
||||||
|
Designation Settings
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormTab('permissions')}
|
||||||
|
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;border-top:none;border-left:none;border-right:none;cursor:pointer;${formTab() === 'permissions' ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;border-bottom:2px solid transparent;margin-bottom:-1px;font-weight:500'}`}
|
||||||
|
>
|
||||||
|
Permissions
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* General Information tab */}
|
||||||
|
<Show when={formTab() === 'general'}>
|
||||||
|
<div style="padding:24px">
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||||
<FormSelect label="Department" required value={department()} onChange={setDepartment}>
|
<FormInput
|
||||||
|
label="Designation Name"
|
||||||
|
required
|
||||||
|
value={name()}
|
||||||
|
onInput={setName}
|
||||||
|
placeholder="e.g. Senior Software Engineer"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="Designation Code"
|
||||||
|
required
|
||||||
|
value={code()}
|
||||||
|
onInput={setCode}
|
||||||
|
placeholder="e.g. SSE-001"
|
||||||
|
/>
|
||||||
|
<FormSelect
|
||||||
|
label="Department"
|
||||||
|
required
|
||||||
|
value={department()}
|
||||||
|
onChange={setDepartment}
|
||||||
|
>
|
||||||
<option value="">Select department</option>
|
<option value="">Select department</option>
|
||||||
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
|
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
<FormSelect label="Designation Level" required value={level()} onChange={setLevel}>
|
<FormSelect
|
||||||
|
label="Designation Level"
|
||||||
|
required
|
||||||
|
value={level()}
|
||||||
|
onChange={setLevel}
|
||||||
|
>
|
||||||
<option value="">Select level</option>
|
<option value="">Select level</option>
|
||||||
<For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For>
|
<For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For>
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
</div>
|
<div style="grid-column:1 / -1">
|
||||||
<label style="display:block">
|
<label style="display:block">
|
||||||
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
|
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
|
||||||
<textarea
|
<textarea
|
||||||
value={description()}
|
value={description()}
|
||||||
onInput={(e) => setDescription(e.currentTarget.value)}
|
onInput={(e) => setDescription(e.currentTarget.value)}
|
||||||
placeholder="Brief description of this designation's responsibilities..."
|
placeholder="Describe the designation role and responsibilities..."
|
||||||
rows="3"
|
rows={4}
|
||||||
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
|
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box;resize:vertical;font-family:inherit"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Designation Settings */}
|
{/* Designation Settings tab */}
|
||||||
<Show when={formTab() === 'settings'}>
|
<Show when={formTab() === 'settings'}>
|
||||||
<div style="display:flex;flex-direction:column;gap:32px">
|
<div style="padding:24px;display:flex;flex-direction:column;gap:24px">
|
||||||
|
{/* Status */}
|
||||||
<div>
|
<div>
|
||||||
<p style="font-size:14px;font-weight:600;color:#111827">Designation Status</p>
|
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 10px 0">Designation Status</p>
|
||||||
<p style="margin-top:2px;font-size:13px;color:#6B7280">Set whether this designation is currently active</p>
|
<div style="display:flex;gap:10px">
|
||||||
<div style="margin-top:12px;display:flex;gap:10px">
|
|
||||||
{(['ACTIVE', 'INACTIVE'] as const).map((s) => (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStatus(s)}
|
onClick={() => setFormStatus('ACTIVE')}
|
||||||
style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? '#FF5E13' : '#E5E7EB'};background:${status() === s ? '#FFF3EE' : 'white'};color:${status() === s ? '#FF5E13' : '#6B7280'}`}
|
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${formStatus() === 'ACTIVE' ? 'border:1px solid #FF5E13;background:#FFF3EE;color:#FF5E13' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
|
||||||
>
|
>
|
||||||
{s === 'ACTIVE' ? 'Active' : 'Inactive'}
|
Active
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormStatus('INACTIVE')}
|
||||||
|
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${formStatus() === 'INACTIVE' ? 'border:1px solid #FF5E13;background:#FFF3EE;color:#FF5E13' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
|
||||||
|
>
|
||||||
|
Inactive
|
||||||
</button>
|
</button>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
|
{/* Toggle: Manage Team */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
|
||||||
<div>
|
<div>
|
||||||
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage Team</p>
|
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Designation to Manage Team Members</p>
|
||||||
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can manage team members</p>
|
<p style="font-size:12px;color:#6B7280;margin:0">Enable this to allow team member management capabilities</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Toggle on={canManageTeam()} onChange={setCanManageTeam} />
|
||||||
type="button"
|
|
||||||
onClick={() => setCanManageTeam((v) => !v)}
|
|
||||||
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canManageTeam() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
|
|
||||||
>
|
|
||||||
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canManageTeam() ? '22px' : '2px'}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
|
{/* Toggle: Approval Permissions */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
|
||||||
<div>
|
<div>
|
||||||
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests</p>
|
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Approval Permissions</p>
|
||||||
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can approve employee requests</p>
|
<p style="font-size:12px;color:#6B7280;margin:0">Enable this to allow approving requests and actions</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Toggle on={canApprove()} onChange={setCanApprove} />
|
||||||
type="button"
|
|
||||||
onClick={() => setCanApprove((v) => !v)}
|
|
||||||
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canApprove() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
|
|
||||||
>
|
|
||||||
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canApprove() ? '22px' : '2px'}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Permissions */}
|
{/* Permissions tab */}
|
||||||
<Show when={formTab() === 'permissions'}>
|
<Show when={formTab() === 'permissions'}>
|
||||||
<div style="display:flex;flex-direction:column;gap:16px">
|
<div style="padding:24px">
|
||||||
<p style="font-size:13px;color:#6B7280">Select the permissions available to employees with this designation.</p>
|
{/* Employee Management */}
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
<p style="font-size:14px;font-weight:600;color:#111827;margin:0 0 12px 0">Employee Management</p>
|
||||||
{['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees', 'Assign Roles', 'Approve Requests', 'Manage Team Members'].map((item) => (
|
<div style="display:flex;flex-direction:column;gap:0">
|
||||||
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer">
|
{[
|
||||||
<input type="checkbox" style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer" />
|
{ label: 'View Employees', sig: permViewEmp, set: setPermViewEmp },
|
||||||
<span style="font-size:13px;font-weight:500;color:#374151">{item}</span>
|
{ label: 'Create Employees', sig: permCreateEmp, set: setPermCreateEmp },
|
||||||
</label>
|
{ label: 'Edit Employees', sig: permEditEmp, set: setPermEditEmp },
|
||||||
|
{ label: 'Delete Employees', sig: permDeleteEmp, set: setPermDeleteEmp },
|
||||||
|
].map((item) => (
|
||||||
|
<div style="border-radius:8px;border:1px solid #E5E7EB;padding:12px 16px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<span style="font-size:14px;color:#374151">{item.label}</span>
|
||||||
|
<Toggle on={item.sig()} onChange={item.set} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Permissions */}
|
||||||
|
<p style="font-size:14px;font-weight:600;color:#111827;margin:20px 0 12px 0">Additional Permissions</p>
|
||||||
|
<div style="border-radius:8px;border:1px solid #E5E7EB;padding:12px 16px;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<span style="font-size:14px;color:#374151">Assign Roles</span>
|
||||||
|
<Toggle on={permAssignRoles()} onChange={setPermAssignRoles} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form actions */}
|
{/* Form footer */}
|
||||||
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
|
<div style="display:flex;align-items:center;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #E5E7EB">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setView('list'); resetForm(); }}
|
onClick={() => { resetForm(); setMainTab('all'); }}
|
||||||
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
|
style="height:38px;padding:0 20px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:14px;cursor:pointer"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void save()}
|
onClick={() => {
|
||||||
disabled={isSaving()}
|
if (!name().trim()) { setFormTab('general'); return; }
|
||||||
style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer"
|
setMainTab('all');
|
||||||
|
resetForm();
|
||||||
|
}}
|
||||||
|
style="height:38px;padding:0 20px;border-radius:8px;background:#0D0D2A;color:white;font-size:14px;font-weight:500;border:none;cursor:pointer"
|
||||||
>
|
>
|
||||||
{isSaving() ? 'Saving...' : editingId() ? 'Update Designation' : 'Create Designation'}
|
Create Designation
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,41 @@
|
||||||
import { createResource, createSignal, For, Show } from 'solid-js';
|
import { createResource, createSignal, For, Show } from 'solid-js';
|
||||||
import { Eye, SquarePen, Search, Trash2 } from 'lucide-solid';
|
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
import { Upload } from 'lucide-solid';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '/api/gateway';
|
||||||
|
|
||||||
type Employee = {
|
type Employee = {
|
||||||
id: string;
|
id: string;
|
||||||
|
empId?: string;
|
||||||
|
employeeId?: string;
|
||||||
full_name?: string;
|
full_name?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
email: string;
|
email: string;
|
||||||
role?: { id: string; name: string } | string;
|
phone?: string;
|
||||||
role_name?: string;
|
|
||||||
department?: { id: string; name: string } | string;
|
department?: { id: string; name: string } | string;
|
||||||
department_name?: string;
|
department_name?: string;
|
||||||
designation?: { id: string; name: string } | string;
|
designation?: { id: string; name: string } | string;
|
||||||
designation_name?: string;
|
designation_name?: string;
|
||||||
|
role?: { id: string; name: string } | string;
|
||||||
|
role_name?: string;
|
||||||
|
joiningDate?: string;
|
||||||
|
joining_date?: string;
|
||||||
|
created_at?: string;
|
||||||
|
employmentType?: string;
|
||||||
|
employment_type?: string;
|
||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
status?: string;
|
status?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FALLBACK: Employee[] = [
|
const FALLBACK_EMPLOYEES: Employee[] = [
|
||||||
{
|
{ id: 'e1', empId: 'EMP001', name: 'John Smith', email: 'john.smith@nxtgauge.com', phone: '+1 234-567-8901', department: 'Engineering', designation: 'Senior Software Engineer', role: 'Engineering Lead', joiningDate: '2024-01-15', employmentType: 'Full Time', status: 'ACTIVE' },
|
||||||
id: 'EMP001',
|
{ id: 'e2', empId: 'EMP002', name: 'Sarah Johnson', email: 'sarah.j@nxtgauge.com', phone: '+1 234-567-8902', department: 'Marketing', designation: 'Marketing Manager', role: 'Marketing Manager', joiningDate: '2024-01-20', employmentType: 'Full Time', status: 'ACTIVE' },
|
||||||
full_name: 'John Smith',
|
{ id: 'e3', empId: 'EMP003', name: 'Mike Davis', email: 'mike.d@nxtgauge.com', phone: '+1 234-567-8903', department: 'Sales', designation: 'Sales Executive', role: 'Sales Director', joiningDate: '2024-02-01', employmentType: 'Full Time', status: 'ACTIVE' },
|
||||||
email: 'john.smith@nxtgauge.com',
|
{ id: 'e4', empId: 'EMP004', name: 'Emma Wilson', email: 'emma.w@nxtgauge.com', phone: '+1 234-567-8904', department: 'Human Resources', designation: 'HR Specialist', role: 'HR Admin', joiningDate: '2024-02-10', employmentType: 'Full Time', status: 'ACTIVE' },
|
||||||
department: { id: 'd1', name: 'Engineering' },
|
{ id: 'e5', empId: 'EMP005', name: 'David Wilson', email: 'david.w@nxtgauge.com', phone: '+1 234-567-8905', department: 'Finance', designation: 'Financial Analyst', role: 'Finance Controller', joiningDate: '2024-02-15', employmentType: 'Full Time', status: 'ACTIVE' },
|
||||||
designation: { id: 'dg1', name: 'Senior Software Engineer' },
|
{ id: 'e6', empId: 'EMP006', name: 'Lisa Anderson', email: 'lisa.a@nxtgauge.com', phone: '+1 234-567-8906', department: 'Operations', designation: 'Operations Manager', role: 'Operations Head', joiningDate: '2024-03-01', employmentType: 'Part Time', status: 'ACTIVE' },
|
||||||
role: { id: 'r1', name: 'Engineering Lead' },
|
{ id: 'e7', empId: 'EMP007', name: 'James Taylor', email: 'james.t@nxtgauge.com', phone: '+1 234-567-8907', department: 'Customer Support', designation: 'Support Lead', role: 'Support Lead', joiningDate: '2024-03-10', employmentType: 'Full Time', status: 'ACTIVE' },
|
||||||
is_active: true,
|
{ id: 'e8', empId: 'EMP008', name: 'Jennifer Martinez', email: 'jennifer.m@nxtgauge.com', phone: '+1 234-567-8908', department: 'Product', designation: 'Product Designer', role: 'Product Owner', joiningDate: '2024-03-15', employmentType: 'Full Time', status: 'INACTIVE' },
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'EMP002',
|
|
||||||
full_name: 'Sarah Johnson',
|
|
||||||
email: 'sarah.j@nxtgauge.com',
|
|
||||||
department: { id: 'd2', name: 'Marketing' },
|
|
||||||
designation: { id: 'dg2', name: 'Marketing Manager' },
|
|
||||||
role: { id: 'r2', name: 'Marketing Manager' },
|
|
||||||
is_active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'EMP003',
|
|
||||||
full_name: 'Michael Brown',
|
|
||||||
email: 'michael.b@nxtgauge.com',
|
|
||||||
department: { id: 'd3', name: 'Sales' },
|
|
||||||
designation: { id: 'dg3', name: 'Sales Executive' },
|
|
||||||
role: { id: 'r3', name: 'Sales Director' },
|
|
||||||
status: 'PROBATION',
|
|
||||||
is_active: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'EMP004',
|
|
||||||
full_name: 'Emily Davis',
|
|
||||||
email: 'emily.d@nxtgauge.com',
|
|
||||||
department: { id: 'd4', name: 'Human Resources' },
|
|
||||||
designation: { id: 'dg4', name: 'HR Specialist' },
|
|
||||||
role: { id: 'r4', name: 'HR Admin' },
|
|
||||||
is_active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'EMP005',
|
|
||||||
full_name: 'David Wilson',
|
|
||||||
email: 'david.w@nxtgauge.com',
|
|
||||||
department: { id: 'd5', name: 'Finance' },
|
|
||||||
designation: { id: 'dg5', name: 'Financial Analyst' },
|
|
||||||
role: { id: 'r5', name: 'Finance Controller' },
|
|
||||||
is_active: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
async function fetchEmployees(): Promise<Employee[]> {
|
async function fetchEmployees(): Promise<Employee[]> {
|
||||||
|
|
@ -74,187 +44,262 @@ async function fetchEmployees(): Promise<Employee[]> {
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const list = Array.isArray(data) ? data : (data.employees ?? data.users ?? []);
|
const list = Array.isArray(data) ? data : (data.employees ?? data.users ?? []);
|
||||||
return list.length > 0 ? list : FALLBACK;
|
return list.length > 0 ? list : FALLBACK_EMPLOYEES;
|
||||||
} catch {
|
} catch {
|
||||||
return FALLBACK;
|
return FALLBACK_EMPLOYEES;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function empName(e: Employee) {
|
function getEmpId(e: Employee) { return e.empId || e.employeeId || e.id || '—'; }
|
||||||
return e.full_name || e.name || '—';
|
function getEmpName(e: Employee) { return e.full_name || e.name || '—'; }
|
||||||
}
|
function getEmpDept(e: Employee) {
|
||||||
function empRole(e: Employee) {
|
|
||||||
if (!e.role) return e.role_name ?? '—';
|
|
||||||
if (typeof e.role === 'string') return e.role;
|
|
||||||
return e.role.name ?? '—';
|
|
||||||
}
|
|
||||||
function empDept(e: Employee) {
|
|
||||||
if (!e.department) return e.department_name ?? '—';
|
if (!e.department) return e.department_name ?? '—';
|
||||||
if (typeof e.department === 'string') return e.department;
|
if (typeof e.department === 'string') return e.department;
|
||||||
return e.department.name ?? '—';
|
return e.department.name ?? '—';
|
||||||
}
|
}
|
||||||
function empDesig(e: Employee) {
|
function getEmpDesignation(e: Employee) {
|
||||||
if (!e.designation) return e.designation_name ?? '—';
|
if (!e.designation) return e.designation_name ?? '—';
|
||||||
if (typeof e.designation === 'string') return e.designation;
|
if (typeof e.designation === 'string') return e.designation;
|
||||||
return e.designation.name ?? '—';
|
return e.designation.name ?? '—';
|
||||||
}
|
}
|
||||||
function empStatus(e: Employee) {
|
function getEmpRole(e: Employee) {
|
||||||
|
if (!e.role) return e.role_name ?? '—';
|
||||||
|
if (typeof e.role === 'string') return e.role;
|
||||||
|
return e.role.name ?? '—';
|
||||||
|
}
|
||||||
|
function getEmpJoiningDate(e: Employee) {
|
||||||
|
return e.joiningDate || e.joining_date || e.created_at?.slice(0, 10) || '—';
|
||||||
|
}
|
||||||
|
function getEmpEmploymentType(e: Employee) { return e.employmentType || e.employment_type || '—'; }
|
||||||
|
function getEmpStatus(e: Employee): 'ACTIVE' | 'INACTIVE' {
|
||||||
const raw = String(e.status || '').toUpperCase();
|
const raw = String(e.status || '').toUpperCase();
|
||||||
if (raw === 'PROBATION') return 'Probation';
|
if (e.is_active === false || raw === 'INACTIVE') return 'INACTIVE';
|
||||||
if (e.is_active === false || raw === 'INACTIVE') return 'Inactive';
|
return 'ACTIVE';
|
||||||
return 'Active';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusBadge(props: { status: 'Active' | 'Inactive' | 'Probation' }) {
|
function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) {
|
||||||
const classes = () => {
|
const active = () => props.status === 'ACTIVE';
|
||||||
if (props.status === 'Active') return 'border-[#B7E4C7] bg-[#DEF7E8] text-[#0B8A4A]';
|
|
||||||
if (props.status === 'Probation') return 'border-[#F6D78F] bg-[#FFF3D6] text-[#B7791F]';
|
|
||||||
return 'border-[#D1D5DB] bg-[#F3F4F6] text-[#6B7280]';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span class={`inline-flex min-w-[68px] items-center justify-center border px-2 py-1 text-[11px] font-semibold ${classes()}`}>
|
<span style={active()
|
||||||
{props.status}
|
? 'display:inline-flex;align-items:center;justify-content:center;min-width:68px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;border:1px solid #FFD8C2;background:#FFF1EB;color:#FF5E13'
|
||||||
|
: 'display:inline-flex;align-items:center;justify-content:center;min-width:68px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;border:1px solid #D1D5DB;background:#F3F4F6;color:#6B7280'}>
|
||||||
|
{active() ? 'Active' : 'Inactive'}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Toggle(props: { on: boolean; onChange: (val: boolean) => void }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => props.onChange(!props.on)}
|
||||||
|
style={`width:40px;height:22px;border-radius:11px;background:${props.on ? '#FF5E13' : '#E5E7EB'};position:relative;cursor:pointer;flex-shrink:0;transition:background 0.2s`}
|
||||||
|
>
|
||||||
|
<div style={`position:absolute;width:18px;height:18px;border-radius:50%;background:white;top:2px;transition:left 0.2s;left:${props.on ? '20px' : '2px'}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const INPUT_STYLE = 'width:100%;height:36px;padding:0 12px;font-size:13px;border:1px solid #E5E7EB;border-radius:8px;outline:none;color:#374151;background:white;box-sizing:border-box';
|
||||||
|
const LABEL_STYLE = 'display:block;font-size:13px;font-weight:500;color:#374151;margin-bottom:6px';
|
||||||
|
|
||||||
|
const DEPARTMENTS = ['Engineering', 'Marketing', 'Human Resources', 'Finance', 'Sales', 'Operations', 'Product', 'Customer Support'];
|
||||||
|
const DESIGNATIONS = ['Senior Software Engineer', 'Marketing Manager', 'Sales Executive', 'HR Specialist', 'Financial Analyst', 'Operations Manager', 'Support Lead', 'Product Designer'];
|
||||||
|
const EMPLOYMENT_TYPES = ['Full Time', 'Part Time', 'Contract', 'Internship'];
|
||||||
|
const STATUSES = ['ACTIVE', 'INACTIVE'];
|
||||||
|
const GENDERS = ['Male', 'Female', 'Other', 'Prefer not to say'];
|
||||||
|
const INTERNAL_ROLES = ['Engineering Lead', 'Marketing Manager', 'Sales Director', 'HR Admin', 'Finance Controller', 'Operations Head', 'Support Lead', 'Product Owner'];
|
||||||
|
const SHIFT_TYPES = ['Day Shift', 'Night Shift', 'Flexible'];
|
||||||
|
const EMPLOYEE_CATEGORIES = ['Permanent', 'Temporary', 'Probation'];
|
||||||
|
|
||||||
export default function EmployeesIndexPage() {
|
export default function EmployeesIndexPage() {
|
||||||
const [employees] = createResource(fetchEmployees);
|
const [employees] = createResource(fetchEmployees);
|
||||||
|
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
|
||||||
|
const [formTab, setFormTab] = createSignal<'basic' | 'work' | 'access' | 'docs'>('basic');
|
||||||
const [search, setSearch] = createSignal('');
|
const [search, setSearch] = createSignal('');
|
||||||
const [activeTab, setActiveTab] = createSignal<'list' | 'create'>('list');
|
const [deptFilter, setDeptFilter] = createSignal('');
|
||||||
|
const [designationFilter, setDesignationFilter] = createSignal('');
|
||||||
|
const [empTypeFilter, setEmpTypeFilter] = createSignal('');
|
||||||
|
const [statusFilter, setStatusFilter] = createSignal('');
|
||||||
|
const [page, setPage] = createSignal(1);
|
||||||
|
const [openActionId, setOpenActionId] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
// Access & Permissions toggles
|
||||||
|
const [allowAdminLogin, setAllowAdminLogin] = createSignal(false);
|
||||||
|
const [requirePasswordReset, setRequirePasswordReset] = createSignal(false);
|
||||||
|
const [enable2FA, setEnable2FA] = createSignal(false);
|
||||||
|
const [accountActive, setAccountActive] = createSignal(true);
|
||||||
|
const [allowMobileApp, setAllowMobileApp] = createSignal(false);
|
||||||
|
const [allowReports, setAllowReports] = createSignal(false);
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
const filtered = () => {
|
const filtered = () => {
|
||||||
const list = employees() ?? [];
|
const list = employees() ?? [];
|
||||||
const q = search().trim().toLowerCase();
|
const q = search().trim().toLowerCase();
|
||||||
if (!q) return list;
|
return list.filter((e) => {
|
||||||
|
const matchSearch = !q || [getEmpId(e), getEmpName(e), e.email, getEmpDept(e), getEmpDesignation(e)].join(' ').toLowerCase().includes(q);
|
||||||
return list.filter((e) =>
|
const matchDept = !deptFilter() || getEmpDept(e) === deptFilter();
|
||||||
[e.id, empName(e), e.email, empDept(e), empDesig(e), empRole(e)]
|
const matchDesignation = !designationFilter() || getEmpDesignation(e) === designationFilter();
|
||||||
.join(' ')
|
const matchEmpType = !empTypeFilter() || getEmpEmploymentType(e) === empTypeFilter();
|
||||||
.toLowerCase()
|
const matchStatus = !statusFilter() || getEmpStatus(e) === statusFilter();
|
||||||
.includes(q),
|
return matchSearch && matchDept && matchDesignation && matchEmpType && matchStatus;
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const totalPages = () => Math.max(1, Math.ceil(filtered().length / PAGE_SIZE));
|
||||||
|
const paged = () => filtered().slice((page() - 1) * PAGE_SIZE, page() * PAGE_SIZE);
|
||||||
|
|
||||||
|
function mainTabStyle(active: boolean) {
|
||||||
|
return active
|
||||||
|
? 'padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px'
|
||||||
|
: 'padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;color:#6B7280';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formTabStyle(active: boolean) {
|
||||||
|
return active
|
||||||
|
? 'padding:14px 0;margin-right:24px;font-size:14px;font-weight:600;background:none;border:none;cursor:pointer;color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px'
|
||||||
|
: 'padding:14px 0;margin-right:24px;font-size:14px;font-weight:400;background:none;border:none;cursor:pointer;color:#6B7280';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div class="w-full space-y-4 pb-8">
|
<div style="width:100%;padding-bottom:32px">
|
||||||
|
{/* Page Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-[48px] font-bold leading-[1.1] tracking-[-0.02em] text-[#0B1246]">Employee Management</h1>
|
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Employee Management</h1>
|
||||||
<p class="mt-1 text-[15px] text-[#7E849F]">Manage internal employees and their information</p>
|
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage internal employees and their information</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-none border border-[#D9DDE6] bg-white">
|
{/* Main Tabs */}
|
||||||
<div class="border-b border-[#D9DDE6] px-6">
|
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
|
||||||
<div class="flex items-end gap-10">
|
<button type="button" style={mainTabStyle(mainTab() === 'all')} onClick={() => setMainTab('all')}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setActiveTab('list')}
|
|
||||||
class={`relative pb-3 pt-4 text-[36px] font-semibold ${
|
|
||||||
activeTab() === 'list' ? 'text-[#0B1246]' : 'text-[#7E849F]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
All Employees
|
All Employees
|
||||||
<Show when={activeTab() === 'list'}>
|
|
||||||
<span class="absolute bottom-0 left-0 h-[4px] w-full bg-[#FF5E13]" />
|
|
||||||
</Show>
|
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" style={mainTabStyle(mainTab() === 'create')} onClick={() => setMainTab('create')}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setActiveTab('create')}
|
|
||||||
class={`relative pb-3 pt-4 text-[36px] font-semibold ${
|
|
||||||
activeTab() === 'create' ? 'text-[#0B1246]' : 'text-[#7E849F]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Create Employee
|
Create Employee
|
||||||
<Show when={activeTab() === 'create'}>
|
|
||||||
<span class="absolute bottom-0 left-0 h-[4px] w-full bg-[#FF5E13]" />
|
|
||||||
</Show>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show
|
{/* All Employees View */}
|
||||||
when={activeTab() === 'list'}
|
<Show when={mainTab() === 'all'}>
|
||||||
fallback={
|
<div style="margin-top:20px;margin-left:-24px;margin-right:-24px;border-radius:0;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white">
|
||||||
<div class="px-6 py-12 text-[14px] text-[#6B7280]">
|
{/* Filter Row 1 */}
|
||||||
Create Employee form will follow this new base design next.
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px 8px 20px">
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="border-b border-[#D9DDE6] px-6 py-5">
|
|
||||||
<div class="grid grid-cols-[1fr_160px_120px] gap-4">
|
|
||||||
<label class="flex h-[58px] items-center border border-[#D9DDE6] bg-[#F7F8FA] px-4 text-[#9CA3AF]">
|
|
||||||
<Search size={19} class="mr-3 text-[#9CA3AF]" />
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search employees..."
|
placeholder="Search employees"
|
||||||
value={search()}
|
value={search()}
|
||||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
onInput={(e) => { setSearch(e.currentTarget.value); setPage(1); }}
|
||||||
class="w-full border-0 bg-transparent text-[34px] text-[#0B1246] placeholder:text-[#9CA3AF] outline-none"
|
style={`height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:text;flex:1;max-width:240px;box-sizing:border-box`}
|
||||||
/>
|
/>
|
||||||
</label>
|
<select
|
||||||
|
value={deptFilter()}
|
||||||
<div class="h-[58px] border border-[#D9DDE6] bg-white" />
|
onChange={(e) => { setDeptFilter(e.currentTarget.value); setPage(1); }}
|
||||||
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:pointer"
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="h-[58px] border border-[#D9DDE6] bg-white text-[34px] font-medium text-[#0B1246] transition-colors hover:bg-[#F9FAFB]"
|
|
||||||
>
|
>
|
||||||
|
<option value="">Department</option>
|
||||||
|
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={designationFilter()}
|
||||||
|
onChange={(e) => { setDesignationFilter(e.currentTarget.value); setPage(1); }}
|
||||||
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="">Designation</option>
|
||||||
|
<For each={DESIGNATIONS}>{(d) => <option value={d}>{d}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Row 2 */}
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;padding:0 20px 14px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
|
<select
|
||||||
|
value={empTypeFilter()}
|
||||||
|
onChange={(e) => { setEmpTypeFilter(e.currentTarget.value); setPage(1); }}
|
||||||
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="">Employment Type</option>
|
||||||
|
<For each={EMPLOYMENT_TYPES}>{(t) => <option value={t}>{t}</option>}</For>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={statusFilter()}
|
||||||
|
onChange={(e) => { setStatusFilter(e.currentTarget.value); setPage(1); }}
|
||||||
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#374151;outline:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="">Status</option>
|
||||||
|
<For each={STATUSES}>{(s) => <option value={s}>{s}</option>}</For>
|
||||||
|
</select>
|
||||||
|
<button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer">
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
{/* Table */}
|
||||||
<table class="w-full min-w-[1400px] table-fixed border-collapse">
|
<div style="overflow-x:auto">
|
||||||
|
<table style="width:100%;border-collapse:collapse;min-width:1100px">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-[#02033B] text-left">
|
<tr style="background:#0D0D2A">
|
||||||
{['ID', 'Name', 'Email', 'Department', 'Designation', 'Role', 'Status', 'Actions'].map((h) => (
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Employee ID</th>
|
||||||
<th class="px-6 py-5 text-[31px] font-semibold text-white">{h}</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Employee Name</th>
|
||||||
))}
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Email</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Phone</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Department</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Designation</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Internal Role</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Joining Date</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Employment Type</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Status</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<Show when={employees.loading}>
|
<Show when={employees.loading}>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="px-6 py-10 text-center text-[24px] text-[#9CA3AF]">Loading employees...</td>
|
<td colspan="11" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">Loading employees...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={!employees.loading && paged().length === 0}>
|
||||||
<Show when={!employees.loading && filtered().length === 0}>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="px-6 py-10 text-center text-[24px] text-[#9CA3AF]">No employees found.</td>
|
<td colspan="11" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">No employees found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
|
<For each={paged()}>
|
||||||
<For each={filtered()}>
|
|
||||||
{(emp) => (
|
{(emp) => (
|
||||||
<tr class="border-b border-[#D9DDE6] bg-white align-middle">
|
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA]">
|
||||||
<td class="px-6 py-4 text-[35px] italic text-[#303A67]">{emp.id}</td>
|
<td style="padding:12px 20px">
|
||||||
<td class="px-6 py-4 text-[41px] font-semibold leading-[1.15] text-[#0B1246]">{empName(emp)}</td>
|
<span style="font-size:12px;font-family:monospace;color:#6B7280">{getEmpId(emp)}</span>
|
||||||
<td class="px-6 py-4 text-[35px] text-[#59608A]">{emp.email}</td>
|
|
||||||
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empDept(emp)}</td>
|
|
||||||
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empDesig(emp)}</td>
|
|
||||||
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empRole(emp)}</td>
|
|
||||||
<td class="px-6 py-4">
|
|
||||||
<StatusBadge status={empStatus(emp) as 'Active' | 'Inactive' | 'Probation'} />
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827;white-space:nowrap">{getEmpName(emp)}</td>
|
||||||
<div class="flex items-center gap-4">
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{emp.email}</td>
|
||||||
<button type="button" class="text-[#FF5E13] transition-colors hover:text-[#E04D0A]" aria-label="View">
|
<td style="padding:12px 20px;font-size:13px;color:#374151;white-space:nowrap">{emp.phone || '—'}</td>
|
||||||
<Eye size={23} />
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{getEmpDept(emp)}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{getEmpDesignation(emp)}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{getEmpRole(emp)}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#374151;white-space:nowrap">{getEmpJoiningDate(emp)}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#374151;white-space:nowrap">{getEmpEmploymentType(emp)}</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<StatusBadge status={getEmpStatus(emp)} />
|
||||||
|
</td>
|
||||||
|
<td style="padding:12px 20px;position:relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenActionId(openActionId() === emp.id ? null : emp.id)}
|
||||||
|
style="height:30px;padding:0 12px;font-size:12px;font-weight:500;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer"
|
||||||
|
>
|
||||||
|
Actions ▾
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="text-[#5B628D] transition-colors hover:text-[#303A67]" aria-label="Edit">
|
<Show when={openActionId() === emp.id}>
|
||||||
<SquarePen size={23} />
|
<div style="position:absolute;right:20px;top:44px;z-index:20;width:180px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.10)">
|
||||||
|
<button type="button" style="display:block;width:100%;text-align:left;padding:9px 12px;font-size:13px;color:#111827;background:none;border:none;cursor:pointer;border-radius:6px" class="hover:bg-[#F9FAFB]">
|
||||||
|
View Employee
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="text-[#5B628D] transition-colors hover:text-[#303A67]" aria-label="Delete">
|
<button type="button" style="display:block;width:100%;text-align:left;padding:9px 12px;font-size:13px;color:#111827;background:none;border:none;cursor:pointer;border-radius:6px" class="hover:bg-[#F9FAFB]">
|
||||||
<Trash2 size={23} />
|
Edit Employee
|
||||||
|
</button>
|
||||||
|
<button type="button" style="display:block;width:100%;text-align:left;padding:9px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;border-radius:6px" class="hover:bg-[#FEF2F2]">
|
||||||
|
Deactivate
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
@ -262,9 +307,388 @@ export default function EmployeesIndexPage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 20px">
|
||||||
|
<span style="font-size:13px;color:#6B7280">
|
||||||
|
Showing {Math.min((page() - 1) * PAGE_SIZE + 1, Math.max(filtered().length, 1))}–{Math.min(page() * PAGE_SIZE, filtered().length)} of {filtered().length}
|
||||||
|
</span>
|
||||||
|
<div style="display:flex;gap:4px">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPage(Math.max(1, page() - 1))}
|
||||||
|
disabled={page() === 1}
|
||||||
|
style={`height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer;opacity:${page() === 1 ? '0.4' : '1'}`}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<For each={Array.from({ length: totalPages() }, (_, i) => i + 1)}>
|
||||||
|
{(p) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPage(p)}
|
||||||
|
style={p === page()
|
||||||
|
? 'height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #FF5E13;background:#FF5E13;color:white;border-radius:6px;cursor:pointer;font-weight:600'
|
||||||
|
: 'height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer'}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPage(Math.min(totalPages(), page() + 1))}
|
||||||
|
disabled={page() === totalPages()}
|
||||||
|
style={`height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer;opacity:${page() === totalPages() ? '0.4' : '1'}`}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Create Employee View */}
|
||||||
|
<Show when={mainTab() === 'create'}>
|
||||||
|
<div style="margin-top:20px;border-radius:16px;border:1px solid #E5E7EB;background:white;overflow:hidden">
|
||||||
|
{/* Form Sub-Tabs */}
|
||||||
|
<div style="display:flex;align-items:center;border-bottom:1px solid #E5E7EB;padding:0 24px">
|
||||||
|
<button type="button" style={formTabStyle(formTab() === 'basic')} onClick={() => setFormTab('basic')}>
|
||||||
|
Basic Information
|
||||||
|
</button>
|
||||||
|
<button type="button" style={formTabStyle(formTab() === 'work')} onClick={() => setFormTab('work')}>
|
||||||
|
Work Information
|
||||||
|
</button>
|
||||||
|
<button type="button" style={formTabStyle(formTab() === 'access')} onClick={() => setFormTab('access')}>
|
||||||
|
Access & Permissions
|
||||||
|
</button>
|
||||||
|
<button type="button" style={formTabStyle(formTab() === 'docs')} onClick={() => setFormTab('docs')}>
|
||||||
|
Documents
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Information */}
|
||||||
|
<Show when={formTab() === 'basic'}>
|
||||||
|
<div style="padding:24px">
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0 0 20px 0">Add employee personal and contact details</p>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>First Name <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="Enter first name" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Last Name <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="Enter last name" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Employee ID <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="e.g., EMP001" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Email Address <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="email" placeholder="Enter email address" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Phone Number <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="tel" placeholder="+1 234-567-6900" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Alternate Phone Number</label>
|
||||||
|
<input type="tel" placeholder="+1 234-567-0900" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Date of Birth <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="date" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Gender <span style="color:#EF4444">*</span></label>
|
||||||
|
<select style={`${INPUT_STYLE};cursor:pointer`}>
|
||||||
|
<option value="">Select gender</option>
|
||||||
|
<For each={GENDERS}>{(g) => <option value={g}>{g}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Photo Upload */}
|
||||||
|
<div style="margin-top:16px">
|
||||||
|
<label style={LABEL_STYLE}>Profile Photo</label>
|
||||||
|
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:40px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:8px">
|
||||||
|
<Upload size={28} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
<p style="font-size:13px;font-weight:500;color:#374151;margin:0 0 4px 0">Click to upload profile photo</p>
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0">PNG, JPG up to 5MB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:16px">
|
||||||
|
<label style={LABEL_STYLE}>Address Line 1 <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="Street address" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:16px">
|
||||||
|
<label style={LABEL_STYLE}>Address Line 2</label>
|
||||||
|
<input type="text" placeholder="Apartment, suite, etc." style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>City <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="Enter city" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>State <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="Enter state" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Country <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="Enter country" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Postal Code <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="Enter postal code" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Work Information */}
|
||||||
|
<Show when={formTab() === 'work'}>
|
||||||
|
<div style="padding:24px">
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0 0 20px 0">Map employee to the correct internal structure</p>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Department <span style="color:#EF4444">*</span></label>
|
||||||
|
<select style={`${INPUT_STYLE};cursor:pointer`}>
|
||||||
|
<option value="">Select department</option>
|
||||||
|
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Designation <span style="color:#EF4444">*</span></label>
|
||||||
|
<select style={`${INPUT_STYLE};cursor:pointer`}>
|
||||||
|
<option value="">Select designation</option>
|
||||||
|
<For each={DESIGNATIONS}>{(d) => <option value={d}>{d}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Internal Role <span style="color:#EF4444">*</span></label>
|
||||||
|
<select style={`${INPUT_STYLE};cursor:pointer`}>
|
||||||
|
<option value="">Select internal role</option>
|
||||||
|
<For each={INTERNAL_ROLES}>{(r) => <option value={r}>{r}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Reporting Manager</label>
|
||||||
|
<input type="text" placeholder="Select reporting manager" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Employment Type <span style="color:#EF4444">*</span></label>
|
||||||
|
<select style={`${INPUT_STYLE};cursor:pointer`}>
|
||||||
|
<option value="">Select employment type</option>
|
||||||
|
<For each={EMPLOYMENT_TYPES}>{(t) => <option value={t}>{t}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Joining Date <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="date" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Work Location</label>
|
||||||
|
<input type="text" placeholder="Enter work location" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Employee Category</label>
|
||||||
|
<select style={`${INPUT_STYLE};cursor:pointer`}>
|
||||||
|
<option value="">Select category</option>
|
||||||
|
<For each={EMPLOYEE_CATEGORIES}>{(c) => <option value={c}>{c}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Shift Type</label>
|
||||||
|
<select style={`${INPUT_STYLE};cursor:pointer`}>
|
||||||
|
<option value="">Select shift type</option>
|
||||||
|
<For each={SHIFT_TYPES}>{(s) => <option value={s}>{s}</option>}</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Status <span style="color:#EF4444">*</span></label>
|
||||||
|
<select style={`${INPUT_STYLE};cursor:pointer`}>
|
||||||
|
<option value="ACTIVE">Active</option>
|
||||||
|
<option value="INACTIVE">Inactive</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Access & Permissions */}
|
||||||
|
<Show when={formTab() === 'access'}>
|
||||||
|
<div style="padding:24px">
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0 0 20px 0">Configure admin panel access and employee account settings</p>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Username <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="text" placeholder="Enter username" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Password <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="password" placeholder="Enter password" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={LABEL_STYLE}>Confirm Password <span style="color:#EF4444">*</span></label>
|
||||||
|
<input type="password" placeholder="Confirm password" style={INPUT_STYLE} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle rows */}
|
||||||
|
<div style="margin-top:20px">
|
||||||
|
{/* Allow Admin Panel Login */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Admin Panel Login</p>
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0">Enable this employee to access the admin panel</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={allowAdminLogin()} onChange={setAllowAdminLogin} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Require Password Reset */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Require Password Reset on First Login</p>
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0">Force employee to change password on first login</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={requirePasswordReset()} onChange={setRequirePasswordReset} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enable 2FA */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Enable Two-Factor Authentication</p>
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0">Require 2FA for enhanced security</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={enable2FA()} onChange={setEnable2FA} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Account Active */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Account Active / Inactive</p>
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0">Control whether the account is active</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={accountActive()} onChange={setAccountActive} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Allow Mobile App */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Access from Mobile App</p>
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0">Enable access from mobile devices</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={allowMobileApp()} onChange={setAllowMobileApp} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Allow Reports */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;margin-top:12px">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:14px;font-weight:500;color:#111827;margin:0 0 2px 0">Allow Access to Reports</p>
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0">Grant access to view and download reports</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={allowReports()} onChange={setAllowReports} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Documents */}
|
||||||
|
<Show when={formTab() === 'docs'}>
|
||||||
|
<div style="padding:24px">
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:0 0 20px 0">Upload and manage employee verification and employment documents</p>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||||
|
{/* ID Proof */}
|
||||||
|
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">ID Proof</p>
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:6px">
|
||||||
|
<Upload size={24} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Address Proof */}
|
||||||
|
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Address Proof</p>
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:6px">
|
||||||
|
<Upload size={24} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Offer Letter */}
|
||||||
|
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Offer Letter</p>
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:6px">
|
||||||
|
<Upload size={24} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Employment Contract */}
|
||||||
|
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Employment Contract</p>
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:6px">
|
||||||
|
<Upload size={24} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Education Certificate */}
|
||||||
|
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Education Certificate</p>
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:6px">
|
||||||
|
<Upload size={24} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Experience Certificate */}
|
||||||
|
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer" class="hover:bg-[#FAFAFA]">
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 12px 0">Experience Certificate</p>
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:6px">
|
||||||
|
<Upload size={24} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0">Click to upload</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Other Supporting Documents */}
|
||||||
|
<div style="border:2px dashed #E5E7EB;border-radius:12px;padding:32px;text-align:center;margin-top:16px;cursor:pointer" class="hover:bg-[#FAFAFA]">
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:8px">
|
||||||
|
<Upload size={28} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
<p style="font-size:13px;font-weight:500;color:#374151;margin:0 0 4px 0">Click to upload additional documents</p>
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0">PDF, DOC, DOCX, JPG, PNG up to 10MB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Form Footer */}
|
||||||
|
<div style="display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #E5E7EB">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMainTab('all')}
|
||||||
|
style="height:38px;padding:0 20px;font-size:14px;font-weight:500;background:white;color:#374151;border:1px solid #E5E7EB;border-radius:8px;cursor:pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style="background:#0D0D2A;color:white;height:38px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
Create Employee
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,537 +1,233 @@
|
||||||
import { createMemo, createSignal, For, onMount, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
import { A } from '@solidjs/router';
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '/api/gateway';
|
||||||
const LEGACY_INTERNAL_PREVIEW_BASE = String(import.meta.env.VITE_LEGACY_ADMIN_PREVIEW_URL || 'http://localhost:3002').replace(/\/+$/, '');
|
|
||||||
|
|
||||||
// ---------- Types ----------
|
type InternalDashboard = {
|
||||||
type SidebarItem = { key: string; label: string; visible: boolean; order: number };
|
id: string;
|
||||||
type Field = { id: string; label: string; type: 'text' | 'number' | 'select' | 'date'; required: boolean; placeholder?: string };
|
name: string;
|
||||||
type Tab = { id: string; title: string; fields: Field[] };
|
department: string;
|
||||||
type Widget = { id: string; title: string; metric: string; description?: string };
|
designation: string;
|
||||||
type Section = { id: string; title: string; tabs: Tab[]; widgets: Widget[] };
|
role: string;
|
||||||
type Dashboard = { id: string; roleId: string; roleName: string; title: string; description?: string; status: string; version: number; sidebar: SidebarItem[]; sections: Section[] };
|
widgetsCount: number;
|
||||||
type InternalRole = { id: string; key: string; name: string };
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
const FIELD_TYPES: Field['type'][] = ['text', 'number', 'select', 'date'];
|
const FALLBACK: InternalDashboard[] = [
|
||||||
const makeId = (prefix: string) => `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
{ 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 {
|
async function loadDashboards(): Promise<InternalDashboard[]> {
|
||||||
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 () => {
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
const res = await fetch(`${API}/api/admin/internal-dashboards`);
|
||||||
setError('');
|
if (!res.ok) throw new Error('Failed');
|
||||||
const res = await fetch(`${API}/api/admin/dashboard-config?audience=INTERNAL`);
|
|
||||||
if (!res.ok) throw new Error('Failed to load internal dashboards');
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const rows = (Array.isArray(data) ? data : (data.dashboards || []))
|
const rows = Array.isArray(data) ? data : (data.dashboards || data.templates || []);
|
||||||
.filter((item: any) => String(item?.audience || '').toUpperCase() === 'INTERNAL')
|
if (!rows.length) return FALLBACK;
|
||||||
.map((item: any) => ({
|
return rows.map((item: any) => ({
|
||||||
id: String(item.id || ''),
|
id: String(item.id || ''),
|
||||||
roleId: String(item.role_id || ''),
|
name: String(item.name || item.title || 'Untitled Dashboard'),
|
||||||
roleName: '',
|
department: String(item.department || item.assigned_department || ''),
|
||||||
title: 'Internal Dashboard',
|
designation: String(item.designation || item.assigned_designation || ''),
|
||||||
description: '',
|
role: String(item.role || item.role_name || item.assigned_role || ''),
|
||||||
status: item.is_active ? 'published' : 'draft',
|
widgetsCount: Number(item.widgets_count || item.widgetsCount || 0),
|
||||||
version: Number(item.version) || 1,
|
status: item.is_active ? 'ACTIVE' : (String(item.status || 'DRAFT').toUpperCase()),
|
||||||
sidebar: [],
|
|
||||||
sections: [],
|
|
||||||
}));
|
}));
|
||||||
setDashboards(rows);
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || 'Failed to load dashboards');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadRoles = async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API}/api/admin/roles?audience=INTERNAL`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
setRoles(
|
|
||||||
(Array.isArray(data) ? data : (data.roles || []))
|
|
||||||
.filter((r: any) => String(r?.audience || '').toUpperCase() === 'INTERNAL')
|
|
||||||
.map((r: any) => ({ id: String(r.id || ''), key: String(r.key || ''), name: String(r.name || 'Internal Role') })),
|
|
||||||
);
|
|
||||||
} catch { setRoles([]); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const hydrateDashboard = async (configId: string) => {
|
|
||||||
const base = dashboards().find((item) => item.id === configId);
|
|
||||||
if (!base || !base.roleId) return;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API}/api/admin/dashboard-config/${base.roleId}?audience=INTERNAL`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const detail = await res.json();
|
|
||||||
const role = roles().find((item) => item.id === base.roleId);
|
|
||||||
const hydrated = normalizeInternalDashboardFromConfig(
|
|
||||||
{
|
|
||||||
...base,
|
|
||||||
roleName: role?.name || base.roleName,
|
|
||||||
status: detail?.is_active ? 'published' : base.status,
|
|
||||||
version: Number(detail?.version) || base.version,
|
|
||||||
},
|
|
||||||
detail?.config_json || {},
|
|
||||||
);
|
|
||||||
setDashboards((prev) => prev.map((item) => (item.id === configId ? hydrated : item)));
|
|
||||||
} catch {
|
} catch {
|
||||||
// Keep list-summary data if detail fetch fails.
|
return FALLBACK;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const openDashboard = async (configId: string) => {
|
function StatusBadge(props: { status: string }) {
|
||||||
setSelectedId(configId);
|
const isActive = () => props.status === 'ACTIVE';
|
||||||
setActiveTab('overview');
|
const isDraft = () => props.status === 'DRAFT';
|
||||||
await hydrateDashboard(configId);
|
const baseStyle = 'border-radius:9999px;padding:3px 10px;font-size:12px;font-weight:500;display:inline-block';
|
||||||
};
|
const activeStyle = `${baseStyle};background:#FFF1EB;color:#FF5E13;border:1px solid #FFD8C2`;
|
||||||
|
const draftStyle = `${baseStyle};background:#F3F4F6;color:#6B7280;border:1px solid #D1D5DB`;
|
||||||
|
const otherStyle = `${baseStyle};background:#F0FDF4;color:#166534;border:1px solid #BBF7D0`;
|
||||||
|
return (
|
||||||
|
<span style={isActive() ? activeStyle : isDraft() ? draftStyle : otherStyle}>
|
||||||
|
{props.status}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const selected = () => dashboards().find((d) => d.id === selectedId()) || null;
|
export default function InternalDashboardManagementPage() {
|
||||||
const livePreviewUrl = createMemo(() => {
|
const [dashboards, { refetch }] = createResource(loadDashboards);
|
||||||
const roleId = String(selected()?.roleId || '').trim();
|
const [search, setSearch] = createSignal('');
|
||||||
if (!roleId) return `${LEGACY_INTERNAL_PREVIEW_BASE}/internal-dashboard-management`;
|
const [statusFilter, setStatusFilter] = createSignal('All');
|
||||||
return `${LEGACY_INTERNAL_PREVIEW_BASE}/internal-dashboard-management?roleId=${encodeURIComponent(roleId)}`;
|
const [activeTab, setActiveTab] = createSignal<'dashboard' | 'preview'>('dashboard');
|
||||||
|
|
||||||
|
const filtered = createMemo(() => {
|
||||||
|
const q = search().toLowerCase();
|
||||||
|
const st = statusFilter();
|
||||||
|
return (dashboards() || FALLBACK).filter((d) => {
|
||||||
|
const matchesSearch = !q || d.name.toLowerCase().includes(q) || d.department.toLowerCase().includes(q) || d.role.toLowerCase().includes(q);
|
||||||
|
const matchesStatus = st === 'All' || d.status === st;
|
||||||
|
return matchesSearch && matchesStatus;
|
||||||
});
|
});
|
||||||
const livePreviewRoleLabel = createMemo(() => String(selected()?.roleName || '').trim() || 'Unlinked Role');
|
|
||||||
|
|
||||||
const update = (patch: Partial<Dashboard>) =>
|
|
||||||
setDashboards((prev) => prev.map((d) => d.id === selectedId() ? { ...d, ...patch } : d));
|
|
||||||
|
|
||||||
const updateSection = (sectionId: string, patch: Partial<Section>) =>
|
|
||||||
update({ sections: selected()!.sections.map((s) => s.id === sectionId ? { ...s, ...patch } : s) });
|
|
||||||
|
|
||||||
const updateTab = (sectionId: string, tabId: string, patch: Partial<Tab>) => {
|
|
||||||
const section = selected()!.sections.find((s) => s.id === sectionId)!;
|
|
||||||
updateSection(sectionId, { tabs: section.tabs.map((t) => t.id === tabId ? { ...t, ...patch } : t) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateField = (sId: string, tId: string, fId: string, patch: Partial<Field>) => {
|
|
||||||
const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!;
|
|
||||||
updateTab(sId, tId, { fields: tab.fields.map((f) => f.id === fId ? { ...f, ...patch } : f) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateWidget = (sId: string, wId: string, patch: Partial<Widget>) => {
|
|
||||||
const section = selected()!.sections.find((s) => s.id === sId)!;
|
|
||||||
updateSection(sId, { widgets: section.widgets.map((w) => w.id === wId ? { ...w, ...patch } : w) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const addSidebarItem = () => {
|
|
||||||
const items = selected()!.sidebar;
|
|
||||||
update({ sidebar: [...items, { key: makeId('sb'), label: 'New Sidebar Item', visible: true, order: items.length + 1 }] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeSidebarItem = (key: string) =>
|
|
||||||
update({ sidebar: selected()!.sidebar.filter((i) => i.key !== key).map((i, idx) => ({ ...i, order: idx + 1 })) });
|
|
||||||
|
|
||||||
const addSection = () =>
|
|
||||||
update({ sections: [...selected()!.sections, { id: makeId('sec'), title: 'New Section', tabs: [], widgets: [] }] });
|
|
||||||
|
|
||||||
const removeSection = (id: string) =>
|
|
||||||
update({ sections: selected()!.sections.filter((s) => s.id !== id) });
|
|
||||||
|
|
||||||
const addTab = (sId: string) => {
|
|
||||||
const section = selected()!.sections.find((s) => s.id === sId)!;
|
|
||||||
updateSection(sId, { tabs: [...section.tabs, { id: makeId('tab'), title: 'New Tab', fields: [] }] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTab = (sId: string, tId: string) => {
|
|
||||||
const section = selected()!.sections.find((s) => s.id === sId)!;
|
|
||||||
updateSection(sId, { tabs: section.tabs.filter((t) => t.id !== tId) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const addField = (sId: string, tId: string) => {
|
|
||||||
const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!;
|
|
||||||
updateTab(sId, tId, { fields: [...tab.fields, { id: makeId('fld'), label: 'New Field', type: 'text', required: false, placeholder: 'Enter value' }] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeField = (sId: string, tId: string, fId: string) => {
|
|
||||||
const tab = selected()!.sections.find((s) => s.id === sId)!.tabs.find((t) => t.id === tId)!;
|
|
||||||
updateTab(sId, tId, { fields: tab.fields.filter((f) => f.id !== fId) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const addWidget = (sId: string) => {
|
|
||||||
const section = selected()!.sections.find((s) => s.id === sId)!;
|
|
||||||
updateSection(sId, { widgets: [...section.widgets, { id: makeId('wgt'), title: 'New Widget', metric: '0', description: 'Describe this widget.' }] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeWidget = (sId: string, wId: string) => {
|
|
||||||
const section = selected()!.sections.find((s) => s.id === sId)!;
|
|
||||||
updateSection(sId, { widgets: section.widgets.filter((w) => w.id !== wId) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDashboard = async () => {
|
|
||||||
try {
|
|
||||||
setCreating(true);
|
|
||||||
setError('');
|
|
||||||
let newId = makeId('local');
|
|
||||||
const defaultRole = roles()[0];
|
|
||||||
try {
|
|
||||||
if (defaultRole?.id) {
|
|
||||||
const res = await fetch(`${API}/api/admin/dashboard-config`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
role_id: defaultRole.id,
|
|
||||||
audience: 'INTERNAL',
|
|
||||||
config_json: { title: `${defaultRole.name} Dashboard`, description: '', roleName: defaultRole.name, version: 1, sidebar: [], sections: [], nav: [] },
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
newId = data.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { /* backend unavailable — use local draft */ }
|
|
||||||
const nd: Dashboard = {
|
|
||||||
id: newId,
|
|
||||||
roleId: defaultRole?.id || '',
|
|
||||||
roleName: defaultRole?.name || '',
|
|
||||||
title: `${defaultRole?.name || 'New Internal'} Dashboard`,
|
|
||||||
status: 'draft',
|
|
||||||
version: 1,
|
|
||||||
sidebar: [],
|
|
||||||
sections: [],
|
|
||||||
};
|
|
||||||
setDashboards((prev) => [nd, ...prev]);
|
|
||||||
setSelectedId(newId);
|
|
||||||
setActiveTab('overview');
|
|
||||||
} finally {
|
|
||||||
setCreating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSelected = async () => {
|
const totalTemplates = createMemo(() => (dashboards() || FALLBACK).length);
|
||||||
const d = selected();
|
const activeTemplates = createMemo(() => (dashboards() || FALLBACK).filter((d) => d.status === 'ACTIVE').length);
|
||||||
if (!d) return;
|
const draftTemplates = createMemo(() => (dashboards() || FALLBACK).filter((d) => d.status === 'DRAFT').length);
|
||||||
if (!d.roleId) {
|
const assignedRoles = createMemo(() => {
|
||||||
setError('Please select an internal role before saving.');
|
const roles = new Set((dashboards() || FALLBACK).map((d) => d.role).filter(Boolean));
|
||||||
return;
|
return roles.size;
|
||||||
}
|
|
||||||
try {
|
|
||||||
setSaving(true);
|
|
||||||
setError('');
|
|
||||||
const nav = d.sidebar
|
|
||||||
.filter((item) => item.visible)
|
|
||||||
.sort((a, b) => a.order - b.order)
|
|
||||||
.map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
label: item.label,
|
|
||||||
path: '/internal-dashboard-management',
|
|
||||||
}));
|
|
||||||
const res = await fetch(`${API}/api/admin/dashboard-config`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
role_id: d.roleId,
|
|
||||||
audience: 'INTERNAL',
|
|
||||||
config_json: { title: d.title, description: d.description, roleName: d.roleName, version: d.version, sidebar: d.sidebar, sections: d.sections, nav },
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Failed to save dashboard');
|
const unassignedRoles = createMemo(() => (dashboards() || FALLBACK).filter((d) => !d.role).length);
|
||||||
await loadDashboards();
|
|
||||||
const next = dashboards().find((item) => item.roleId === d.roleId);
|
const pillActive = 'background:#0D0D2A;color:white;border:none;height:32px;border-radius:9999px;padding:0 16px;font-size:13px;font-weight:500;cursor:pointer';
|
||||||
if (next) await openDashboard(next.id);
|
const pillInactive = 'background:transparent;color:#6B7280;border:1px solid #E5E7EB;height:32px;border-radius:9999px;padding:0 16px;font-size:13px;font-weight:500;cursor:pointer';
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || 'Failed to save dashboard');
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------- List view ----------
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
<div style="width:100%;padding-bottom:32px">
|
||||||
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
|
|
||||||
<Show when={!selected()}>
|
{/* Header */}
|
||||||
{/* Header & Title */}
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px">
|
||||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-[32px] font-bold text-[#0D0D2A] leading-tight">Internal Dashboard Management</h1>
|
<h1 style="font-size:28px;font-weight:700;color:#111827">Internal Dashboard Management</h1>
|
||||||
<p class="text-[15px] text-[#8087a0] mt-1">Configure dashboards for internal staff members</p>
|
<p style="margin-top:4px;font-size:14px;color:#6B7280">Manage internal dashboard templates for departments, designations, and roles.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div style="display:flex;gap:8px;align-items:center;flex-shrink:0;margin-top:4px">
|
||||||
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0D0D2A] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]">
|
<button style={activeTab() === 'dashboard' ? pillActive : pillInactive} onClick={() => setActiveTab('dashboard')}>
|
||||||
Dashboard Management
|
Dashboard Management
|
||||||
</button>
|
</button>
|
||||||
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
|
<button style={activeTab() === 'preview' ? pillActive : pillInactive} onClick={() => setActiveTab('preview')}>
|
||||||
Role Preview
|
Role Preview
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 5 KPI Cards Row */}
|
{/* Stats Row */}
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
<div style="display:flex;gap:16px;margin-top:24px">
|
||||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
|
||||||
<p class="text-[13px] text-[#8087a0] leading-snug">Total Dashboard<br/>Templates</p>
|
<p style="font-size:13px;color:#6B7280;margin:0">Total Dashboard Templates</p>
|
||||||
<p class="mt-3 text-[32px] font-bold text-[#0D0D2A] leading-none">18</p>
|
<p style="font-size:28px;font-weight:700;color:#111827;margin:4px 0 0">{dashboards.loading ? '—' : totalTemplates()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
|
||||||
<p class="text-[13px] text-[#8087a0] leading-snug">Active Templates</p>
|
<p style="font-size:13px;color:#6B7280;margin:0">Active Templates</p>
|
||||||
<p class="mt-3 text-[32px] font-bold text-[#00c853] leading-none">14</p>
|
<p style="font-size:28px;font-weight:700;color:#FF5E13;margin:4px 0 0">{dashboards.loading ? '—' : activeTemplates()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
|
||||||
<p class="text-[13px] text-[#8087a0] leading-snug">Draft Templates</p>
|
<p style="font-size:13px;color:#6B7280;margin:0">Draft Templates</p>
|
||||||
<p class="mt-3 text-[32px] font-bold text-[#64748b] leading-none">2</p>
|
<p style="font-size:28px;font-weight:700;color:#6B7280;margin:4px 0 0">{dashboards.loading ? '—' : draftTemplates()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
|
||||||
<p class="text-[13px] text-[#8087a0] leading-snug">Assigned Roles</p>
|
<p style="font-size:13px;color:#6B7280;margin:0">Assigned Roles</p>
|
||||||
<p class="mt-3 text-[32px] font-bold text-[#2962ff] leading-none">9</p>
|
<p style="font-size:28px;font-weight:700;color:#3730A3;margin:4px 0 0">{dashboards.loading ? '—' : assignedRoles()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px 20px;flex:1">
|
||||||
<p class="text-[13px] text-[#8087a0] leading-snug">Unassigned Roles</p>
|
<p style="font-size:13px;color:#6B7280;margin:0">Unassigned Roles</p>
|
||||||
<p class="mt-3 text-[32px] font-bold text-[#ff6e30] leading-none">2</p>
|
<p style="font-size:28px;font-weight:700;color:#111827;margin:4px 0 0">{dashboards.loading ? '—' : unassignedRoles()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Table Section */}
|
{/* Table Card */}
|
||||||
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
|
<div style="margin-top:24px;border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
|
||||||
<div class="rounded-[20px] bg-white p-5">
|
|
||||||
{/* Table Action Header */}
|
{/* Card Header */}
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between mb-6">
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
<h2 class="text-[22px] font-bold text-[#0D0D2A]">Internal Dashboard<br/>Templates</h2>
|
<h2 style="font-size:16px;font-weight:600;color:#111827;margin:0">Internal Dashboard Templates</h2>
|
||||||
<div class="flex items-center gap-3">
|
<div style="display:flex;gap:8px">
|
||||||
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
|
<button style="border:1px solid #E5E7EB;background:white;color:#374151;height:36px;border-radius:8px;padding:0 14px;font-size:13px;font-weight:500;cursor:pointer">
|
||||||
Import Layout
|
Import Layout
|
||||||
</button>
|
</button>
|
||||||
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
|
<button style="border:1px solid #E5E7EB;background:white;color:#374151;height:36px;border-radius:8px;padding:0 14px;font-size:13px;font-weight:500;cursor:pointer">
|
||||||
Export Config
|
Export Config
|
||||||
</button>
|
</button>
|
||||||
<button class="inline-flex h-[42px] items-center justify-center rounded-xl bg-[#0D0D2A] px-5 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" onClick={createDashboard} disabled={creating()}>
|
<A
|
||||||
<span class="mr-2 text-lg leading-none">+</span> {creating() ? 'Creating...' : 'Create Dashboard Template'}
|
href="/admin/internal-dashboard-management/new"
|
||||||
</button>
|
style="background:#0D0D2A;color:white;border:none;height:36px;border-radius:8px;padding:0 16px;font-size:13px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;text-decoration:none"
|
||||||
|
>
|
||||||
|
<span>+</span> Create Dashboard Template
|
||||||
|
</A>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Filter Bar */}
|
||||||
<Show when={error()}>
|
<div style="display:flex;gap:12px;align-items:center;padding:12px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
|
<div style="position:relative;flex:1;max-width:320px">
|
||||||
</Show>
|
<svg style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#9CA3AF" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
|
||||||
{/* Filters Row */}
|
|
||||||
<div class="flex items-center gap-4 mb-6">
|
|
||||||
<div class="relative w-[320px]">
|
|
||||||
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
|
|
||||||
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search templates..."
|
placeholder="Search dashboards..."
|
||||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white"
|
value={search()}
|
||||||
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
style="height:36px;width:100%;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px 0 32px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
|
<select
|
||||||
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
|
value={statusFilter()}
|
||||||
|
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||||
|
style="height:36px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="All">Status: All</option>
|
||||||
|
<option value="ACTIVE">Active</option>
|
||||||
|
<option value="DRAFT">Draft</option>
|
||||||
|
</select>
|
||||||
|
<select style="height:36px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer">
|
||||||
|
<option>Department: All</option>
|
||||||
|
<option>Administration</option>
|
||||||
|
<option>Engineering</option>
|
||||||
|
<option>Human Resources</option>
|
||||||
|
<option>Finance</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div class="overflow-x-auto">
|
<div style="overflow-x:auto">
|
||||||
<table class="w-full min-w-[1000px] border-collapse">
|
<table style="width:100%;border-collapse:collapse;min-width:900px">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-[#0D0D2A] text-left text-white">
|
<tr style="background:#0D0D2A">
|
||||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl">DASHBOARD NAME</th>
|
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">DASHBOARD NAME</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED DEPARTMENT</th>
|
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ASSIGNED DEPARTMENT</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED DESIGNATION</th>
|
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ASSIGNED DESIGNATION</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED INTERNAL ROLE</th>
|
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ASSIGNED INTERNAL ROLE</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-center">WIDGETS COUNT</th>
|
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">WIDGETS COUNT</th>
|
||||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tr-xl text-center">STATUS</th>
|
<th style="padding:12px 20px;text-align:left;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">STATUS</th>
|
||||||
|
<th style="padding:12px 20px;text-align:right;font-size:12px;font-weight:600;color:#FFFFFF;white-space:nowrap">ACTIONS</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<Show when={loading()}>
|
<Show when={dashboards.loading}>
|
||||||
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-sm">Loading templates...</td></tr>
|
<tr>
|
||||||
|
<td colspan="7" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">Loading dashboards...</td>
|
||||||
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!loading() && dashboards().length === 0}>
|
<Show when={!dashboards.loading && filtered().length === 0}>
|
||||||
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-sm">No dashboard templates found.</td></tr>
|
<tr>
|
||||||
|
<td colspan="7" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">No dashboard templates found.</td>
|
||||||
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
<For each={dashboards()}>
|
<For each={filtered()}>
|
||||||
{(d) => (
|
{(dashboard, index) => (
|
||||||
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc] cursor-pointer" onClick={() => void openDashboard(d.id)}>
|
<tr style={`border-bottom:1px solid #F3F4F6;background:${index() % 2 === 0 ? 'white' : '#FAFAFA'}`}>
|
||||||
<td class="px-6 py-4 text-[14px] font-bold text-[#0D0D2A]">{d.title}</td>
|
<td style="padding:12px 20px;font-size:14px;color:#111827;font-weight:500">{dashboard.name}</td>
|
||||||
<td class="px-6 py-4 text-[14px] text-[#475569]">Administration</td>
|
<td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.department || '—'}</td>
|
||||||
<td class="px-6 py-4 text-[14px] text-[#475569]">Super Admin</td>
|
<td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.designation || '—'}</td>
|
||||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{d.roleName || 'Unassigned'}</td>
|
<td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.role || '—'}</td>
|
||||||
<td class="px-6 py-4 text-[14px] text-[#475569] text-center">{d.sections.length * 3 || 12}</td>
|
<td style="padding:12px 20px;font-size:14px;color:#374151">{dashboard.widgetsCount}</td>
|
||||||
<td class="px-6 py-4 text-center">
|
<td style="padding:12px 20px">
|
||||||
<span class={`inline-flex items-center justify-center rounded-lg px-3 py-1 text-[12px] font-bold ${
|
<StatusBadge status={dashboard.status} />
|
||||||
d.status === 'published' ? 'bg-[#e6f9ed] text-[#00c853]' : 'bg-[#f1f5f9] text-[#64748b]'
|
</td>
|
||||||
}`}>
|
<td style="padding:12px 20px;text-align:right">
|
||||||
{d.status === 'published' ? 'Active' : 'Draft'}
|
<div style="display:inline-flex;gap:8px">
|
||||||
</span>
|
<A
|
||||||
|
href={`/admin/internal-dashboard-management/${dashboard.id}`}
|
||||||
|
style="font-size:13px;color:#FF5E13;font-weight:500;text-decoration:none;border:1px solid #FFD8C2;border-radius:6px;padding:4px 10px;background:#FFF1EB"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</A>
|
||||||
|
<button style="font-size:13px;color:#6B7280;font-weight:500;border:1px solid #E5E7EB;border-radius:6px;padding:4px 10px;background:white;cursor:pointer">
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
@ -539,267 +235,16 @@ export default function InternalDashboardManagementPage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* ---------- Builder view ---------- */}
|
{/* Pagination */}
|
||||||
<Show when={selected()}>
|
<div style="display:flex;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px;align-items:center">
|
||||||
<div class="builder-header">
|
<span style="font-size:13px;color:#6B7280">Showing {filtered().length} of {(dashboards() || FALLBACK).length} templates</span>
|
||||||
<div>
|
<div style="display:flex;gap:4px">
|
||||||
<h2>Internal Dashboard Builder</h2>
|
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:white;border-radius:6px;font-size:13px;color:#374151;cursor:pointer;padding:0 10px">Previous</button>
|
||||||
<p>Manage menu items, sections, tabs, form fields, and summary cards from one place.</p>
|
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:#0D0D2A;border-radius:6px;font-size:13px;color:white;cursor:pointer;padding:0 10px">1</button>
|
||||||
</div>
|
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:white;border-radius:6px;font-size:13px;color:#374151;cursor:pointer;padding:0 10px">Next</button>
|
||||||
<div class="builder-header-actions">
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => setSelectedId('')}>Back to List</button>
|
|
||||||
<button class="btn-primary" onClick={saveSelected} disabled={saving()}>
|
|
||||||
{saving() ? 'Saving...' : 'Save Dashboard'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={error()}><div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div></Show>
|
|
||||||
|
|
||||||
{/* Tab bar */}
|
|
||||||
<div class="builder-tab-bar">
|
|
||||||
{(['overview', 'sidebar', 'sections', 'preview'] as const).map((t) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`builder-tab-btn ${activeTab() === t ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab(t)}
|
|
||||||
>
|
|
||||||
{t === 'sections' ? 'Sections, Tabs & Fields' : t.charAt(0).toUpperCase() + t.slice(1)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Overview */}
|
|
||||||
<Show when={activeTab() === 'overview'}>
|
|
||||||
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
||||||
<div class="field">
|
|
||||||
<label>Team Role</label>
|
|
||||||
<select
|
|
||||||
value={selected()!.roleId}
|
|
||||||
onChange={(e) => {
|
|
||||||
const role = roles().find((r) => r.id === e.currentTarget.value);
|
|
||||||
update({ roleId: e.currentTarget.value, roleName: role?.name || '', title: role ? `${role.name} Dashboard` : selected()!.title });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="">Select team role</option>
|
|
||||||
{roles().map((r) => <option value={r.id}>{r.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Dashboard Title</label>
|
|
||||||
<input value={selected()!.title} onInput={(e) => update({ title: e.currentTarget.value })} />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Short description</label>
|
|
||||||
<input value={selected()!.description || ''} onInput={(e) => update({ description: e.currentTarget.value })} />
|
|
||||||
</div>
|
|
||||||
<div class="info-box">
|
|
||||||
<p style="margin:0 0 4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#64748b">Linked Role</p>
|
|
||||||
<p style="margin:0;font-weight:600;color:#0f172a">{selected()!.roleName || 'No role selected yet'}</p>
|
|
||||||
<p style="margin:2px 0 0;font-size:12px;color:#64748b">{selected()!.roleId || 'Select a team role to connect this dashboard.'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Sidebar */}
|
|
||||||
<Show when={activeTab() === 'sidebar'}>
|
|
||||||
<div class="builder-section">
|
|
||||||
<div class="sub-card-header">
|
|
||||||
<h4>Menu</h4>
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSidebarItem}>Add Menu Item</button>
|
|
||||||
</div>
|
|
||||||
<For each={selected()!.sidebar}>
|
|
||||||
{(item, idx) => (
|
|
||||||
<div class="builder-item builder-item-row-4">
|
|
||||||
<input
|
|
||||||
value={item.label}
|
|
||||||
placeholder="Menu label"
|
|
||||||
onInput={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, label: e.currentTarget.value } : i) })}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={item.order}
|
|
||||||
onInput={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, order: Number(e.currentTarget.value) || idx() + 1 } : i) })}
|
|
||||||
style="width:80px"
|
|
||||||
/>
|
|
||||||
<label class="checkbox-label" style="justify-content:center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={item.visible}
|
|
||||||
onChange={(e) => update({ sidebar: selected()!.sidebar.map((i) => i.key === item.key ? { ...i, visible: e.currentTarget.checked } : i) })}
|
|
||||||
/>
|
|
||||||
Show
|
|
||||||
</label>
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSidebarItem(item.key)}>Remove</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<Show when={selected()!.sidebar.length === 0}>
|
|
||||||
<p class="notice">No menu items yet. Add your first menu item.</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Sections, Tabs & Fields */}
|
|
||||||
<Show when={activeTab() === 'sections'}>
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
|
|
||||||
<h3 style="margin:0;font-size:15px;font-weight:700">Sections</h3>
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSection}>Add Section</button>
|
|
||||||
</div>
|
|
||||||
<For each={selected()!.sections}>
|
|
||||||
{(section) => (
|
|
||||||
<div class="builder-section">
|
|
||||||
<div class="builder-section-header">
|
|
||||||
<input
|
|
||||||
value={section.title}
|
|
||||||
onInput={(e) => updateSection(section.id, { title: e.currentTarget.value })}
|
|
||||||
placeholder="Section title"
|
|
||||||
/>
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSection(section.id)}>Remove</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div class="sub-card">
|
|
||||||
<div class="sub-card-header">
|
|
||||||
<h4>Tabs and Form Fields</h4>
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addTab(section.id)}>Add Tab</button>
|
|
||||||
</div>
|
|
||||||
<For each={section.tabs}>
|
|
||||||
{(tab) => (
|
|
||||||
<div class="nested-card">
|
|
||||||
<div class="nested-card-header">
|
|
||||||
<input
|
|
||||||
value={tab.title}
|
|
||||||
onInput={(e) => updateTab(section.id, tab.id, { title: e.currentTarget.value })}
|
|
||||||
placeholder="Tab title"
|
|
||||||
/>
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeTab(section.id, tab.id)}>Remove</button>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;justify-content:flex-end;margin-bottom:8px">
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addField(section.id, tab.id)}>Add Field</button>
|
|
||||||
</div>
|
|
||||||
<For each={tab.fields}>
|
|
||||||
{(field) => (
|
|
||||||
<div class="field-row">
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
value={field.label}
|
|
||||||
onInput={(e) => updateField(section.id, tab.id, field.id, { label: e.currentTarget.value })}
|
|
||||||
placeholder="Field label"
|
|
||||||
style="width:100%;margin-bottom:4px"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
value={field.placeholder || ''}
|
|
||||||
onInput={(e) => updateField(section.id, tab.id, field.id, { placeholder: e.currentTarget.value })}
|
|
||||||
placeholder="Help text inside the input"
|
|
||||||
style="width:100%"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
value={field.type}
|
|
||||||
onChange={(e) => updateField(section.id, tab.id, field.id, { type: e.currentTarget.value as Field['type'] })}
|
|
||||||
>
|
|
||||||
{FIELD_TYPES.map((t) => <option value={t}>{t}</option>)}
|
|
||||||
</select>
|
|
||||||
<label class="checkbox-label" style="justify-content:center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={field.required}
|
|
||||||
onChange={(e) => updateField(section.id, tab.id, field.id, { required: e.currentTarget.checked })}
|
|
||||||
/>
|
|
||||||
Required
|
|
||||||
</label>
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeField(section.id, tab.id, field.id)}>Remove</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<Show when={tab.fields.length === 0}>
|
|
||||||
<p class="notice" style="text-align:center">No fields in this tab yet.</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<Show when={section.tabs.length === 0}>
|
|
||||||
<p class="notice">No tabs yet. Add a tab above.</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Widgets */}
|
|
||||||
<div class="sub-card" style="margin-top:8px">
|
|
||||||
<div class="sub-card-header">
|
|
||||||
<h4>Widgets</h4>
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addWidget(section.id)}>Add Widget</button>
|
|
||||||
</div>
|
|
||||||
<For each={section.widgets}>
|
|
||||||
{(widget) => (
|
|
||||||
<div class="widget-item">
|
|
||||||
<input value={widget.title} onInput={(e) => updateWidget(section.id, widget.id, { title: e.currentTarget.value })} placeholder="Widget title" />
|
|
||||||
<input value={widget.metric} onInput={(e) => updateWidget(section.id, widget.id, { metric: e.currentTarget.value })} placeholder="Metric value e.g. 42" />
|
|
||||||
<textarea rows={2} value={widget.description || ''} onInput={(e) => updateWidget(section.id, widget.id, { description: e.currentTarget.value })} placeholder="Widget description" />
|
|
||||||
<div style="display:flex;justify-content:flex-end">
|
|
||||||
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" style="font-size:12px;padding:5px 10px" onClick={() => removeWidget(section.id, widget.id)}>Remove Widget</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<Show when={section.widgets.length === 0}>
|
|
||||||
<p class="notice">No widgets yet.</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<Show when={selected()!.sections.length === 0}>
|
|
||||||
<div style="text-align:center;padding:32px;border:1px dashed #cbd5e1;border-radius:12px;color:#94a3b8;font-size:13px">No sections yet. Add the first section above.</div>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Preview */}
|
|
||||||
<Show when={activeTab() === 'preview'}>
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:12px">
|
|
||||||
<p class="notice" style="margin:0">Preview this dashboard as a sample view or open the live version.</p>
|
|
||||||
<div class="admin-segmented" style="margin:0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`admin-segment ${previewMode() === 'configured' ? 'active' : ''}`}
|
|
||||||
onClick={() => setPreviewMode('configured')}
|
|
||||||
>
|
|
||||||
Sample Preview
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`admin-segment ${previewMode() === 'live' ? 'active' : ''}`}
|
|
||||||
onClick={() => setPreviewMode('live')}
|
|
||||||
>
|
|
||||||
Live Preview
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={previewMode() === 'configured'}>
|
|
||||||
<DashboardPreview dashboard={selected()!} />
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={previewMode() === 'live'}>
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px">
|
|
||||||
<span class="meta-chip">Role: {livePreviewRoleLabel()}</span>
|
|
||||||
<Show when={selected()!.roleId}>
|
|
||||||
<span class="meta-chip">Role ID: {selected()!.roleId}</span>
|
|
||||||
</Show>
|
|
||||||
<a class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={livePreviewUrl()} target="_blank" rel="noreferrer">Open Full Page Preview</a>
|
|
||||||
</div>
|
|
||||||
<iframe
|
|
||||||
src={livePreviewUrl()}
|
|
||||||
title="Live Internal Dashboard Preview"
|
|
||||||
style="width:100%;height:760px;border:1px solid #e2e8f0;border-radius:14px;background:#fff"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,393 @@
|
||||||
import { onMount } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
import { MoreVertical } from 'lucide-solid';
|
||||||
|
|
||||||
|
const API = '/api/gateway';
|
||||||
|
|
||||||
|
type OnboardingFlow = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
userType: string;
|
||||||
|
totalSteps: number;
|
||||||
|
requiredDocs: number;
|
||||||
|
verificationType: string;
|
||||||
|
status: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FALLBACK_FLOWS: OnboardingFlow[] = [
|
||||||
|
{ id: 'f1', name: 'Customer Service Onboarding', userType: 'Customer', totalSteps: 6, requiredDocs: 2, verificationType: 'Identity Verification', status: 'ACTIVE', lastUpdated: '2024-03-20' },
|
||||||
|
{ id: 'f2', name: 'Professional Photographer Onboarding', userType: 'Professional', totalSteps: 6, requiredDocs: 3, verificationType: 'Identity Verification', status: 'ACTIVE', lastUpdated: '2024-03-19' },
|
||||||
|
{ id: 'f3', name: 'Company Hiring Onboarding', userType: 'Company', totalSteps: 6, requiredDocs: 4, verificationType: 'Business Verification', status: 'ACTIVE', lastUpdated: '2024-03-18' },
|
||||||
|
{ id: 'f4', name: 'Jobseeker Profile Onboarding', userType: 'Jobseeker', totalSteps: 6, requiredDocs: 2, verificationType: 'Identity Verification', status: 'ACTIVE', lastUpdated: '2024-03-17' },
|
||||||
|
{ id: 'f5', name: 'Professional Developer Onboarding', userType: 'Professional', totalSteps: 6, requiredDocs: 2, verificationType: 'Identity Verification', status: 'DRAFT', lastUpdated: '2024-03-16' },
|
||||||
|
{ id: 'f6', name: 'Customer Requirements Collection', userType: 'Customer', totalSteps: 5, requiredDocs: 1, verificationType: 'No Verification', status: 'INACTIVE', lastUpdated: '2024-03-15' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function loadFlows(): Promise<OnboardingFlow[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/api/admin/onboarding-schemas`);
|
||||||
|
if (!res.ok) {
|
||||||
|
const res2 = await fetch(`${API}/api/admin/onboarding-flows`);
|
||||||
|
if (!res2.ok) throw new Error('Failed');
|
||||||
|
const data2 = await res2.json();
|
||||||
|
const rows2 = Array.isArray(data2) ? data2 : (data2.flows || data2.schemas || []);
|
||||||
|
if (!rows2.length) return FALLBACK_FLOWS;
|
||||||
|
return rows2.map((item: any) => ({
|
||||||
|
id: String(item.id || ''),
|
||||||
|
name: String(item.name || item.title || 'Untitled Flow'),
|
||||||
|
userType: String(item.user_type || item.userType || item.role_key || ''),
|
||||||
|
totalSteps: Number(item.step_count || item.steps || item.totalSteps || 0),
|
||||||
|
requiredDocs: Number(item.required_docs || item.requiredDocs || 0),
|
||||||
|
verificationType: String(item.verification_type || item.verificationType || ''),
|
||||||
|
status: item.is_active ? 'ACTIVE' : String(item.status || 'DRAFT').toUpperCase(),
|
||||||
|
lastUpdated: String(item.updated_at || item.lastUpdated || '').slice(0, 10),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
const rows = Array.isArray(data) ? data : (data.flows || data.schemas || []);
|
||||||
|
if (!rows.length) return FALLBACK_FLOWS;
|
||||||
|
return rows.map((item: any) => ({
|
||||||
|
id: String(item.id || ''),
|
||||||
|
name: String(item.name || item.title || 'Untitled Flow'),
|
||||||
|
userType: String(item.user_type || item.userType || item.role_key || ''),
|
||||||
|
totalSteps: Number(item.step_count || item.steps || item.totalSteps || 0),
|
||||||
|
requiredDocs: Number(item.required_docs || item.requiredDocs || 0),
|
||||||
|
verificationType: String(item.verification_type || item.verificationType || ''),
|
||||||
|
status: item.is_active ? 'ACTIVE' : String(item.status || 'DRAFT').toUpperCase(),
|
||||||
|
lastUpdated: String(item.updated_at || item.lastUpdated || '').slice(0, 10),
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return FALLBACK_FLOWS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge(props: { status: string }) {
|
||||||
|
const s = props.status;
|
||||||
|
if (s === 'ACTIVE') {
|
||||||
|
return (
|
||||||
|
<span style="display:inline-flex;border-radius:9999px;border:1px solid #FFD8C2;background:#FFF1EB;color:#FF5E13;padding:2px 10px;font-size:12px;font-weight:500">
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (s === 'DRAFT') {
|
||||||
|
return (
|
||||||
|
<span style="display:inline-flex;border-radius:9999px;border:1px solid #FDE68A;background:#FFFBEB;color:#D97706;padding:2px 10px;font-size:12px;font-weight:500">
|
||||||
|
Draft
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span style="display:inline-flex;border-radius:9999px;border:1px solid #D1D5DB;background:#F3F4F6;color:#4B5563;padding:2px 10px;font-size:12px;font-weight:500">
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VerificationBadge(props: { type: string }) {
|
||||||
|
const t = props.type;
|
||||||
|
if (t === 'Identity Verification') {
|
||||||
|
return (
|
||||||
|
<span style="display:inline-flex;border-radius:9999px;border:1px solid #BFDBFE;background:#EFF6FF;color:#1D4ED8;padding:2px 10px;font-size:12px;font-weight:500">
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (t === 'Business Verification') {
|
||||||
|
return (
|
||||||
|
<span style="display:inline-flex;border-radius:9999px;border:1px solid #E9D5FF;background:#F5F3FF;color:#7C3AED;padding:2px 10px;font-size:12px;font-weight:500">
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span style="display:inline-flex;border-radius:9999px;border:1px solid #D1D5DB;background:#F3F4F6;color:#4B5563;padding:2px 10px;font-size:12px;font-weight:500">
|
||||||
|
{t || '—'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActionsMenu(props: { flowId: string; status: string }) {
|
||||||
|
const [open, setOpen] = createSignal(false);
|
||||||
|
return (
|
||||||
|
<div style="position:relative;display:inline-block">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
style="width:32px;height:32px;border-radius:6px;border:1px solid #E5E7EB;background:white;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:#6B7280"
|
||||||
|
>
|
||||||
|
<MoreVertical size={15} />
|
||||||
|
</button>
|
||||||
|
<Show when={open()}>
|
||||||
|
<div
|
||||||
|
style="position:absolute;right:0;top:36px;background:white;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,0.10);z-index:100;min-width:168px;padding:4px 0"
|
||||||
|
onMouseLeave={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
|
View Flow
|
||||||
|
</button>
|
||||||
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||||
|
Edit Flow
|
||||||
|
</button>
|
||||||
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||||
|
Duplicate Flow
|
||||||
|
</button>
|
||||||
|
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
|
||||||
|
<Show when={props.status !== 'ACTIVE'}>
|
||||||
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||||
|
Activate Flow
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={props.status === 'ACTIVE'}>
|
||||||
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
|
||||||
|
Deactivate Flow
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 14px;font-size:13px;color:#EF4444;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||||
|
Delete Flow
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OnboardingManagementPage() {
|
||||||
|
const [flows] = createResource(loadFlows);
|
||||||
|
const [search, setSearch] = createSignal('');
|
||||||
|
const [statusFilter, setStatusFilter] = createSignal('All');
|
||||||
|
const [userTypeFilter, setUserTypeFilter] = createSignal('All');
|
||||||
|
const [activeTab, setActiveTab] = createSignal<'flow' | 'preview'>('flow');
|
||||||
|
|
||||||
|
const allFlows = () => flows() || FALLBACK_FLOWS;
|
||||||
|
|
||||||
|
const filtered = createMemo(() => {
|
||||||
|
const q = search().toLowerCase();
|
||||||
|
const st = statusFilter();
|
||||||
|
const ut = userTypeFilter();
|
||||||
|
return allFlows().filter((f) => {
|
||||||
|
const matchesSearch = !q || f.name.toLowerCase().includes(q) || f.userType.toLowerCase().includes(q);
|
||||||
|
const matchesStatus = st === 'All' || f.status === st;
|
||||||
|
const matchesType = ut === 'All' || f.userType === ut;
|
||||||
|
return matchesSearch && matchesStatus && matchesType;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalFlows = () => allFlows().length;
|
||||||
|
const activeFlows = () => allFlows().filter((f) => f.status === 'ACTIVE').length;
|
||||||
|
const draftFlows = () => allFlows().filter((f) => f.status === 'DRAFT').length;
|
||||||
|
const requireVerification = () => allFlows().filter((f) => f.verificationType && f.verificationType !== 'No Verification').length;
|
||||||
|
const recentlyUpdated = () => allFlows().filter((f) => f.lastUpdated >= '2024-03-18').length;
|
||||||
|
|
||||||
|
const STATS = [
|
||||||
|
{ label: 'Total Flows', value: () => totalFlows(), color: '#111827' },
|
||||||
|
{ label: 'Active Flows', value: () => activeFlows(), color: '#10B981' },
|
||||||
|
{ label: 'Draft Flows', value: () => draftFlows(), color: '#F59E0B' },
|
||||||
|
{ label: 'Flows Requiring Verification', value: () => requireVerification(), color: '#FF5E13' },
|
||||||
|
{ label: 'Recently Updated', value: () => recentlyUpdated(), color: '#3B82F6' },
|
||||||
|
];
|
||||||
|
|
||||||
export default function OnboardingManagementAliasPage() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
onMount(() => navigate('/admin/onboarding-schemas', { replace: true }));
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to onboarding management...</p></div>
|
<div style="width:100%;padding-bottom:32px">
|
||||||
|
|
||||||
|
{/* Page Header */}
|
||||||
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:24px">
|
||||||
|
<div>
|
||||||
|
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0 0 4px 0">External Onboarding Management</h1>
|
||||||
|
<p style="font-size:14px;color:#6B7280;margin:0">Create and manage onboarding flows for external users.</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;border-radius:10px;overflow:hidden;border:1px solid #E5E7EB">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('flow')}
|
||||||
|
style={activeTab() === 'flow'
|
||||||
|
? 'background:#0D0D2A;color:white;padding:8px 16px;font-size:13px;font-weight:600;border:none;cursor:pointer;border-radius:0'
|
||||||
|
: 'background:white;color:#374151;padding:8px 16px;font-size:13px;font-weight:500;border-left:1px solid #E5E7EB;border:1px solid #E5E7EB;cursor:pointer'}
|
||||||
|
>
|
||||||
|
Flow Management
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('preview')}
|
||||||
|
style={activeTab() === 'preview'
|
||||||
|
? 'background:#0D0D2A;color:white;padding:8px 16px;font-size:13px;font-weight:600;border:none;cursor:pointer;border-radius:0'
|
||||||
|
: 'background:white;color:#374151;padding:8px 16px;font-size:13px;font-weight:500;border-left:1px solid #E5E7EB;border:1px solid #E5E7EB;cursor:pointer'}
|
||||||
|
>
|
||||||
|
User Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:24px">
|
||||||
|
<For each={STATS}>
|
||||||
|
{(stat) => (
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px">
|
||||||
|
<p style="font-size:12px;color:#6B7280;margin:0 0 8px 0">{stat.label}</p>
|
||||||
|
<p style={`font-size:28px;font-weight:700;color:${stat.color};margin:0`}>
|
||||||
|
{flows.loading ? '—' : stat.value()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flow Management View */}
|
||||||
|
<Show when={activeTab() === 'flow'}>
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
|
||||||
|
|
||||||
|
{/* Section Header */}
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0;padding:16px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
|
<h2 style="font-size:18px;font-weight:700;color:#111827;margin:0">Onboarding Flow Management</h2>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<button style="height:34px;padding:0 14px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer">
|
||||||
|
Import Flow
|
||||||
|
</button>
|
||||||
|
<button style="height:34px;padding:0 14px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer">
|
||||||
|
Export Flow
|
||||||
|
</button>
|
||||||
|
<A
|
||||||
|
href="/admin/onboarding-management/new"
|
||||||
|
style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px;text-decoration:none"
|
||||||
|
>
|
||||||
|
+ Create Onboarding Flow
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Bar */}
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:0;padding:12px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
|
<div style="position:relative;flex:1;max-width:280px">
|
||||||
|
<svg style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#9CA3AF" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search flows..."
|
||||||
|
value={search()}
|
||||||
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
style="height:34px;width:100%;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px 0 30px;font-size:13px;color:#111827;outline:none;box-sizing:border-box;background:white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={statusFilter()}
|
||||||
|
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||||
|
style="height:34px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="All">Status: All</option>
|
||||||
|
<option value="ACTIVE">Active</option>
|
||||||
|
<option value="DRAFT">Draft</option>
|
||||||
|
<option value="INACTIVE">Inactive</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={userTypeFilter()}
|
||||||
|
onChange={(e) => setUserTypeFilter(e.currentTarget.value)}
|
||||||
|
style="height:34px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="All">User Type: All</option>
|
||||||
|
<option value="Customer">Customer</option>
|
||||||
|
<option value="Professional">Professional</option>
|
||||||
|
<option value="Company">Company</option>
|
||||||
|
<option value="Jobseeker">Jobseeker</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div style="overflow-x:auto">
|
||||||
|
<table style="width:100%;border-collapse:collapse;min-width:960px">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#0D0D2A">
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">FLOW NAME</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">USER TYPE</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">TOTAL STEPS</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">REQUIRED DOCUMENTS</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">VERIFICATION TYPE</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">STATUS</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">LAST UPDATED</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">ACTIONS</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Show when={flows.loading}>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">Loading flows...</td>
|
||||||
|
</tr>
|
||||||
|
</Show>
|
||||||
|
<Show when={!flows.loading && filtered().length === 0}>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" style="padding:32px 20px;text-align:center;color:#9CA3AF;font-size:14px">No onboarding flows found.</td>
|
||||||
|
</tr>
|
||||||
|
</Show>
|
||||||
|
<For each={filtered()}>
|
||||||
|
{(flow) => (
|
||||||
|
<tr style="border-bottom:1px solid #F3F4F6">
|
||||||
|
<td style="padding:12px 20px;font-size:14px;color:#111827;font-weight:600">{flow.name}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:14px;color:#374151">{flow.userType}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:14px;color:#374151">{flow.totalSteps}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:14px;color:#374151">{flow.requiredDocs}</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<VerificationBadge type={flow.verificationType} />
|
||||||
|
</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<StatusBadge status={flow.status} />
|
||||||
|
</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{flow.lastUpdated || '—'}</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<ActionsMenu flowId={flow.id} status={flow.status} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;border-top:1px solid #F3F4F6;padding:12px 20px">
|
||||||
|
<span style="font-size:13px;color:#6B7280">
|
||||||
|
Showing {filtered().length} of {allFlows().length} flows
|
||||||
|
</span>
|
||||||
|
<div style="display:flex;gap:4px">
|
||||||
|
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:white;border-radius:6px;font-size:13px;color:#374151;cursor:pointer;padding:0 10px">
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:#0D0D2A;border-radius:6px;font-size:13px;color:white;cursor:pointer;padding:0 10px">
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
<button style="height:32px;min-width:32px;border:1px solid #E5E7EB;background:white;border-radius:6px;font-size:13px;color:#374151;cursor:pointer;padding:0 10px">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* User Preview View */}
|
||||||
|
<Show when={activeTab() === 'preview'}>
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:32px;text-align:center">
|
||||||
|
<div style="margin-bottom:24px">
|
||||||
|
<label style="font-size:13px;font-weight:500;color:#374151;display:block;margin-bottom:8px">
|
||||||
|
Select a flow to preview
|
||||||
|
</label>
|
||||||
|
<select style="height:36px;border:1px solid #E5E7EB;border-radius:8px;padding:0 12px;font-size:13px;color:#374151;background:white;outline:none;cursor:pointer;min-width:280px">
|
||||||
|
<option value="">— Choose a flow —</option>
|
||||||
|
<For each={allFlows()}>
|
||||||
|
{(f) => <option value={f.id}>{f.name}</option>}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="padding:48px 0;color:#9CA3AF">
|
||||||
|
<svg style="margin:0 auto 16px;display:block;color:#D1D5DB" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||||
|
<p style="font-size:14px;color:#9CA3AF;margin:0">Select a flow above to see a user-facing preview.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
</div>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,357 +1,463 @@
|
||||||
import { A, useNavigate } from '@solidjs/router';
|
import { For, Show, createSignal } from 'solid-js';
|
||||||
import { createResource, createSignal, For, Show } from 'solid-js';
|
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
import { ArrowUpDown, Filter, Download, Eye, Pencil } from 'lucide-solid';
|
import { ChevronDown, SlidersHorizontal, Download, MoreVertical } from 'lucide-solid';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
|
||||||
|
|
||||||
type Role = {
|
type Role = {
|
||||||
id: string;
|
id: string;
|
||||||
key: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
audience: string;
|
department: string;
|
||||||
description?: string;
|
usersAssigned: number;
|
||||||
department_name?: string;
|
permissionsCount: number;
|
||||||
is_active: boolean;
|
status: 'ACTIVE' | 'INACTIVE';
|
||||||
users_assigned: number;
|
createdDate: string;
|
||||||
permissions_count: number;
|
|
||||||
created_at: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ListResponse = { roles: Role[]; total: number; page: number; per_page: number };
|
|
||||||
|
|
||||||
const FALLBACK_ROLES: Role[] = [
|
const FALLBACK_ROLES: Role[] = [
|
||||||
{ id: 'r1', key: 'ADM-SYS-001', name: 'System Administrator', audience: 'INTERNAL', department_name: 'Information Technology', is_active: true, users_assigned: 12, permissions_count: 0, created_at: '2024-01-12' },
|
{ id: 'r1', name: 'Engineering Lead', department: 'Engineering', usersAssigned: 12, permissionsCount: 28, status: 'ACTIVE', createdDate: '2026-01-15' },
|
||||||
{ id: 'r2', key: 'FIN-MGR-002', name: 'Finance Manager', audience: 'INTERNAL', department_name: 'Accounting & Finance', is_active: true, users_assigned: 4, permissions_count: 42, created_at: '2024-02-05' },
|
{ id: 'r2', name: 'Marketing Manager', department: 'Marketing', usersAssigned: 8, permissionsCount: 18, status: 'ACTIVE', createdDate: '2026-01-20' },
|
||||||
{ id: 'r3', key: 'HR-COOR-003', name: 'HR Coordinator', audience: 'INTERNAL', department_name: 'Human Resources', is_active: false, users_assigned: 8, permissions_count: 28, created_at: '2024-03-18' },
|
{ id: 'r3', name: 'Sales Director', department: 'Sales', usersAssigned: 15, permissionsCount: 32, status: 'ACTIVE', createdDate: '2026-02-01' },
|
||||||
{ id: 'r4', key: 'ENG-LEAD-004', name: 'Lead Developer', audience: 'INTERNAL', department_name: 'Engineering', is_active: true, users_assigned: 25, permissions_count: 64, created_at: '2024-04-02' },
|
{ id: 'r4', name: 'HR Admin', department: 'Human Resources', usersAssigned: 5, permissionsCount: 24, status: 'ACTIVE', createdDate: '2026-02-05' },
|
||||||
{ id: 'r5', key: 'SLS-ASSC-005', name: 'Sales Associate', audience: 'INTERNAL', department_name: 'Growth & Sales', is_active: true, users_assigned: 48, permissions_count: 15, created_at: '2024-05-11' },
|
{ id: 'r5', name: 'Finance Controller', department: 'Finance', usersAssigned: 6, permissionsCount: 20, status: 'ACTIVE', createdDate: '2026-02-10' },
|
||||||
|
{ id: 'r6', name: 'Operations Head', department: 'Operations', usersAssigned: 4, permissionsCount: 16, status: 'INACTIVE', createdDate: '2026-03-01' },
|
||||||
|
{ id: 'r7', name: 'Support Lead', department: 'Customer Support', usersAssigned: 9, permissionsCount: 16, status: 'ACTIVE', createdDate: '2026-03-05' },
|
||||||
|
{ id: 'r8', name: 'Product Owner', department: 'Product', usersAssigned: 7, permissionsCount: 26, status: 'ACTIVE', createdDate: '2026-03-10' },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function loadRoles(params: { q: string; page: number }): Promise<ListResponse> {
|
const MODULES = [
|
||||||
try {
|
'Department Management',
|
||||||
const qs = new URLSearchParams({ audience: 'INTERNAL', q: params.q, page: String(params.page), per_page: '10' });
|
'Designation Management',
|
||||||
const res = await fetch(`${API}/api/admin/roles?${qs}`);
|
'Internal Role Management',
|
||||||
if (!res.ok) throw new Error('Failed');
|
'Employee Management',
|
||||||
const data = await res.json();
|
'External Role Management',
|
||||||
if (data.roles?.length > 0) return data;
|
'External Onboarding Management',
|
||||||
throw new Error('empty');
|
'Internal Dashboard Management',
|
||||||
} catch {
|
'External Dashboard Management',
|
||||||
const q = params.q.toLowerCase();
|
'Verification Management',
|
||||||
const filtered = q ? FALLBACK_ROLES.filter((r) => r.name.toLowerCase().includes(q) || r.key.toLowerCase().includes(q)) : FALLBACK_ROLES;
|
'Approval Management',
|
||||||
return { roles: filtered, total: filtered.length, page: 1, per_page: 10 };
|
'Users Management',
|
||||||
|
'Company Management',
|
||||||
|
'Candidate Management',
|
||||||
|
'Customer Management',
|
||||||
|
'Jobs Management',
|
||||||
|
'Leads Management',
|
||||||
|
'Pricing Management',
|
||||||
|
'Credit Management',
|
||||||
|
'Coupon Management',
|
||||||
|
'Discount Management',
|
||||||
|
'Tax Management',
|
||||||
|
'Order Management',
|
||||||
|
'Invoice Management',
|
||||||
|
'Review Management',
|
||||||
|
'Support Management',
|
||||||
|
'Report Management',
|
||||||
|
'Ledger Management',
|
||||||
|
];
|
||||||
|
|
||||||
|
type PermKey = 'view' | 'create' | 'update' | 'delete';
|
||||||
|
const PERM_KEYS: PermKey[] = ['view', 'create', 'update', 'delete'];
|
||||||
|
|
||||||
|
type ModulePerms = Record<PermKey, boolean>;
|
||||||
|
type PermissionsMap = Record<string, ModulePerms>;
|
||||||
|
|
||||||
|
function defaultPerms(): PermissionsMap {
|
||||||
|
const map: PermissionsMap = {};
|
||||||
|
for (const m of MODULES) {
|
||||||
|
map[m] = { view: false, create: false, update: false, delete: false };
|
||||||
}
|
}
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(iso: string) {
|
function StatusBadge(props: { status: string }) {
|
||||||
try {
|
const active = () => props.status === 'ACTIVE';
|
||||||
const d = new Date(iso);
|
|
||||||
return d.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
|
|
||||||
} catch { return '—'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusBadge(props: { active: boolean }) {
|
|
||||||
return (
|
return (
|
||||||
<span class={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-[12px] font-medium ${
|
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
|
||||||
props.active
|
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
|
||||||
? 'border-[#FF5E13] bg-[#FFF3EE] text-[#FF5E13]'
|
{active() ? 'Active' : 'Inactive'}
|
||||||
: 'border-[#D1D5DB] bg-[#F9FAFB] text-[#6B7280]'
|
|
||||||
}`}>
|
|
||||||
<span class={`h-1.5 w-1.5 rounded-full ${props.active ? 'bg-[#FF5E13]' : 'bg-[#9CA3AF]'}`} />
|
|
||||||
{props.active ? 'Active' : 'Inactive'}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UsersBadge(props: { count: number }) {
|
function Toggle(props: { on: boolean; onChange: (v: boolean) => void }) {
|
||||||
return (
|
return (
|
||||||
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full bg-[#EEF2FF] text-[12px] font-semibold text-[#4F46E5]">
|
<button
|
||||||
{props.count > 99 ? '99+' : String(props.count).padStart(2, '0')}
|
type="button"
|
||||||
</span>
|
onClick={() => props.onChange(!props.on)}
|
||||||
|
style={`width:40px;height:22px;border-radius:11px;background:${props.on ? '#FF5E13' : '#E5E7EB'};position:relative;cursor:pointer;border:none;padding:0;transition:background 0.2s`}
|
||||||
|
>
|
||||||
|
<span style={`position:absolute;width:18px;height:18px;border-radius:50%;background:white;top:2px;transition:left 0.2s;left:${props.on ? '20px' : '2px'}`} />
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABS = [
|
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string }) {
|
||||||
{ label: 'All Roles', href: '/admin/roles' },
|
return (
|
||||||
{ label: 'Create Role', href: '/admin/roles/create' },
|
<label style="display:block">
|
||||||
{ label: 'View Role', href: '/admin/roles/view' },
|
<span style="font-size:13px;font-weight:600;color:#374151">
|
||||||
{ label: 'Edit Role', href: '/admin/roles/edit' },
|
{props.label}{props.required && <span style="color:#FF5E13;margin-left:2px">*</span>}
|
||||||
];
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={props.value}
|
||||||
|
onInput={(e) => props.onInput(e.currentTarget.value)}
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;padding:0 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function InternalRolesListPage() {
|
export default function RolesPage() {
|
||||||
const navigate = useNavigate();
|
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
|
||||||
|
const [formTab, setFormTab] = createSignal<'general' | 'access' | 'settings'>('general');
|
||||||
|
|
||||||
|
// All Roles state
|
||||||
const [search, setSearch] = createSignal('');
|
const [search, setSearch] = createSignal('');
|
||||||
const [debouncedSearch, setDebouncedSearch] = createSignal('');
|
|
||||||
const [page, setPage] = createSignal(1);
|
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout>;
|
// Create Role state
|
||||||
const handleSearch = (val: string) => {
|
const [roleName, setRoleName] = createSignal('');
|
||||||
setSearch(val);
|
const [roleCode, setRoleCode] = createSignal('');
|
||||||
clearTimeout(debounceTimer);
|
const [department, setDepartment] = createSignal('');
|
||||||
debounceTimer = setTimeout(() => { setDebouncedSearch(val); setPage(1); }, 300);
|
const [description, setDescription] = createSignal('');
|
||||||
};
|
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
|
||||||
|
const [approveRequests, setApproveRequests] = createSignal(true);
|
||||||
|
const [manageSettings, setManageSettings] = createSignal(false);
|
||||||
|
const [permissions, setPermissions] = createSignal<PermissionsMap>(defaultPerms());
|
||||||
|
|
||||||
const [data] = createResource(
|
const filteredRoles = () => {
|
||||||
() => ({ q: debouncedSearch(), page: page() }),
|
const q = search().toLowerCase();
|
||||||
loadRoles,
|
if (!q) return FALLBACK_ROLES;
|
||||||
|
return FALLBACK_ROLES.filter(
|
||||||
|
(r) => r.name.toLowerCase().includes(q) || r.department.toLowerCase().includes(q)
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalPages = () => {
|
|
||||||
const d = data();
|
|
||||||
if (!d || d.per_page === 0) return 1;
|
|
||||||
return Math.max(1, Math.ceil(d.total / d.per_page));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const roles = () => data()?.roles ?? [];
|
const togglePerm = (mod: string, key: PermKey) => {
|
||||||
|
setPermissions((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[mod]: { ...prev[mod], [key]: !prev[mod][key] },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectAll = (mod: string) => {
|
||||||
|
const p = permissions()[mod];
|
||||||
|
const allOn = PERM_KEYS.every((k) => p[k]);
|
||||||
|
setPermissions((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[mod]: { view: !allOn, create: !allOn, update: !allOn, delete: !allOn },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (d: string) => {
|
||||||
|
if (!d) return '—';
|
||||||
|
const dt = new Date(d);
|
||||||
|
return dt.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const shown = () => filteredRoles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div class="w-full space-y-6 pb-8">
|
<div style="padding:24px">
|
||||||
|
{/* Page Header */}
|
||||||
{/* Page header */}
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Internal Role Management</h1>
|
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Internal Role Management</h1>
|
||||||
<p class="mt-1 text-[14px] text-[#6B7280]">Define and manage organizational access levels with granular permission control.</p>
|
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage internal roles and permissions</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Main Tabs */}
|
||||||
<div class="flex items-center gap-6 border-b border-[#E5E7EB]">
|
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
|
||||||
<For each={TABS}>
|
<button
|
||||||
{(tab) => {
|
type="button"
|
||||||
const active = () => tab.href === '/admin/roles';
|
onClick={() => setMainTab('all')}
|
||||||
return (
|
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${mainTab() === 'all' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:none'}`}
|
||||||
<A
|
|
||||||
href={tab.href}
|
|
||||||
class={`pb-3 text-[14px] font-medium transition-colors ${
|
|
||||||
active()
|
|
||||||
? 'border-b-2 border-[#FF5E13] text-[#FF5E13]'
|
|
||||||
: 'text-[#6B7280] hover:text-[#111827]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
All Roles
|
||||||
</A>
|
</button>
|
||||||
);
|
<button
|
||||||
}}
|
type="button"
|
||||||
</For>
|
onClick={() => setMainTab('create')}
|
||||||
|
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${mainTab() === 'create' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:none'}`}
|
||||||
|
>
|
||||||
|
Create Role
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table card */}
|
{/* ── ALL ROLES TAB ── */}
|
||||||
<div class="overflow-hidden rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
|
<Show when={mainTab() === 'all'}>
|
||||||
|
{/* Edge-to-edge card */}
|
||||||
{/* Filter bar */}
|
<div style="margin-top:20px;margin-left:-24px;margin-right:-24px;border-radius:0;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white">
|
||||||
<div class="flex items-center gap-2 px-5 py-4">
|
{/* Filter Bar */}
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Filter by role name or code..."
|
placeholder="Search roles..."
|
||||||
value={search()}
|
value={search()}
|
||||||
onInput={(e) => handleSearch(e.currentTarget.value)}
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||||
class="h-[34px] flex-1 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.08)] transition-colors"
|
style="height:34px;flex:1;max-width:240px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827"
|
||||||
/>
|
/>
|
||||||
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[12px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors">
|
<button type="button" style="height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer;display:inline-flex;align-items:center;gap:4px">
|
||||||
<ArrowUpDown size={13} />
|
<ChevronDown size={14} />
|
||||||
Sort
|
Sort
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[12px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors">
|
<button type="button" style="height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer;display:inline-flex;align-items:center;gap:4px">
|
||||||
<Filter size={13} />
|
<SlidersHorizontal size={14} />
|
||||||
Filters
|
Filters
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg bg-[#0D0D2A] px-3 text-[12px] font-semibold text-white hover:bg-[#1a1a3e] transition-colors">
|
<button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px">
|
||||||
<Download size={13} />
|
<Download size={14} />
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div class="overflow-x-auto">
|
<div style="overflow-x:auto">
|
||||||
<table class="min-w-full">
|
<table style="width:100%;border-collapse:collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-[#0D0D2A] text-left">
|
<tr style="background:#0D0D2A">
|
||||||
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Role Name</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Role Name</th>
|
||||||
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Role Code</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Department</th>
|
||||||
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Department</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Users Assigned</th>
|
||||||
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Users Assigned</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Permissions Count</th>
|
||||||
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Permissions</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Status</th>
|
||||||
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Status</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Created Date</th>
|
||||||
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Created Date</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap"></th>
|
||||||
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">AC</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-[#F3F4F6]">
|
<tbody>
|
||||||
<Show when={data.loading}>
|
<For each={shown()}>
|
||||||
<For each={[0, 1, 2, 3, 4]}>
|
|
||||||
{() => (
|
|
||||||
<tr class="animate-pulse">
|
|
||||||
<td class="px-5 py-3.5"><div class="h-4 w-36 rounded-lg bg-[#F3F4F6]" /></td>
|
|
||||||
<td class="px-5 py-3.5"><div class="h-4 w-20 rounded-lg bg-[#F3F4F6]" /></td>
|
|
||||||
<td class="px-5 py-3.5"><div class="h-4 w-28 rounded-lg bg-[#F3F4F6]" /></td>
|
|
||||||
<td class="px-5 py-3.5"><div class="h-7 w-7 rounded-full bg-[#F3F4F6]" /></td>
|
|
||||||
<td class="px-5 py-3.5"><div class="h-4 w-20 rounded-lg bg-[#F3F4F6]" /></td>
|
|
||||||
<td class="px-5 py-3.5"><div class="h-6 w-20 rounded-full bg-[#F3F4F6]" /></td>
|
|
||||||
<td class="px-5 py-3.5"><div class="h-4 w-24 rounded-lg bg-[#F3F4F6]" /></td>
|
|
||||||
<td class="px-5 py-3.5"><div class="h-4 w-12 rounded-lg bg-[#F3F4F6]" /></td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
<Show when={!data.loading && roles().length === 0}>
|
|
||||||
<tr>
|
|
||||||
<td colspan="8" class="px-6 py-16 text-center">
|
|
||||||
<p class="text-[15px] font-semibold text-[#111827]">No roles found</p>
|
|
||||||
<p class="mt-1 text-[13px] text-[#6B7280]">Create your first internal role to control admin access.</p>
|
|
||||||
<A href="/admin/roles/create" class="mt-4 inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-4 py-2 text-[13px] font-semibold text-white">
|
|
||||||
Create Role
|
|
||||||
</A>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</Show>
|
|
||||||
<Show when={!data.loading}>
|
|
||||||
<For each={roles()}>
|
|
||||||
{(role) => (
|
{(role) => (
|
||||||
<tr class="hover:bg-[#FAFAFA] transition-colors">
|
<tr style="border-bottom:1px solid #F3F4F6">
|
||||||
<td class="px-5 py-3.5">
|
<td style="padding:12px 20px">
|
||||||
<p class="text-[14px] font-bold text-[#111827]">{role.name}</p>
|
<span style="font-size:14px;font-weight:600;color:#111827">{role.name}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-5 py-3.5">
|
<td style="padding:12px 20px">
|
||||||
<div class="text-[11px] font-mono leading-[1.6] text-[#9CA3AF]">
|
<span style="font-size:14px;color:#374151">{role.department}</span>
|
||||||
{role.key.split('-').map((seg, i, arr) => (
|
|
||||||
<span class="block">{seg}{i < arr.length - 1 ? '-' : ''}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-5 py-3.5 text-[13px] text-[#374151]">{role.department_name || '—'}</td>
|
<td style="padding:12px 20px">
|
||||||
<td class="px-5 py-3.5">
|
<span style="font-size:14px;font-weight:600;color:#111827">{role.usersAssigned}</span>
|
||||||
<UsersBadge count={role.users_assigned} />
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-5 py-3.5">
|
<td style="padding:12px 20px">
|
||||||
<Show when={role.permissions_count === 0} fallback={
|
<span style="display:inline-flex;border-radius:8px;background:#FFF1EB;border:1px solid #FFD8C2;color:#FF5E13;padding:2px 10px;font-size:12px;font-weight:600">
|
||||||
<span class="text-[13px] text-[#374151]">{role.permissions_count} Controls</span>
|
{role.permissionsCount} Permissions
|
||||||
}>
|
</span>
|
||||||
<span class="text-[13px] font-semibold text-[#FF5E13]">All Access</span>
|
|
||||||
</Show>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-5 py-3.5">
|
<td style="padding:12px 20px">
|
||||||
<StatusBadge active={role.is_active} />
|
<StatusBadge status={role.status} />
|
||||||
</td>
|
</td>
|
||||||
<td class="px-5 py-3.5 text-[13px] text-[#6B7280]">{formatDate(role.created_at)}</td>
|
<td style="padding:12px 20px">
|
||||||
<td class="px-5 py-3.5">
|
<span style="font-size:13px;color:#6B7280">{formatDate(role.createdDate)}</span>
|
||||||
<div class="flex items-center gap-3">
|
</td>
|
||||||
<button
|
<td style="padding:12px 20px;text-align:center">
|
||||||
type="button"
|
<button type="button" style="background:none;border:none;cursor:pointer;color:#6B7280;display:inline-flex;align-items:center">
|
||||||
onClick={() => navigate(`/admin/roles/${role.id}`)}
|
<MoreVertical size={16} />
|
||||||
class="text-[#9CA3AF] hover:text-[#374151] transition-colors"
|
|
||||||
aria-label="View role"
|
|
||||||
>
|
|
||||||
<Eye size={17} />
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => navigate(`/admin/roles/${role.id}/edit`)}
|
|
||||||
class="text-[#9CA3AF] hover:text-[#FF5E13] transition-colors"
|
|
||||||
aria-label="Edit role"
|
|
||||||
>
|
|
||||||
<Pencil size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<Show when={!data.loading && roles().length > 0}>
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-top:1px solid #F3F4F6">
|
||||||
<div class="flex items-center justify-between border-t border-[#F3F4F6] px-6 py-4">
|
<span style="font-size:13px;color:#6B7280">
|
||||||
<p class="text-[13px] text-[#6B7280]">
|
Showing 1–{shown().length} of {shown().length} roles
|
||||||
Showing <span class="font-semibold text-[#111827]">{roles().length}</span> of <span class="font-semibold text-[#111827]">{data()?.total ?? 0}</span> roles
|
</span>
|
||||||
</p>
|
<div style="display:flex;align-items:center;gap:4px">
|
||||||
<div class="flex items-center gap-1.5">
|
<button type="button" style="height:30px;min-width:30px;padding:0 10px;border-radius:6px;border:1px solid #E5E7EB;background:#FF5E13;color:white;font-size:13px;font-weight:600;cursor:pointer">1</button>
|
||||||
<button
|
</div>
|
||||||
type="button"
|
|
||||||
disabled={page() === 1}
|
|
||||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-[16px]"
|
|
||||||
>‹</button>
|
|
||||||
<For each={Array.from({ length: Math.min(totalPages(), 5) }, (_, i) => i + 1)}>
|
|
||||||
{(p) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setPage(p)}
|
|
||||||
class={`inline-flex h-8 w-8 items-center justify-center rounded-lg text-[13px] font-medium transition-colors ${
|
|
||||||
page() === p
|
|
||||||
? 'bg-[#FF5E13] text-white'
|
|
||||||
: 'border border-[#E5E7EB] text-[#374151] hover:bg-[#F9FAFB]'
|
|
||||||
}`}
|
|
||||||
>{p}</button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={page() >= totalPages()}
|
|
||||||
onClick={() => setPage((p) => Math.min(totalPages(), p + 1))}
|
|
||||||
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-[16px]"
|
|
||||||
>›</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
</div>
|
{/* ── CREATE ROLE TAB ── */}
|
||||||
|
<Show when={mainTab() === 'create'}>
|
||||||
{/* Bottom stats row */}
|
<div style="margin-top:20px;border-radius:16px;border:1px solid #E5E7EB;background:white;overflow:hidden">
|
||||||
<div class="grid grid-cols-2 gap-5">
|
{/* Form Sub-tabs */}
|
||||||
|
<div style="display:flex;align-items:center;gap:0;border-bottom:1px solid #E5E7EB;padding:0 24px">
|
||||||
{/* Distribution card */}
|
{(
|
||||||
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
[
|
||||||
<div class="flex items-center justify-between mb-5">
|
{ key: 'general', label: 'General Information' },
|
||||||
<p class="text-[15px] font-semibold text-[#111827]">Distribution</p>
|
{ key: 'access', label: 'Module Access' },
|
||||||
<button type="button" class="text-[#9CA3AF] hover:text-[#374151]">
|
{ key: 'settings', label: 'Role Settings' },
|
||||||
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
] as const
|
||||||
|
).map(({ key, label }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormTab(key)}
|
||||||
|
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;cursor:pointer;${formTab() === key ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;font-weight:400'}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="flex items-end gap-3 h-[100px]">
|
|
||||||
{[
|
|
||||||
{ h: 55, color: '#FFD4C2' },
|
|
||||||
{ h: 80, color: '#FF5E13' },
|
|
||||||
{ h: 45, color: '#FFD4C2' },
|
|
||||||
{ h: 65, color: '#FFD4C2' },
|
|
||||||
{ h: 38, color: '#FFD4C2' },
|
|
||||||
].map((bar) => (
|
|
||||||
<div class="flex-1 rounded-t-md" style={{ height: `${bar.h}%`, background: bar.color }} />
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-[12px] text-[#6B7280]">
|
|
||||||
Most users are concentrated in <span class="font-semibold text-[#FF5E13]">Sales</span> and{' '}
|
|
||||||
<span class="font-semibold text-[#FF5E13]">Engineering</span> departments.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Audit Readiness Score card */}
|
{/* ── General Information ── */}
|
||||||
<div class="flex items-center justify-between rounded-2xl bg-[#0D0D2A] p-8 shadow-sm">
|
<Show when={formTab() === 'general'}>
|
||||||
<div class="flex-1 pr-8">
|
<div style="padding:24px">
|
||||||
<p class="text-[18px] font-bold text-white">Audit Readiness Score</p>
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||||
<p class="mt-2 text-[13px] leading-relaxed text-[#8AACC8]">
|
<FormInput
|
||||||
Your organizational permissions currently align with 94% of compliance standards. Review inactive roles to reach 100%.
|
label="Role Name"
|
||||||
|
required
|
||||||
|
value={roleName()}
|
||||||
|
onInput={setRoleName}
|
||||||
|
placeholder="e.g. Engineering Lead"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
label="Role Code"
|
||||||
|
required
|
||||||
|
value={roleCode()}
|
||||||
|
onInput={setRoleCode}
|
||||||
|
placeholder="e.g. ENG_LEAD"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:20px">
|
||||||
|
<label style="display:block">
|
||||||
|
<span style="font-size:13px;font-weight:600;color:#374151">
|
||||||
|
Department<span style="color:#FF5E13;margin-left:2px">*</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={department()}
|
||||||
|
onInput={(e) => setDepartment(e.currentTarget.value)}
|
||||||
|
placeholder="e.g. Engineering"
|
||||||
|
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;padding:0 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:20px">
|
||||||
|
<label style="display:block">
|
||||||
|
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
|
||||||
|
<textarea
|
||||||
|
value={description()}
|
||||||
|
onInput={(e) => setDescription(e.currentTarget.value)}
|
||||||
|
placeholder="Describe this role's responsibilities..."
|
||||||
|
style="display:block;margin-top:6px;height:100px;width:100%;border-radius:10px;border:1px solid #E5E7EB;padding:10px 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white;resize:vertical;font-family:inherit"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* ── Module Access ── */}
|
||||||
|
<Show when={formTab() === 'access'}>
|
||||||
|
<div style="padding:24px">
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin-top:0;margin-bottom:16px">
|
||||||
|
Configure module access permissions for this role.
|
||||||
</p>
|
</p>
|
||||||
<button type="button" class="mt-5 inline-flex h-10 items-center rounded-xl bg-[#FF5E13] px-5 text-[13px] font-semibold text-white hover:bg-[#e54d0a] transition-colors">
|
<div style="overflow-x:auto">
|
||||||
Review Audit Log
|
<table style="width:100%;border-collapse:collapse;border-radius:12px;overflow:hidden;border:1px solid #E5E7EB">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#0D0D2A">
|
||||||
|
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:left">Module Name</th>
|
||||||
|
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">View</th>
|
||||||
|
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Create</th>
|
||||||
|
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Update</th>
|
||||||
|
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Delete</th>
|
||||||
|
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Select All</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<For each={MODULES}>
|
||||||
|
{(mod) => {
|
||||||
|
const p = () => permissions()[mod];
|
||||||
|
const allOn = () => PERM_KEYS.every((k) => p()[k]);
|
||||||
|
return (
|
||||||
|
<tr style="border-bottom:1px solid #F3F4F6">
|
||||||
|
<td style="padding:12px 16px;font-size:13px;color:#111827;font-weight:500;text-align:left">{mod}</td>
|
||||||
|
<For each={PERM_KEYS}>
|
||||||
|
{(key) => (
|
||||||
|
<td style="text-align:center;padding:12px">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={p()[key]}
|
||||||
|
onChange={() => togglePerm(mod, key)}
|
||||||
|
style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<td style="text-align:center;padding:12px">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={allOn()}
|
||||||
|
onChange={() => toggleSelectAll(mod)}
|
||||||
|
style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* ── Role Settings ── */}
|
||||||
|
<Show when={formTab() === 'settings'}>
|
||||||
|
<div style="padding:24px;display:flex;flex-direction:column;gap:24px">
|
||||||
|
{/* Role Status */}
|
||||||
|
<div>
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 10px 0">Role Status</p>
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStatus('ACTIVE')}
|
||||||
|
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${status() === 'ACTIVE' ? 'border:1px solid #FF5E13;background:#FF5E13;color:white' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
|
||||||
|
>
|
||||||
|
Active
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStatus('INACTIVE')}
|
||||||
|
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${status() === 'INACTIVE' ? 'border:1px solid #6B7280;background:#6B7280;color:white' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
|
||||||
|
>
|
||||||
|
Inactive
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex h-[100px] w-[100px] shrink-0 items-center justify-center">
|
|
||||||
<svg class="h-full w-full -rotate-90" viewBox="0 0 100 100">
|
|
||||||
<circle cx="50" cy="50" r="42" fill="none" stroke="#1E2D3D" stroke-width="10" />
|
|
||||||
<circle cx="50" cy="50" r="42" fill="none" stroke="#FF5E13" stroke-width="10"
|
|
||||||
stroke-dasharray={`${2 * Math.PI * 42 * 0.94} ${2 * Math.PI * 42}`}
|
|
||||||
stroke-linecap="round" />
|
|
||||||
</svg>
|
|
||||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
|
||||||
<span class="text-[22px] font-bold text-white">94%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle: Approve Requests */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:14px;font-weight:600;color:#111827;margin:0">Allow Role to Approve Requests</p>
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:4px 0 0 0">Grant this role the ability to approve or reject submitted requests.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Toggle on={approveRequests()} onChange={setApproveRequests} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle: Manage Settings */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:14px;font-weight:600;color:#111827;margin:0">Allow Role to Manage System Settings</p>
|
||||||
|
<p style="font-size:13px;color:#6B7280;margin:4px 0 0 0">Grant this role access to configure and modify system-wide settings.</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={manageSettings()} onChange={setManageSettings} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Form Footer */}
|
||||||
|
<div style="display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #E5E7EB">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMainTab('all')}
|
||||||
|
style="height:38px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;border:1px solid #E5E7EB;background:white;color:#374151;cursor:pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style="background:#0D0D2A;color:white;height:38px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
Create Role
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,173 +1,318 @@
|
||||||
import { A } from '@solidjs/router';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
import { createMemo, createResource, For, Show } from 'solid-js';
|
|
||||||
import { Search } from 'lucide-solid';
|
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
import { Clock } from 'lucide-solid';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '/api/gateway';
|
||||||
|
|
||||||
type ApprovalRow = {
|
type VerificationStatus = 'PENDING' | 'VERIFIED' | 'FLAGGED' | 'RE_UPLOAD';
|
||||||
|
|
||||||
|
type VerificationRow = {
|
||||||
id: string;
|
id: string;
|
||||||
requestType?: string;
|
verificationId?: string;
|
||||||
type?: string;
|
name?: string;
|
||||||
requestStatus?: string;
|
|
||||||
status?: string;
|
|
||||||
requester?: { name?: string; email?: string };
|
|
||||||
requesterName?: string;
|
requesterName?: string;
|
||||||
requesterEmail?: string;
|
requester?: { name?: string; email?: string };
|
||||||
|
userType?: string;
|
||||||
|
verificationType?: string;
|
||||||
|
type?: string;
|
||||||
|
submittedDate?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
|
documents?: number;
|
||||||
|
status?: string;
|
||||||
|
requestStatus?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function fetchApprovals(): Promise<ApprovalRow[]> {
|
const FALLBACK: VerificationRow[] = [
|
||||||
|
{ id: 'v1', verificationId: 'VER-2024-001', name: 'Rajesh Kumar', userType: 'Professional', verificationType: 'Identity Verification', submittedDate: '2024-03-20', documents: 3, status: 'PENDING' },
|
||||||
|
{ id: 'v2', verificationId: 'VER-2024-002', name: 'Priya Sharma', userType: 'Company', verificationType: 'Business Verification', submittedDate: '2024-03-19', documents: 5, status: 'VERIFIED' },
|
||||||
|
{ id: 'v3', verificationId: 'VER-2024-003', name: 'Anil Patel', userType: 'Customer', verificationType: 'Profile Verification', submittedDate: '2024-03-18', documents: 2, status: 'PENDING' },
|
||||||
|
{ id: 'v4', verificationId: 'VER-2024-004', name: 'Meera Singh', userType: 'Jobseeker', verificationType: 'Document Verification', submittedDate: '2024-03-17', documents: 4, status: 'RE_UPLOAD' },
|
||||||
|
{ id: 'v5', verificationId: 'VER-2024-005', name: 'Vikram Reddy', userType: 'Professional', verificationType: 'Mixed Verification', submittedDate: '2024-03-16', documents: 6, status: 'FLAGGED' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FALLBACK_RULES = [
|
||||||
|
{ id: 'r1', name: 'Professional Identity Verification', userType: 'Professional', verificationType: 'Identity Verification', requiredDocs: 3, checklistItems: 8, status: 'ACTIVE' },
|
||||||
|
{ id: 'r2', name: 'Company Business Verification', userType: 'Company', verificationType: 'Business Verification', requiredDocs: 5, checklistItems: 12, status: 'ACTIVE' },
|
||||||
|
{ id: 'r3', name: 'Customer Basic Verification', userType: 'Customer', verificationType: 'Profile Verification', requiredDocs: 2, checklistItems: 6, status: 'ACTIVE' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATS = [
|
||||||
|
{ label: 'Total Pending', value: '42', color: '#6B7280' },
|
||||||
|
{ label: 'Identity Verification', value: '18', color: '#3B82F6' },
|
||||||
|
{ label: 'Business Verification', value: '12', color: '#8B5CF6' },
|
||||||
|
{ label: 'Re-upload Review', value: '8', color: '#FF5E13' },
|
||||||
|
{ label: 'Verified Today', value: '15', color: '#10B981' },
|
||||||
|
{ label: 'Flagged Cases', value: '4', color: '#EF4444' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PREVIEW_STATES = [
|
||||||
|
{ key: 'pending', label: 'Pending Verification' },
|
||||||
|
{ key: 'review', label: 'Under Review' },
|
||||||
|
{ key: 'reupload', label: 'Re-upload Requested' },
|
||||||
|
{ key: 'completed', label: 'Completed' },
|
||||||
|
{ key: 'rejected', label: 'Rejected' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
async function fetchVerifications(): Promise<VerificationRow[]> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API}/api/admin/approvals`);
|
const res = await fetch(`${API}/api/admin/verifications`);
|
||||||
if (!res.ok) return [];
|
if (!res.ok) throw new Error();
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return Array.isArray(data) ? data : (data.approvals || []);
|
const list = Array.isArray(data) ? data : (data.verifications ?? data.approvals ?? []);
|
||||||
|
return list.length > 0 ? list : FALLBACK;
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return FALLBACK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowName(r: VerificationRow) {
|
||||||
|
return r.name || r.requester?.name || r.requesterName || '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowVerifId(r: VerificationRow) {
|
||||||
|
return r.verificationId || r.id || '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowStatus(r: VerificationRow): VerificationStatus {
|
||||||
|
const raw = String(r.status || r.requestStatus || '').toUpperCase();
|
||||||
|
if (raw === 'VERIFIED' || raw === 'APPROVED') return 'VERIFIED';
|
||||||
|
if (raw === 'FLAGGED' || raw === 'REJECTED') return 'FLAGGED';
|
||||||
|
if (raw === 'RE_UPLOAD' || raw === 'CHANGES_REQUESTED') return 'RE_UPLOAD';
|
||||||
|
return 'PENDING';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowDate(r: VerificationRow) {
|
||||||
|
return r.submittedDate || (r.createdAt ? r.createdAt.split('T')[0] : r.created_at ? r.created_at.split('T')[0] : '—');
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge(props: { status: VerificationStatus }) {
|
||||||
|
const styles: Record<VerificationStatus, string> = {
|
||||||
|
PENDING: 'display:inline-flex;align-items:center;justify-content:center;min-width:72px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;border:1px solid #FFD8C2;background:#FFF1EB;color:#FF5E13',
|
||||||
|
VERIFIED: 'display:inline-flex;align-items:center;justify-content:center;min-width:72px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;background:#ECFDF5;color:#059669;border:1px solid #A7F3D0',
|
||||||
|
FLAGGED: 'display:inline-flex;align-items:center;justify-content:center;min-width:72px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;background:#FEF2F2;color:#DC2626;border:1px solid #FECACA',
|
||||||
|
RE_UPLOAD: 'display:inline-flex;align-items:center;justify-content:center;min-width:72px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;background:#EFF6FF;color:#3B82F6;border:1px solid #BFDBFE',
|
||||||
|
};
|
||||||
|
const labels: Record<VerificationStatus, string> = {
|
||||||
|
PENDING: 'Pending',
|
||||||
|
VERIFIED: 'Verified',
|
||||||
|
FLAGGED: 'Flagged',
|
||||||
|
RE_UPLOAD: 'Re-upload',
|
||||||
|
};
|
||||||
|
return <span style={styles[props.status]}>{labels[props.status]}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function VerificationStatusPage() {
|
export default function VerificationStatusPage() {
|
||||||
const [rows] = createResource(fetchApprovals);
|
const [rows] = createResource(fetchVerifications);
|
||||||
|
const [search, setSearch] = createSignal('');
|
||||||
|
const [userTypeFilter, setUserTypeFilter] = createSignal('');
|
||||||
|
const [verifTypeFilter, setVerifTypeFilter] = createSignal('');
|
||||||
|
const [statusFilter, setStatusFilter] = createSignal('');
|
||||||
|
const [activeView, setActiveView] = createSignal<'queue' | 'rules' | 'preview'>('queue');
|
||||||
|
const [previewState, setPreviewState] = createSignal('review');
|
||||||
|
const [page, setPage] = createSignal(1);
|
||||||
|
const [openActionId, setOpenActionId] = createSignal<string | null>(null);
|
||||||
|
const [rulesSearch, setRulesSearch] = createSignal('');
|
||||||
|
const [rulesUserType, setRulesUserType] = createSignal('');
|
||||||
|
|
||||||
const normalized = createMemo(() => (rows() || []).map((r) => ({
|
const PAGE_SIZE = 10;
|
||||||
...r,
|
|
||||||
status: (r.requestStatus || r.status || 'UNKNOWN').toUpperCase(),
|
|
||||||
type: (r.requestType || r.type || 'PROFILE').toUpperCase(),
|
|
||||||
requesterName: r.requester?.name || r.requesterName || 'Unknown',
|
|
||||||
requesterEmail: r.requester?.email || r.requesterEmail || '—',
|
|
||||||
createdAt: r.createdAt || r.created_at || '',
|
|
||||||
})));
|
|
||||||
|
|
||||||
function statusDoc(status: string) {
|
const filtered = createMemo(() => {
|
||||||
if (status === 'APPROVED') return 'Verified';
|
const list = rows() ?? [];
|
||||||
if (status === 'REJECTED') return 'Flagged';
|
const q = search().trim().toLowerCase();
|
||||||
if (status === 'PENDING') return 'Review';
|
const sf = statusFilter();
|
||||||
return 'Pending';
|
const ut = userTypeFilter();
|
||||||
|
const vt = verifTypeFilter();
|
||||||
|
return list.filter((r) => {
|
||||||
|
const matchSearch = !q || [rowVerifId(r), rowName(r), r.userType, r.verificationType].join(' ').toLowerCase().includes(q);
|
||||||
|
const matchStatus = !sf || rowStatus(r) === sf;
|
||||||
|
const matchUserType = !ut || (r.userType || '').toLowerCase() === ut.toLowerCase();
|
||||||
|
const matchVerifType = !vt || (r.verificationType || '').toLowerCase().includes(vt.toLowerCase());
|
||||||
|
return matchSearch && matchStatus && matchUserType && matchVerifType;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredRules = createMemo(() => {
|
||||||
|
const q = rulesSearch().trim().toLowerCase();
|
||||||
|
const ut = rulesUserType();
|
||||||
|
return FALLBACK_RULES.filter((r) => {
|
||||||
|
const matchSearch = !q || r.name.toLowerCase().includes(q) || r.userType.toLowerCase().includes(q);
|
||||||
|
const matchUserType = !ut || r.userType.toLowerCase() === ut.toLowerCase();
|
||||||
|
return matchSearch && matchUserType;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = () => Math.max(1, Math.ceil(filtered().length / PAGE_SIZE));
|
||||||
|
const paged = () => filtered().slice((page() - 1) * PAGE_SIZE, page() * PAGE_SIZE);
|
||||||
|
|
||||||
|
function viewBtnStyle(view: 'queue' | 'rules' | 'preview') {
|
||||||
|
const isActive = activeView() === view;
|
||||||
|
const isFirst = view === 'queue';
|
||||||
|
return isActive
|
||||||
|
? `background:#0D0D2A;color:white;padding:8px 16px;font-size:13px;font-weight:600;border:none;cursor:pointer${!isFirst ? ';border-left:1px solid #E5E7EB' : ''}`
|
||||||
|
: `background:white;color:#374151;padding:8px 16px;font-size:13px;font-weight:500;border:none;cursor:pointer${!isFirst ? ';border-left:1px solid #E5E7EB' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div class="space-y-6 max-w-[1600px]">
|
<div style="width:100%;padding-bottom:32px">
|
||||||
{/* Header Configuration */}
|
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between px-1">
|
{/* Page Header */}
|
||||||
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:24px">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-4">
|
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Verification Management</h1>
|
||||||
<h1 class="text-[32px] font-bold leading-tight text-[#0A1128]">Verification Management</h1>
|
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Review and verify user submissions</p>
|
||||||
<div class="hidden items-center gap-3 md:flex">
|
</div>
|
||||||
<button class="inline-flex h-10 items-center justify-center rounded-xl bg-[#0A1128] px-5 text-[14px] font-semibold text-white hover:bg-[#1E293B] transition-colors">
|
{/* Toggle button group */}
|
||||||
|
<div style="display:flex;align-items:center;gap:0;border-radius:10px;overflow:hidden;border:1px solid #E5E7EB">
|
||||||
|
<button type="button" onClick={() => setActiveView('queue')} style={viewBtnStyle('queue')}>
|
||||||
Verification Queue
|
Verification Queue
|
||||||
</button>
|
</button>
|
||||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-5 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC] transition-colors">
|
<button type="button" onClick={() => setActiveView('rules')} style={viewBtnStyle('rules')}>
|
||||||
Verification Rules
|
Verification Rules
|
||||||
</button>
|
</button>
|
||||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-5 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC] transition-colors">
|
<button type="button" onClick={() => setActiveView('preview')} style={viewBtnStyle('preview')}>
|
||||||
User Preview
|
User Preview
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-[15px] text-[#64748B]">Review and verify user submissions</p>
|
|
||||||
|
{/* ─── VERIFICATION QUEUE VIEW ─── */}
|
||||||
|
<Show when={activeView() === 'queue'}>
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:16px;margin-bottom:24px">
|
||||||
|
<For each={STATS}>
|
||||||
|
{(stat) => (
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:16px;">
|
||||||
|
<p style="font-size:12px;color:#6B7280;font-weight:500;margin:0 0 8px 0">{stat.label}</p>
|
||||||
|
<p style={`font-size:28px;font-weight:700;color:${stat.color};margin:0`}>{stat.value}</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 6 KPI Cards Grid */}
|
{/* Section Header */}
|
||||||
<section class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
||||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
<span style="font-size:18px;font-weight:700;color:#111827">Verification Cases</span>
|
||||||
<p class="text-[14px] leading-tight text-[#64748B]">Total<br/>Pending</p>
|
<div style="display:flex;gap:8px">
|
||||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#0A1128]">42</p>
|
<button type="button" style="height:34px;padding:0 14px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer">
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
|
||||||
<p class="text-[14px] leading-tight text-[#64748B]">Identity<br/>Verification</p>
|
|
||||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#2563EB]">18</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
|
||||||
<p class="text-[14px] leading-tight text-[#64748B]">Business<br/>Verification</p>
|
|
||||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#7C3AED]">12</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
|
||||||
<p class="text-[14px] leading-tight text-[#64748B]">Re-upload<br/>Review</p>
|
|
||||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#FF5E13]">8</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
|
||||||
<p class="text-[14px] leading-tight text-[#64748B]">Verified<br/>Today</p>
|
|
||||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#16A34A]">15</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
|
||||||
<p class="text-[14px] leading-tight text-[#64748B]">Flagged<br/>Cases</p>
|
|
||||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#FF5E13]">4</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Table Container */}
|
|
||||||
<section class="rounded-2xl border border-[#E2E8F0] bg-white p-6 shadow-sm">
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-6">
|
|
||||||
<h2 class="text-[20px] font-bold text-[#0A1128]">Verification Cases</h2>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] px-4 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC]">
|
|
||||||
Export Queue
|
Export Queue
|
||||||
</button>
|
</button>
|
||||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] px-4 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC]">
|
<button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer">
|
||||||
Bulk Actions
|
Bulk Actions
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 md:flex-row md:items-center mb-6">
|
{/* Filter Bar */}
|
||||||
<div class="relative w-full max-w-sm">
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap">
|
||||||
<Search size={18} class="absolute left-3.5 top-1/2 -translate-y-1/2 text-[#94A3B8]" />
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name or ID..."
|
placeholder="Search by name or ID..."
|
||||||
class="h-11 w-full rounded-xl border border-[#E2E8F0] bg-[#F8FAFC] pl-10 pr-4 text-[14px] outline-none transition-colors focus:border-[#CBD5E1] focus:bg-white"
|
value={search()}
|
||||||
|
onInput={(e) => { setSearch(e.currentTarget.value); setPage(1); }}
|
||||||
|
style="height:34px;flex:1;max-width:280px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827;background:white;box-sizing:border-box"
|
||||||
/>
|
/>
|
||||||
</div>
|
<select
|
||||||
<div class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white" />
|
value={userTypeFilter()}
|
||||||
<div class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white" />
|
onChange={(e) => { setUserTypeFilter(e.currentTarget.value); setPage(1); }}
|
||||||
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;color:#374151;background:white;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="">User Type</option>
|
||||||
|
<option value="Professional">Professional</option>
|
||||||
|
<option value="Company">Company</option>
|
||||||
|
<option value="Customer">Customer</option>
|
||||||
|
<option value="Jobseeker">Jobseeker</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={verifTypeFilter()}
|
||||||
|
onChange={(e) => { setVerifTypeFilter(e.currentTarget.value); setPage(1); }}
|
||||||
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;color:#374151;background:white;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="">Verification Type</option>
|
||||||
|
<option value="Identity Verification">Identity Verification</option>
|
||||||
|
<option value="Business Verification">Business Verification</option>
|
||||||
|
<option value="Profile Verification">Profile Verification</option>
|
||||||
|
<option value="Document Verification">Document Verification</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={statusFilter()}
|
||||||
|
onChange={(e) => { setStatusFilter(e.currentTarget.value); setPage(1); }}
|
||||||
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;color:#374151;background:white;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="">Status</option>
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="VERIFIED">Verified</option>
|
||||||
|
<option value="FLAGGED">Flagged</option>
|
||||||
|
<option value="RE_UPLOAD">Re-upload</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto rounded-xl border border-[#E2E8F0]">
|
{/* Table Card */}
|
||||||
<table class="w-full min-w-[1000px] border-collapse bg-white text-left">
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
|
||||||
|
<div style="overflow-x:auto">
|
||||||
|
<table style="width:100%;border-collapse:collapse;min-width:900px">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-[#0A1128] text-white">
|
<tr style="background:#0D0D2A">
|
||||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERIFICATION ID</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Verification ID</th>
|
||||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">APPLICANT NAME</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Applicant Name</th>
|
||||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">USER TYPE</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">User Type</th>
|
||||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERIFICATION TYPE</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Verification Type</th>
|
||||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">SUBMITTED DATE</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Submitted Date</th>
|
||||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">DOCUMENT STATUS</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Documents</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Status</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<Show when={rows.loading}>
|
<Show when={rows.loading}>
|
||||||
<tr><td colspan="6" class="text-center py-12 text-[#64748B] text-[14px]">Loading verification cases...</td></tr>
|
<tr>
|
||||||
|
<td colspan="8" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">Loading verification cases...</td>
|
||||||
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!rows.loading && normalized().length === 0}>
|
<Show when={!rows.loading && paged().length === 0}>
|
||||||
<tr><td colspan="6" class="text-center py-12 text-[#64748B] text-[14px]">No pending verification cases found.</td></tr>
|
<tr>
|
||||||
|
<td colspan="8" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">No verification cases found.</td>
|
||||||
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
<For each={normalized()}>
|
<For each={paged()}>
|
||||||
{(item) => (
|
{(row) => (
|
||||||
<tr class="border-b border-[#E2E8F0] transition-colors hover:bg-[#F8FAFC]">
|
<tr style="border-bottom:1px solid #F3F4F6">
|
||||||
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">VER—2024—{item.id.slice(0, 3)}</td>
|
<td style="padding:12px 20px;font-size:13px">
|
||||||
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">{item.requesterName}</td>
|
<span style="font-size:12px;font-family:monospace;color:#6B7280">{rowVerifId(row)}</span>
|
||||||
<td class="px-6 py-4 text-[14px] text-[#475569]">Professional</td>
|
|
||||||
<td class="px-6 py-4 text-[14px] text-[#475569]">Identity Verification</td>
|
|
||||||
<td class="px-6 py-4 text-[14px] text-[#475569]">
|
|
||||||
{item.createdAt ? new Date(item.createdAt).toISOString().split('T')[0] : '2024-03-20'}
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td style="padding:12px 20px;font-size:13px;font-weight:600;color:#111827">{rowName(row)}</td>
|
||||||
<A
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{row.userType || '—'}</td>
|
||||||
href={`/admin/verification-status/${item.id}`}
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{row.verificationType || row.type || '—'}</td>
|
||||||
class={`inline-flex items-center rounded-lg px-3 py-1.5 text-[13px] font-bold transition-opacity hover:opacity-80 ${
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{rowDate(row)}</td>
|
||||||
statusDoc(item.status) === 'Verified' ? 'bg-[#DCFCE7] text-[#16A34A]' :
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{row.documents ?? '—'}</td>
|
||||||
statusDoc(item.status) === 'Flagged' ? 'bg-[#FEE2E2] text-[#EF4444]' :
|
<td style="padding:12px 20px">
|
||||||
'bg-[#F1F5F9] text-[#64748B]'
|
<StatusBadge status={rowStatus(row)} />
|
||||||
}`}
|
</td>
|
||||||
|
<td style="padding:12px 20px;position:relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenActionId(openActionId() === row.id ? null : row.id)}
|
||||||
|
style="height:30px;padding:0 12px;font-size:12px;font-weight:500;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer"
|
||||||
>
|
>
|
||||||
{statusDoc(item.status)}
|
Actions ▾
|
||||||
</A>
|
</button>
|
||||||
|
<Show when={openActionId() === row.id}>
|
||||||
|
<div style="position:absolute;right:20px;top:44px;z-index:20;width:210px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
|
||||||
|
<button type="button" style="display:block;width:100%;text-align:left;padding:10px 12px;font-size:13px;color:#111827;background:none;border:none;cursor:pointer;border-radius:6px" onMouseEnter={(e) => (e.currentTarget.style.background = '#F9FAFB')} onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}>
|
||||||
|
View Details
|
||||||
|
</button>
|
||||||
|
<button type="button" style="display:block;width:100%;text-align:left;padding:10px 12px;font-size:13px;color:#059669;background:none;border:none;cursor:pointer;border-radius:6px" onMouseEnter={(e) => (e.currentTarget.style.background = '#ECFDF5')} onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}>
|
||||||
|
Approve
|
||||||
|
</button>
|
||||||
|
<button type="button" style="display:block;width:100%;text-align:left;padding:10px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;border-radius:6px" onMouseEnter={(e) => (e.currentTarget.style.background = '#FEF2F2')} onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}>
|
||||||
|
Reject / Flag
|
||||||
|
</button>
|
||||||
|
<button type="button" style="display:block;width:100%;text-align:left;padding:10px 12px;font-size:13px;color:#D97706;background:none;border:none;cursor:pointer;border-radius:6px" onMouseEnter={(e) => (e.currentTarget.style.background = '#FFFBEB')} onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}>
|
||||||
|
Request Re-upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
@ -175,7 +320,195 @@ export default function VerificationStatusPage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;border-top:1px solid #F3F4F6;padding:12px 20px">
|
||||||
|
<span style="font-size:13px;color:#6B7280">
|
||||||
|
Showing {Math.min((page() - 1) * PAGE_SIZE + 1, filtered().length)}–{Math.min(page() * PAGE_SIZE, filtered().length)} of {filtered().length}
|
||||||
|
</span>
|
||||||
|
<div style="display:flex;gap:4px">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPage(Math.max(1, page() - 1))}
|
||||||
|
disabled={page() === 1}
|
||||||
|
style="height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<For each={Array.from({ length: totalPages() }, (_, i) => i + 1)}>
|
||||||
|
{(p) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPage(p)}
|
||||||
|
style={p === page()
|
||||||
|
? 'height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #FF5E13;background:#FF5E13;color:white;border-radius:6px;cursor:pointer;font-weight:600'
|
||||||
|
: 'height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer'}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPage(Math.min(totalPages(), page() + 1))}
|
||||||
|
disabled={page() === totalPages()}
|
||||||
|
style="height:30px;min-width:30px;padding:0 10px;font-size:13px;border:1px solid #E5E7EB;background:white;color:#374151;border-radius:6px;cursor:pointer"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* ─── VERIFICATION RULES VIEW ─── */}
|
||||||
|
<Show when={activeView() === 'rules'}>
|
||||||
|
{/* Section Header */}
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
||||||
|
<span style="font-size:18px;font-weight:700;color:#111827">Verification Rules</span>
|
||||||
|
<button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer">
|
||||||
|
+ Create Verification Rule
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter */}
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search rules..."
|
||||||
|
value={rulesSearch()}
|
||||||
|
onInput={(e) => setRulesSearch(e.currentTarget.value)}
|
||||||
|
style="height:34px;flex:1;max-width:280px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827;background:white;box-sizing:border-box"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={rulesUserType()}
|
||||||
|
onChange={(e) => setRulesUserType(e.currentTarget.value)}
|
||||||
|
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;color:#374151;background:white;cursor:pointer"
|
||||||
|
>
|
||||||
|
<option value="">User Type</option>
|
||||||
|
<option value="Professional">Professional</option>
|
||||||
|
<option value="Company">Company</option>
|
||||||
|
<option value="Customer">Customer</option>
|
||||||
|
<option value="Jobseeker">Jobseeker</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rules Table Card */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;overflow:hidden">
|
||||||
|
<div style="overflow-x:auto">
|
||||||
|
<table style="width:100%;border-collapse:collapse;min-width:800px">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#0D0D2A">
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Rule Name</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">User Type</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Verification Type</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Required Documents</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Checklist Items</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Show when={filteredRules().length === 0}>
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" style="padding:40px 20px;text-align:center;font-size:14px;color:#9CA3AF">No rules found.</td>
|
||||||
|
</tr>
|
||||||
|
</Show>
|
||||||
|
<For each={filteredRules()}>
|
||||||
|
{(rule) => (
|
||||||
|
<tr style="border-bottom:1px solid #F3F4F6">
|
||||||
|
<td style="padding:12px 20px;font-size:13px;font-weight:600;color:#111827">{rule.name}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{rule.userType}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{rule.verificationType}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{rule.requiredDocs}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{rule.checklistItems}</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<span style="display:inline-flex;align-items:center;justify-content:center;min-width:64px;padding:3px 10px;font-size:11px;font-weight:600;border-radius:4px;background:#ECFDF5;color:#059669;border:1px solid #A7F3D0">
|
||||||
|
{rule.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* ─── USER PREVIEW VIEW ─── */}
|
||||||
|
<Show when={activeView() === 'preview'}>
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:24px">
|
||||||
|
<p style="font-size:16px;font-weight:600;color:#111827;margin:0 0 16px 0">Select Verification State</p>
|
||||||
|
|
||||||
|
{/* State Selector Buttons */}
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:32px;flex-wrap:wrap">
|
||||||
|
<For each={PREVIEW_STATES}>
|
||||||
|
{(state) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPreviewState(state.key)}
|
||||||
|
style={previewState() === state.key
|
||||||
|
? 'border:2px solid #FF5E13;background:white;color:#FF5E13;padding:8px 16px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer'
|
||||||
|
: 'border:1px solid #E5E7EB;background:white;color:#374151;padding:8px 16px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer'}
|
||||||
|
>
|
||||||
|
{state.label}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview Card */}
|
||||||
|
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:48px;text-align:center">
|
||||||
|
{/* Clock icon */}
|
||||||
|
<div style="display:flex;justify-content:center">
|
||||||
|
<Clock size={48} color="#FF5E13" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:20px;font-weight:700;color:#111827;margin-top:16px;margin-bottom:0">Verification Status</p>
|
||||||
|
<p style="font-size:14px;color:#6B7280;margin-top:8px;max-width:400px;margin-left:auto;margin-right:auto">
|
||||||
|
Your submitted information and documents are currently under review.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Progress Row */}
|
||||||
|
<div style="display:flex;align-items:center;justify-content:center;gap:0;margin-top:32px">
|
||||||
|
{/* Step 1: Submitted */}
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||||
|
<div style="width:32px;height:32px;border-radius:50%;background:#10B981;display:flex;align-items:center;justify-content:center">
|
||||||
|
<span style="color:white;font-size:16px;font-weight:700;line-height:1">✓</span>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:12px;font-weight:500;color:#10B981">Submitted</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Connector */}
|
||||||
|
<div style="width:80px;height:2px;background:#FF5E13;margin-bottom:20px" />
|
||||||
|
|
||||||
|
{/* Step 2: Under Review */}
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||||
|
<div style="width:32px;height:32px;border-radius:50%;background:#FF5E13;display:flex;align-items:center;justify-content:center">
|
||||||
|
<Clock size={16} color="white" />
|
||||||
|
</div>
|
||||||
|
<span style="font-size:12px;font-weight:500;color:#FF5E13">Under Review</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Connector */}
|
||||||
|
<div style="width:80px;height:2px;background:#E5E7EB;margin-bottom:20px" />
|
||||||
|
|
||||||
|
{/* Step 3: Verification Result */}
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||||
|
<div style="width:32px;height:32px;border-radius:50%;background:#E5E7EB;display:flex;align-items:center;justify-content:center">
|
||||||
|
<span style="color:#9CA3AF;font-size:14px;font-weight:700;line-height:1">?</span>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:12px;font-weight:500;color:#9CA3AF">Verification Result</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" style="height:40px;padding:0 24px;border-radius:10px;background:#0D0D2A;color:white;font-size:14px;font-weight:600;border:none;cursor:pointer;margin-top:24px">
|
||||||
|
Back to Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue