2026-03-27 05:35:18 +01:00
|
|
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
2026-03-19 14:21:49 +01:00
|
|
|
import AdminShell from '~/components/AdminShell';
|
2026-03-27 05:35:18 +01:00
|
|
|
import type { CrudRecord } from '~/lib/admin/types';
|
|
|
|
|
|
|
|
|
|
const API = '/api/gateway';
|
|
|
|
|
|
|
|
|
|
type VerificationRecord = CrudRecord & {
|
|
|
|
|
applicantName?: string;
|
|
|
|
|
userType: 'CUSTOMER' | 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER';
|
|
|
|
|
verificationType: 'IDENTITY' | 'BUSINESS' | 'PROFILE' | 'DOCUMENT' | 'MIXED';
|
|
|
|
|
submittedDate?: string;
|
|
|
|
|
documentsCount?: number;
|
|
|
|
|
assignedVerifier?: string;
|
|
|
|
|
priority: 'LOW' | 'MEDIUM' | 'HIGH';
|
|
|
|
|
status: 'PENDING' | 'IN_REVIEW' | 'PARTIALLY_VERIFIED' | 'VERIFIED' | 'FLAGGED' | 'RE_UPLOAD_REQUESTED' | 'REJECTED';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function StatusBadge(props: { status: string }) {
|
|
|
|
|
const getColors = () => {
|
|
|
|
|
switch (props.status) {
|
|
|
|
|
case 'VERIFIED': return { border: '#B7E4C7', bg: '#DEF7E8', text: '#0B8A4A', dot: '#0B8A4A' };
|
|
|
|
|
case 'IN_REVIEW': return { border: '#F6D78F', bg: '#FFF3D6', text: '#B7791F', dot: '#B7791F' };
|
|
|
|
|
case 'PENDING': return { border: '#D1D5DB', bg: '#F3F4F6', text: '#4B5563', dot: '#9CA3AF' };
|
|
|
|
|
case 'RE_UPLOAD_REQUESTED': return { border: '#FDE68A', bg: '#FEF3C7', text: '#D97706', dot: '#D97706' };
|
|
|
|
|
case 'FLAGGED': return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
|
|
|
|
|
case 'REJECTED': return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
|
|
|
|
|
default: return { border: '#D1D5DB', bg: '#F3F4F6', text: '#4B5563', dot: '#9CA3AF' };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const colors = getColors();
|
|
|
|
|
const label = props.status.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' ');
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${colors.border};background:${colors.bg};color:${colors.text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
|
|
|
|
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${colors.dot};margin-right:5px;flex-shrink:0`} />
|
|
|
|
|
{label}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function PriorityBadge(props: { priority: string }) {
|
|
|
|
|
const color = props.priority === 'HIGH' ? '#DC2626' : props.priority === 'MEDIUM' ? '#F59E0B' : '#16A34A';
|
|
|
|
|
return (
|
|
|
|
|
<span style={`display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:${color}`}>
|
|
|
|
|
<span style={`width:6px;height:6px;border-radius:50%;background:${color}`} />
|
|
|
|
|
{props.priority}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-19 14:21:49 +01:00
|
|
|
|
|
|
|
|
export default function VerificationManagementPage() {
|
2026-03-27 05:35:18 +01:00
|
|
|
const [view, setView] = createSignal<'list' | 'form'>('list');
|
|
|
|
|
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
|
|
|
|
|
const [detailTab, setDetailTab] = createSignal<'overview' | 'documents' | 'checklist' | 'logs'>('overview');
|
|
|
|
|
|
|
|
|
|
const [search, setSearch] = createSignal('');
|
|
|
|
|
const [rows, setRows] = createSignal<VerificationRecord[]>([]);
|
|
|
|
|
const [viewingCase, setViewingCase] = createSignal<VerificationRecord | null>(null);
|
|
|
|
|
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const [statusFilter, setStatusFilter] = createSignal<'all' | 'pending' | 'flagged'>('all');
|
|
|
|
|
const [sortBy, setSortBy] = createSignal<'submitted_desc' | 'submitted_asc' | 'priority_desc' | 'priority_asc'>('submitted_desc');
|
|
|
|
|
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
|
|
|
|
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
|
|
|
|
|
|
|
|
|
const [ruleName, setRuleName] = createSignal('');
|
|
|
|
|
const [ruleUserType, setRuleUserType] = createSignal<VerificationRecord['userType']>('PROFESSIONAL');
|
|
|
|
|
const [ruleVerificationType, setRuleVerificationType] = createSignal<VerificationRecord['verificationType']>('IDENTITY');
|
|
|
|
|
const [ruleActive, setRuleActive] = createSignal(true);
|
|
|
|
|
const [formError, setFormError] = createSignal('');
|
2026-03-30 04:48:09 +02:00
|
|
|
const [error, setError] = createSignal('');
|
2026-03-27 05:35:18 +01:00
|
|
|
|
|
|
|
|
type VerificationRule = {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
userType: VerificationRecord['userType'];
|
|
|
|
|
verificationType: VerificationRecord['verificationType'];
|
|
|
|
|
status: 'ACTIVE' | 'INACTIVE';
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [rules, setRules] = createSignal<VerificationRule[]>([
|
|
|
|
|
{ id: 'vr1', name: 'Professional Identity Rule', userType: 'PROFESSIONAL', verificationType: 'IDENTITY', status: 'ACTIVE', updatedAt: '2026-03-20' },
|
|
|
|
|
{ id: 'vr2', name: 'Company Business Verification', userType: 'COMPANY', verificationType: 'BUSINESS', status: 'ACTIVE', updatedAt: '2026-03-18' },
|
|
|
|
|
{ id: 'vr3', name: 'Jobseeker Profile Check', userType: 'JOBSEEKER', verificationType: 'PROFILE', status: 'INACTIVE', updatedAt: '2026-03-12' },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const load = async () => {
|
2026-03-30 04:48:09 +02:00
|
|
|
setError('');
|
|
|
|
|
try {
|
|
|
|
|
const accessToken = typeof sessionStorage !== 'undefined'
|
|
|
|
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
|
|
|
|
: '';
|
|
|
|
|
const res = await fetch(`${API}/api/admin/approvals?page=1&limit=100`, {
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
|
|
|
},
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
|
|
|
const payload = await res.json().catch(() => ({} as any));
|
|
|
|
|
const jobs = Array.isArray(payload?.jobs) ? payload.jobs : [];
|
|
|
|
|
const requirements = Array.isArray(payload?.requirements) ? payload.requirements : [];
|
|
|
|
|
|
|
|
|
|
const jobCases: VerificationRecord[] = jobs.map((job: any) => ({
|
|
|
|
|
id: `job-${String(job.id)}`,
|
|
|
|
|
name: `Job Verification - ${String(job.title || 'Untitled Job')}`,
|
|
|
|
|
applicantName: String(job.title || 'Untitled Job'),
|
|
|
|
|
userType: 'COMPANY',
|
|
|
|
|
verificationType: 'BUSINESS',
|
|
|
|
|
submittedDate: String(job.created_at || ''),
|
|
|
|
|
documentsCount: 1,
|
|
|
|
|
assignedVerifier: 'Unassigned',
|
|
|
|
|
priority: 'HIGH',
|
|
|
|
|
status: 'IN_REVIEW',
|
|
|
|
|
updatedAt: String(job.updated_at || job.created_at || ''),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const requirementCases: VerificationRecord[] = requirements.map((req: any) => ({
|
|
|
|
|
id: `requirement-${String(req.id)}`,
|
|
|
|
|
name: `Requirement Verification - ${String(req.title || 'Untitled Requirement')}`,
|
|
|
|
|
applicantName: String(req.title || 'Untitled Requirement'),
|
|
|
|
|
userType: 'CUSTOMER',
|
|
|
|
|
verificationType: 'PROFILE',
|
|
|
|
|
submittedDate: String(req.created_at || ''),
|
|
|
|
|
documentsCount: 1,
|
|
|
|
|
assignedVerifier: 'Unassigned',
|
|
|
|
|
priority: 'MEDIUM',
|
|
|
|
|
status: 'PENDING',
|
|
|
|
|
updatedAt: String(req.updated_at || req.created_at || ''),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
setRows([...jobCases, ...requirementCases]);
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
setRows([]);
|
|
|
|
|
setError(e?.message || 'Could not reach verification API.');
|
|
|
|
|
}
|
2026-03-27 05:35:18 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMount(() => void load());
|
|
|
|
|
|
|
|
|
|
const formatDate = (v?: string) => {
|
|
|
|
|
const s = v || '';
|
|
|
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
|
|
|
|
|
return s.slice(0, 10) || '—';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const filteredRows = createMemo(() => {
|
|
|
|
|
let list = rows();
|
|
|
|
|
const f = statusFilter();
|
|
|
|
|
if (f === 'pending') list = list.filter((r) => r.status === 'PENDING' || r.status === 'IN_REVIEW');
|
|
|
|
|
if (f === 'flagged') list = list.filter((r) => r.status === 'FLAGGED');
|
|
|
|
|
|
|
|
|
|
const q = search().trim().toLowerCase();
|
|
|
|
|
if (q) {
|
|
|
|
|
list = list.filter((r) =>
|
|
|
|
|
String(r.applicantName || '').toLowerCase().includes(q)
|
|
|
|
|
|| String(r.id || '').toLowerCase().includes(q)
|
|
|
|
|
|| String(r.verificationType || '').toLowerCase().includes(q)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sorted = [...list];
|
|
|
|
|
const mode = sortBy();
|
|
|
|
|
const priorityRank = (p: VerificationRecord['priority']) => (p === 'HIGH' ? 3 : p === 'MEDIUM' ? 2 : 1);
|
|
|
|
|
sorted.sort((a, b) => {
|
|
|
|
|
const ad = Date.parse(String(a.submittedDate || a.updatedAt || '')) || 0;
|
|
|
|
|
const bd = Date.parse(String(b.submittedDate || b.updatedAt || '')) || 0;
|
|
|
|
|
if (mode === 'submitted_asc') return ad - bd;
|
|
|
|
|
if (mode === 'priority_desc') return priorityRank(b.priority) - priorityRank(a.priority);
|
|
|
|
|
if (mode === 'priority_asc') return priorityRank(a.priority) - priorityRank(b.priority);
|
|
|
|
|
return bd - ad;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return sorted;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const openView = (row: VerificationRecord) => {
|
|
|
|
|
setViewingCase(row);
|
|
|
|
|
setDetailTab('overview');
|
|
|
|
|
setListTab('view');
|
|
|
|
|
setOpenMenuId(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resetRuleForm = () => {
|
|
|
|
|
setRuleName('');
|
|
|
|
|
setRuleUserType('PROFESSIONAL');
|
|
|
|
|
setRuleVerificationType('IDENTITY');
|
|
|
|
|
setRuleActive(true);
|
|
|
|
|
setFormError('');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openCreate = () => {
|
|
|
|
|
resetRuleForm();
|
|
|
|
|
setListTab('create');
|
|
|
|
|
setView('form');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveRule = () => {
|
|
|
|
|
if (!ruleName().trim()) {
|
|
|
|
|
setFormError('Rule name is required.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const now = new Date().toISOString().slice(0, 10);
|
|
|
|
|
const id = `vr_${Math.random().toString(16).slice(2)}`;
|
|
|
|
|
setRules((prev) => [
|
|
|
|
|
{ id, name: ruleName().trim(), userType: ruleUserType(), verificationType: ruleVerificationType(), status: ruleActive() ? 'ACTIVE' : 'INACTIVE', updatedAt: now },
|
|
|
|
|
...prev,
|
|
|
|
|
]);
|
|
|
|
|
setView('list');
|
|
|
|
|
setListTab('all');
|
|
|
|
|
resetRuleForm();
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-19 14:21:49 +01:00
|
|
|
return (
|
|
|
|
|
<AdminShell>
|
2026-03-27 05:35:18 +01:00
|
|
|
<div class="w-full space-y-6 pb-8">
|
|
|
|
|
|
|
|
|
|
{/* Page header */}
|
|
|
|
|
<div style="margin-bottom: 1.5rem">
|
|
|
|
|
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Verification Management</h1>
|
|
|
|
|
<p class="mt-1 text-[14px] text-[#6B7280]">Manage user identity and business verification workflows</p>
|
2026-03-19 14:21:49 +01:00
|
|
|
</div>
|
2026-03-30 04:48:09 +02:00
|
|
|
<Show when={error()}>
|
|
|
|
|
<div style="margin-bottom:10px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
|
|
|
|
{error()}
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
2026-03-27 05:35:18 +01:00
|
|
|
|
|
|
|
|
{/* ── LIST VIEW ── */}
|
|
|
|
|
<Show when={view() === 'list'}>
|
|
|
|
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
|
|
|
{([
|
|
|
|
|
{ key: 'all', label: 'All Verifications', action: () => { setListTab('all'); setStatusFilter('all'); } },
|
|
|
|
|
{ key: 'create', label: 'Create Rule', action: () => openCreate() },
|
|
|
|
|
{ key: 'view', label: 'View Verification', action: () => { setListTab('view'); } },
|
|
|
|
|
] as const).map((tab) => (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={tab.action}
|
|
|
|
|
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
|
|
|
|
>
|
|
|
|
|
{tab.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Show when={listTab() === 'view'}>
|
|
|
|
|
<Show when={!viewingCase()}>
|
|
|
|
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
|
|
|
|
|
<p style="font-size:15px;font-weight:600;color:#111827">No verification selected</p>
|
|
|
|
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click the <strong>⋮</strong> menu on any row and choose <strong>View Verification</strong>.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
<Show when={viewingCase()}>
|
|
|
|
|
<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">
|
|
|
|
|
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
|
|
|
|
<div>
|
|
|
|
|
<div style="display:flex;align-items:center;gap:12px">
|
|
|
|
|
<h2 style="font-size:18px;font-weight:700;color:#111827">{viewingCase()!.applicantName}</h2>
|
|
|
|
|
<StatusBadge status={viewingCase()!.status} />
|
|
|
|
|
<PriorityBadge priority={viewingCase()!.priority} />
|
|
|
|
|
</div>
|
|
|
|
|
<p style="margin-top:2px;font-size:13px;color:#6B7280">ID: {viewingCase()!.id} • {viewingCase()!.verificationType} • Submitted: {formatDate(viewingCase()!.submittedDate)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;gap:10px">
|
|
|
|
|
<button type="button" style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Mark Verified</button>
|
|
|
|
|
<button type="button" style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Request Re-upload</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
|
|
|
|
|
{(['overview', 'documents', 'checklist', 'logs'] as const).map((tab, i) => {
|
|
|
|
|
const labels = ['Overview', 'Documents', 'Checklist', 'Activity Logs'];
|
|
|
|
|
const active = () => detailTab() === tab;
|
|
|
|
|
return (
|
|
|
|
|
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
|
|
|
|
|
{labels[i]}
|
|
|
|
|
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="padding:24px">
|
|
|
|
|
<Show when={detailTab() === 'overview'}>
|
|
|
|
|
<div style="display:grid;grid-template-columns:2fr 1fr;gap:24px">
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:24px">
|
|
|
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
|
|
|
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Case Summary</h3>
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
|
|
|
|
<div><p style="font-size:11px;color:#9CA3AF">Applicant Name</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.applicantName}</p></div>
|
|
|
|
|
<div><p style="font-size:11px;color:#9CA3AF">User Type</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.userType}</p></div>
|
|
|
|
|
<div><p style="font-size:11px;color:#9CA3AF">Assigned Verifier</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.assignedVerifier}</p></div>
|
|
|
|
|
<div><p style="font-size:11px;color:#9CA3AF">Documents</p><p style="font-size:14px;font-weight:600;color:#111827">{Number(viewingCase()!.documentsCount || 0)}</p></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
|
|
|
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Verification Tracker</h3>
|
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center;position:relative">
|
|
|
|
|
<div style="position:absolute;top:10px;left:0;right:0;height:2px;background:#E5E7EB;z-index:1" />
|
|
|
|
|
<div style="position:absolute;top:10px;left:0;width:50%;height:2px;background:#FF5E13;z-index:2" />
|
|
|
|
|
{[
|
|
|
|
|
{ l: 'Submitted', active: true },
|
|
|
|
|
{ l: 'In Review', active: true },
|
|
|
|
|
{ l: 'Verified', active: false },
|
|
|
|
|
].map((step) => (
|
|
|
|
|
<div style="position:relative;z-index:3;text-align:center">
|
|
|
|
|
<div style={`width:20px;height:20px;border-radius:50%;background:${step.active ? '#FF5E13' : 'white'};border:2px solid ${step.active ? '#FF5E13' : '#E5E7EB'};margin:0 auto`} />
|
|
|
|
|
<p style="font-size:11px;margin-top:4px;color:#111827;font-weight:600">{step.l}</p>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px;background:#F9FAFB">
|
|
|
|
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Notes</h3>
|
|
|
|
|
<textarea placeholder="Add internal note..." style="width:100%;height:100px;border-radius:8px;border:1px solid #E5E7EB;padding:10px;font-size:13px;resize:none;margin-bottom:12px" />
|
|
|
|
|
<button type="button" style="width:100%;height:34px;background:#0D0D2A;color:white;border-radius:8px;font-size:12px;font-weight:600;border:none">Add Note</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={detailTab() === 'documents'}>
|
|
|
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;overflow:hidden">
|
|
|
|
|
<table style="width:100%;border-collapse:collapse">
|
|
|
|
|
<thead style="background:#F9FAFB">
|
|
|
|
|
<tr style="text-align:left">
|
|
|
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Document Name</th>
|
|
|
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Status</th>
|
|
|
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<For each={[
|
|
|
|
|
{ n: 'Passport / National ID', s: 'VERIFIED' },
|
|
|
|
|
{ n: 'Address Proof (Utility Bill)', s: 'PENDING' },
|
|
|
|
|
]}>
|
|
|
|
|
{(doc) => (
|
|
|
|
|
<tr style="border-top:1px solid #E5E7EB">
|
|
|
|
|
<td style="padding:12px 16px;font-size:13px;font-weight:600;color:#111827">{doc.n}</td>
|
|
|
|
|
<td style="padding:12px 16px"><StatusBadge status={doc.s} /></td>
|
|
|
|
|
<td style="padding:12px 16px;display:flex;gap:8px">
|
|
|
|
|
<button type="button" style="font-size:12px;color:#FF5E13;background:none;border:none;cursor:pointer;font-weight:600">Preview</button>
|
|
|
|
|
<button type="button" style="font-size:12px;color:#0D0D2A;background:none;border:none;cursor:pointer;font-weight:600">Approve</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={detailTab() === 'checklist'}>
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
|
|
|
{[
|
|
|
|
|
'Identity matches provided documents',
|
|
|
|
|
'Address proof is valid and recent',
|
|
|
|
|
'Business registration is authentic',
|
|
|
|
|
'Contact information is verified',
|
|
|
|
|
].map((item) => (
|
|
|
|
|
<label style="display:flex;align-items:center;gap:12px;padding:12px;border:1px solid #E5E7EB;border-radius:10px;cursor:pointer">
|
|
|
|
|
<input type="checkbox" style="width:16px;height:16px;accent-color:#FF5E13" />
|
|
|
|
|
<span style="font-size:13px;color:#111827;font-weight:500">{item}</span>
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={detailTab() === 'logs'}>
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:16px">
|
|
|
|
|
<div style="display:flex;gap:12px">
|
|
|
|
|
<div style="width:8px;height:8px;border-radius:50%;background:#FF5E13;margin-top:4px" />
|
|
|
|
|
<div>
|
|
|
|
|
<p style="font-size:13px;font-weight:600;color:#111827">Case Review Started</p>
|
|
|
|
|
<p style="font-size:12px;color:#6B7280">Verifier started reviewing documents • 2 hours ago</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;gap:12px">
|
|
|
|
|
<div style="width:8px;height:8px;border-radius:50%;background:#E5E7EB;margin-top:4px" />
|
|
|
|
|
<div>
|
|
|
|
|
<p style="font-size:13px;font-weight:600;color:#111827">Verification Request Submitted</p>
|
|
|
|
|
<p style="font-size:12px;color:#6B7280">System received applicant data • 1 day ago</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="display:flex;align-items:center;gap:10px;padding:14px 24px;border-top:1px solid #E5E7EB">
|
|
|
|
|
<button type="button" onClick={() => { setViewingCase(null); setListTab('all'); }} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<div style={`display:${listTab() === 'view' ? 'none' : 'block'}`}>
|
|
|
|
|
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;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)">
|
|
|
|
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
|
|
|
|
<input
|
|
|
|
|
value={search()}
|
|
|
|
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
|
|
|
|
placeholder="Search verifications..."
|
|
|
|
|
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
|
|
|
|
/>
|
|
|
|
|
<div style="position:relative">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
|
|
|
|
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
|
|
|
>
|
|
|
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
|
|
|
|
Sort
|
|
|
|
|
</button>
|
|
|
|
|
<Show when={sortMenuOpen()}>
|
|
|
|
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
|
|
|
{(['submitted_desc', 'submitted_asc', 'priority_desc', 'priority_asc'] as const).map((s, i) => (
|
|
|
|
|
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
|
|
|
|
|
{['Submitted (Newest)', 'Submitted (Oldest)', 'Priority (High-Low)', 'Priority (Low-High)'][i]}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="position:relative">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
|
|
|
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
|
|
|
>
|
|
|
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
|
|
|
|
Filters
|
|
|
|
|
</button>
|
|
|
|
|
<Show when={filterMenuOpen()}>
|
|
|
|
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
|
|
|
{(['all', 'pending', 'flagged'] as const).map((s) => (
|
|
|
|
|
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
|
|
|
|
{s === 'all' ? 'All Cases' : s === 'pending' ? 'Pending Review' : 'Flagged'}
|
|
|
|
|
</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
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
<table class="min-w-full">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr style="background:#0D0D2A;text-align:left">
|
|
|
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Verification ID</th>
|
|
|
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Applicant</th>
|
|
|
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Type</th>
|
|
|
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Docs</th>
|
|
|
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Priority</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">Submitted</th>
|
|
|
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<For each={filteredRows()}>
|
|
|
|
|
{(row) => (
|
|
|
|
|
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
|
|
|
|
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">{row.id}</td>
|
|
|
|
|
<td style="padding:12px 20px">
|
|
|
|
|
<p style="font-size:14px;font-weight:600;color:#111827">{row.applicantName}</p>
|
|
|
|
|
<p style="font-size:11px;color:#6B7280">{row.userType}</p>
|
|
|
|
|
</td>
|
|
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.verificationType}</td>
|
|
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{Number(row.documentsCount || 0)} docs</td>
|
|
|
|
|
<td style="padding:12px 20px"><PriorityBadge priority={row.priority} /></td>
|
|
|
|
|
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
|
|
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{formatDate(row.submittedDate || row.updatedAt)}</td>
|
|
|
|
|
<td style="padding:12px 20px;position:relative">
|
|
|
|
|
<button type="button" onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#9CA3AF;background:none;border:none;cursor:pointer">
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><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>
|
|
|
|
|
</button>
|
|
|
|
|
<Show when={openMenuId() === row.id}>
|
|
|
|
|
<div style="position:absolute;right:20px;top:44px;z-index:20;width:190px;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" onClick={() => openView(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">View Verification</button>
|
|
|
|
|
<button type="button" style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">Mark Verified</button>
|
|
|
|
|
<button type="button" style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">Flag Case</button>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
ui(step-4): apply reference layout to remaining 7 pages
- catering-services, fitness-trainers, graphic-designers, social-media-managers,
video-editors, verification, kb: white header, data-table/table-card,
navy buttons, orange tab underlines, inline styles removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 05:23:57 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-27 05:35:18 +01:00
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={view() === 'form'}>
|
|
|
|
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
|
|
|
<button type="button" onClick={() => { setView('list'); setListTab('all'); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
|
|
|
|
|
All Verifications
|
|
|
|
|
</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">
|
|
|
|
|
Create Rule
|
|
|
|
|
</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">
|
|
|
|
|
<div style="padding:24px">
|
|
|
|
|
<Show when={formError()}>
|
|
|
|
|
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
|
|
|
|
{formError()}
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
|
|
|
|
<label style="display:block">
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#374151">Rule Name<span style="margin-left:2px;color:#FF5E13">*</span></span>
|
|
|
|
|
<input value={ruleName()} onInput={(e) => setRuleName(e.currentTarget.value)} placeholder="e.g. Professional Identity Review" style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box" />
|
|
|
|
|
</label>
|
|
|
|
|
<label style="display:block">
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#374151">User Type</span>
|
|
|
|
|
<select value={ruleUserType()} onChange={(e) => setRuleUserType(e.currentTarget.value as VerificationRecord['userType'])} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none;box-sizing:border-box">
|
|
|
|
|
{(['CUSTOMER', 'PROFESSIONAL', 'COMPANY', 'JOBSEEKER'] as const).map((t) => <option value={t}>{t}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px">
|
|
|
|
|
<label style="display:block">
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#374151">Verification Type</span>
|
|
|
|
|
<select value={ruleVerificationType()} onChange={(e) => setRuleVerificationType(e.currentTarget.value as VerificationRecord['verificationType'])} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none;box-sizing:border-box">
|
|
|
|
|
{(['IDENTITY', 'BUSINESS', 'PROFILE', 'DOCUMENT', 'MIXED'] as const).map((t) => <option value={t}>{t}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
<div>
|
|
|
|
|
<p style="font-size:13px;font-weight:600;color:#374151">Rule Status</p>
|
|
|
|
|
<div style="margin-top:8px;display:flex;gap:10px">
|
|
|
|
|
<button type="button" onClick={() => setRuleActive(true)} style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${ruleActive() ? '#FF5E13' : '#E5E7EB'};background:${ruleActive() ? '#FFF3EE' : 'white'};color:${ruleActive() ? '#FF5E13' : '#6B7280'}`}>Active</button>
|
|
|
|
|
<button type="button" onClick={() => setRuleActive(false)} style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${!ruleActive() ? '#FF5E13' : '#E5E7EB'};background:${!ruleActive() ? '#FFF3EE' : 'white'};color:${!ruleActive() ? '#FF5E13' : '#6B7280'}`}>Inactive</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-top:24px;display:flex;justify-content:flex-end;gap:10px">
|
|
|
|
|
<button type="button" onClick={() => { setView('list'); setListTab('all'); resetRuleForm(); }} style="height:40px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Cancel</button>
|
|
|
|
|
<button type="button" onClick={saveRule} style="height:40px;border-radius:10px;background:#0D0D2A;padding:0 18px;font-size:13px;font-weight:700;color:white;border:none;cursor:pointer">Save Rule</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-top:24px;border:1px solid #E5E7EB;border-radius:16px;background:white;overflow:hidden">
|
|
|
|
|
<table style="width:100%;border-collapse:collapse">
|
|
|
|
|
<thead style="background:#0D0D2A">
|
|
|
|
|
<tr style="text-align:left">
|
|
|
|
|
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Rule Name</th>
|
|
|
|
|
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">User Type</th>
|
|
|
|
|
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Verification Type</th>
|
|
|
|
|
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Status</th>
|
|
|
|
|
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Updated</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<For each={rules()}>
|
|
|
|
|
{(rule) => (
|
|
|
|
|
<tr style="border-top:1px solid #E5E7EB">
|
|
|
|
|
<td style="padding:14px 20px;font-size:14px;font-weight:600;color:#111827">{rule.name}</td>
|
|
|
|
|
<td style="padding:14px 20px;font-size:13px;color:#6B7280">{rule.userType}</td>
|
|
|
|
|
<td style="padding:14px 20px;font-size:13px;color:#6B7280">{rule.verificationType}</td>
|
|
|
|
|
<td style="padding:14px 20px"><StatusBadge status={rule.status} /></td>
|
|
|
|
|
<td style="padding:14px 20px;font-size:13px;color:#6B7280">{formatDate(rule.updatedAt)}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
ui(step-4): apply reference layout to remaining 7 pages
- catering-services, fitness-trainers, graphic-designers, social-media-managers,
video-editors, verification, kb: white header, data-table/table-card,
navy buttons, orange tab underlines, inline styles removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 05:23:57 +01:00
|
|
|
</div>
|
2026-03-19 14:21:49 +01:00
|
|
|
</AdminShell>
|
|
|
|
|
);
|
|
|
|
|
}
|