import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; import AdminShell from '~/components/AdminShell'; 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 ( {label} ); } function PriorityBadge(props: { priority: string }) { const color = props.priority === 'HIGH' ? '#DC2626' : props.priority === 'MEDIUM' ? '#F59E0B' : '#16A34A'; return ( {props.priority} ); } export default function VerificationManagementPage() { 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([]); const [viewingCase, setViewingCase] = createSignal(null); const [openMenuId, setOpenMenuId] = createSignal(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('PROFESSIONAL'); const [ruleVerificationType, setRuleVerificationType] = createSignal('IDENTITY'); const [ruleActive, setRuleActive] = createSignal(true); const [formError, setFormError] = createSignal(''); const [error, setError] = createSignal(''); type VerificationRule = { id: string; name: string; userType: VerificationRecord['userType']; verificationType: VerificationRecord['verificationType']; status: 'ACTIVE' | 'INACTIVE'; updatedAt: string; }; const [rules, setRules] = createSignal([ { 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 () => { 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.'); } }; 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(); }; return ( {/* Page header */} Verification Management Manage user identity and business verification workflows {error()} {/* ── LIST VIEW ── */} {([ { 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) => ( {tab.label} ))} No verification selected Click the ⋮ menu on any row and choose View Verification. {viewingCase()!.applicantName} ID: {viewingCase()!.id} • {viewingCase()!.verificationType} • Submitted: {formatDate(viewingCase()!.submittedDate)} Mark Verified Request Re-upload {(['overview', 'documents', 'checklist', 'logs'] as const).map((tab, i) => { const labels = ['Overview', 'Documents', 'Checklist', 'Activity Logs']; const active = () => detailTab() === tab; return ( 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]} ); })} Case Summary Applicant Name{viewingCase()!.applicantName} User Type{viewingCase()!.userType} Assigned Verifier{viewingCase()!.assignedVerifier} Documents{Number(viewingCase()!.documentsCount || 0)} Verification Tracker {[ { l: 'Submitted', active: true }, { l: 'In Review', active: true }, { l: 'Verified', active: false }, ].map((step) => ( {step.l} ))} Notes Add Note Document Name Status Actions {(doc) => ( {doc.n} Preview Approve )} {[ 'Identity matches provided documents', 'Address proof is valid and recent', 'Business registration is authentic', 'Contact information is verified', ].map((item) => ( {item} ))} Case Review Started Verifier started reviewing documents • 2 hours ago Verification Request Submitted System received applicant data • 1 day ago { 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 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" /> { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer" > Sort {(['submitted_desc', 'submitted_asc', 'priority_desc', 'priority_asc'] as const).map((s, i) => ( { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}> {['Submitted (Newest)', 'Submitted (Oldest)', 'Priority (High-Low)', 'Priority (Low-High)'][i]} ))} { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer" > Filters {(['all', 'pending', 'flagged'] as const).map((s) => ( { 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'} ))} Export Verification ID Applicant Type Docs Priority Status Submitted Actions {(row) => ( {row.id} {row.applicantName} {row.userType} {row.verificationType} {Number(row.documentsCount || 0)} docs {formatDate(row.submittedDate || row.updatedAt)} 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"> 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 Mark Verified Flag Case )} { setView('list'); setListTab('all'); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer"> All Verifications Create Rule {formError()} Rule Name* 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" /> User Type 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) => {t})} Verification Type 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) => {t})} Rule Status 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 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 { 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 Save Rule Rule Name User Type Verification Type Status Updated {(rule) => ( {rule.name} {rule.userType} {rule.verificationType} {formatDate(rule.updatedAt)} )} ); }
Manage user identity and business verification workflows
No verification selected
Click the ⋮ menu on any row and choose View Verification.
ID: {viewingCase()!.id} • {viewingCase()!.verificationType} • Submitted: {formatDate(viewingCase()!.submittedDate)}
Applicant Name
{viewingCase()!.applicantName}
User Type
{viewingCase()!.userType}
Assigned Verifier
{viewingCase()!.assignedVerifier}
Documents
{Number(viewingCase()!.documentsCount || 0)}
{step.l}
Case Review Started
Verifier started reviewing documents • 2 hours ago
Verification Request Submitted
System received applicant data • 1 day ago
{row.applicantName}
{row.userType}
Rule Status