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; documents?: Array<{ name: string; fileName: string; state: 'APPROVED' | 'RECEIVED' | 'MISSING' | 'REQUESTED'; }>; requestedDocuments?: string[]; roleTags?: string[]; primaryService?: string; area?: string; userId?: string; roleKey?: string; 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} ); } const DEFAULT_DOCS_BY_USER_TYPE: Record = { COMPANY: ['GST Certificate', 'PAN Card', 'Incorporation Certificate'], CUSTOMER: ['Identity Proof', 'Address Proof'], PROFESSIONAL: ['Identity Proof', 'Address Proof', 'Portfolio Ownership'], JOBSEEKER: ['Identity Proof', 'Address Proof', 'Resume'], }; const toTitle = (value: string) => String(value || '') .replace(/_/g, ' ') .replace(/\b\w/g, (c) => c.toUpperCase()); type SubmissionData = { role_key?: string; onboarding?: { status?: string; progress_json?: Record; } | null; }; function extractRoleTags(source: any): string[] { const values: string[] = []; const pushValue = (value: unknown) => { if (!value) return; if (Array.isArray(value)) { value.forEach((v) => pushValue(v)); return; } const text = String(value || '').trim(); if (!text) return; values.push(text); }; pushValue(source?.role_key); pushValue(source?.roleKey); pushValue(source?.role_keys); pushValue(source?.roleKeys); pushValue(source?.roles); pushValue(source?.categories); pushValue(source?.category); pushValue(source?.service_category); pushValue(source?.serviceCategory); pushValue(source?.profession); pushValue(source?.service_type); const unique = Array.from(new Set(values.map((v) => toTitle(v)))); return unique.slice(0, 4); } function flattenFields(obj: Record, prefix = ''): Array<{ key: string; value: string }> { const result: Array<{ key: string; value: string }> = []; for (const [k, v] of Object.entries(obj || {})) { const label = prefix ? `${prefix}.${k}` : k; if (v !== null && typeof v === 'object' && !Array.isArray(v)) { result.push(...flattenFields(v as Record, label)); } else if (Array.isArray(v)) { result.push({ key: label, value: v.map((item) => String(item ?? '')).join(', ') }); } else { result.push({ key: label, value: String(v ?? '') }); } } return result; } async function loadSubmissionDetails(userId: string, roleKey: string): Promise { if (!userId) return null; const qs = roleKey ? `?roleKey=${encodeURIComponent(roleKey)}` : ''; try { const res = await fetch(`${API}/api/admin/approvals/submission/${userId}${qs}`); if (!res.ok) return null; return res.json(); } catch { return null; } } export default function VerificationManagementPage() { const [listTab, setListTab] = createSignal<'all' | 'view' | 'flagged'>('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 [requestNote, setRequestNote] = createSignal(''); const [requestingDocs, setRequestingDocs] = createSignal([]); const [error, setError] = createSignal(''); const [loadingLinkedDetails, setLoadingLinkedDetails] = createSignal(false); const [actionNote, setActionNote] = createSignal(''); 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 buildDocuments = (userType: VerificationRecord['userType'], source: any) => { const names = DEFAULT_DOCS_BY_USER_TYPE[userType]; const provided = Array.isArray(source?.documents) ? source.documents : []; const mapped = names.map((name, index) => { const fromPayload = provided[index]; const fileName = String( fromPayload?.file_name || fromPayload?.fileName || fromPayload?.name || `${name.toLowerCase().replace(/\s+/g, '_')}.pdf`, ); const stateRaw = String(fromPayload?.state || fromPayload?.status || '').toUpperCase(); const state: 'APPROVED' | 'RECEIVED' | 'MISSING' | 'REQUESTED' = stateRaw === 'APPROVED' || stateRaw === 'VERIFIED' ? 'APPROVED' : stateRaw === 'REQUESTED' || stateRaw === 'RE_UPLOAD_REQUESTED' ? 'REQUESTED' : stateRaw === 'RECEIVED' ? 'RECEIVED' : index === names.length - 1 ? 'MISSING' : 'APPROVED'; return { name, fileName, state }; }); const requested = mapped.filter((d) => d.state === 'REQUESTED' || d.state === 'MISSING').map((d) => d.name); return { mapped, requested }; }; const jobCases: VerificationRecord[] = jobs.map((job: any) => { const docs = buildDocuments('COMPANY', job); return { 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: docs.mapped.length, documents: docs.mapped, requestedDocuments: docs.requested, roleTags: extractRoleTags(job), primaryService: String(job.category || job.department || job.role || 'Job Posting'), area: String(job.location || job.city || job.work_mode || '—'), userId: String(job.user_id || job.created_by || ''), roleKey: String(job.role_key || job.roleKey || ''), assignedVerifier: 'Unassigned', priority: 'HIGH', status: docs.requested.length ? 'RE_UPLOAD_REQUESTED' : 'IN_REVIEW', updatedAt: String(job.updated_at || job.created_at || ''), }; }); const requirementCases: VerificationRecord[] = requirements.map((req: any) => { const docs = buildDocuments('CUSTOMER', req); return { 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: docs.mapped.length, documents: docs.mapped, requestedDocuments: docs.requested, roleTags: extractRoleTags(req), primaryService: String(req.category || req.profession || req.service_type || 'Requirement'), area: String(req.location || req.city || req.area || '—'), userId: String(req.user_id || req.created_by || ''), roleKey: String(req.role_key || req.roleKey || ''), assignedVerifier: 'Unassigned', priority: 'MEDIUM', status: docs.requested.length ? 'RE_UPLOAD_REQUESTED' : '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 flaggedCount = createMemo(() => rows().filter((r) => r.status === 'FLAGGED').length); const reviewCount = createMemo(() => rows().filter((r) => r.status === 'PENDING' || r.status === 'IN_REVIEW').length); const setCaseStatus = (id: string, status: VerificationRecord['status']) => { const now = new Date().toISOString(); setRows((prev) => prev.map((r) => (r.id === id ? { ...r, status, updatedAt: now } : r))); setViewingCase((prev) => (prev && prev.id === id ? { ...prev, status, updatedAt: now } : prev)); }; const markVerifiedCase = (row: VerificationRecord) => { const now = new Date().toISOString(); setRows((prev) => prev.map((r) => { if (r.id !== row.id) return r; return { ...r, status: 'VERIFIED', requestedDocuments: [], documents: (r.documents || []).map((d) => (d.state === 'MISSING' || d.state === 'REQUESTED' ? { ...d, state: 'RECEIVED' } : d)), updatedAt: now, }; })); setViewingCase((prev) => { if (!prev || prev.id !== row.id) return prev; return { ...prev, status: 'VERIFIED', requestedDocuments: [], documents: (prev.documents || []).map((d) => (d.state === 'MISSING' || d.state === 'REQUESTED' ? { ...d, state: 'RECEIVED' } : d)), updatedAt: now, }; }); setOpenMenuId(null); }; const requestReuploadCase = (row: VerificationRecord, requested: string[]) => { const fallback = (row.documents || []).filter((d) => d.state === 'MISSING' || d.state === 'REQUESTED').map((d) => d.name); const nextRequested = requested.length ? requested : fallback; if (!nextRequested.length) return; const now = new Date().toISOString(); const requestedSet = new Set(nextRequested); setRows((prev) => prev.map((r) => { if (r.id !== row.id) return r; return { ...r, status: 'RE_UPLOAD_REQUESTED', requestedDocuments: nextRequested, documents: (r.documents || []).map((d) => (requestedSet.has(d.name) ? { ...d, state: 'REQUESTED' } : d)), updatedAt: now, }; })); setViewingCase((prev) => { if (!prev || prev.id !== row.id) return prev; return { ...prev, status: 'RE_UPLOAD_REQUESTED', requestedDocuments: nextRequested, documents: (prev.documents || []).map((d) => (requestedSet.has(d.name) ? { ...d, state: 'REQUESTED' } : d)), updatedAt: now, }; }); setOpenMenuId(null); }; const enrichCaseFromSubmission = async (row: VerificationRecord) => { if (!row.userId) return; setLoadingLinkedDetails(true); try { const submission = await loadSubmissionDetails(row.userId || '', row.roleKey || ''); const progress = (submission?.onboarding?.progress_json && typeof submission.onboarding.progress_json === 'object') ? submission.onboarding.progress_json : {}; const flattened = flattenFields(progress as Record); const getField = (...hints: string[]) => { const hit = flattened.find((field) => hints.some((hint) => field.key.toLowerCase().includes(hint))); return String(hit?.value || '').trim(); }; const linkedDocs = flattened .filter((field) => { const key = field.key.toLowerCase(); const value = String(field.value || '').toLowerCase(); const isDocLike = /(document|doc|file|upload|resume|cv|certificate|license|id|proof|portfolio)/.test(key); const hasPath = value.startsWith('http') || value.startsWith('/'); return isDocLike && (hasPath || value.endsWith('.pdf') || value.endsWith('.jpg') || value.endsWith('.jpeg') || value.endsWith('.png')); }) .slice(0, 8) .map((field) => { const nameParts = field.key.split('.'); const baseName = nameParts[nameParts.length - 1] || field.key; const humanName = toTitle(baseName.replace(/_/g, ' ').replace(/file|upload|url/gi, '').trim() || baseName); const fileName = String(field.value || '').split('/').pop() || `${humanName.toLowerCase().replace(/\s+/g, '_')}.pdf`; const requestedSet = new Set(row.requestedDocuments || []); return { name: humanName, fileName, state: (requestedSet.has(humanName) ? 'REQUESTED' : 'RECEIVED') as 'REQUESTED' | 'RECEIVED', }; }); const nextRoleTags = Array.from(new Set([ ...extractRoleTags(progress), ...extractRoleTags({ role_key: submission?.role_key }), ...(row.roleTags || []), ])).slice(0, 5); const nextPrimary = getField('category', 'profession', 'service', 'role', 'job_title') || row.primaryService || '—'; const nextArea = getField('location', 'city', 'area', 'place', 'venue') || row.area || '—'; const nextDocs = linkedDocs.length ? linkedDocs : (row.documents || []); setRows((prev) => prev.map((item) => { if (item.id !== row.id) return item; return { ...item, roleTags: nextRoleTags.length ? nextRoleTags : item.roleTags, primaryService: nextPrimary, area: nextArea, documents: nextDocs, documentsCount: nextDocs.length || item.documentsCount, roleKey: item.roleKey || String(submission?.role_key || ''), }; })); setViewingCase((prev) => { if (!prev || prev.id !== row.id) return prev; return { ...prev, roleTags: nextRoleTags.length ? nextRoleTags : prev.roleTags, primaryService: nextPrimary, area: nextArea, documents: nextDocs, documentsCount: nextDocs.length || prev.documentsCount, roleKey: prev.roleKey || String(submission?.role_key || ''), }; }); } finally { setLoadingLinkedDetails(false); } }; const requestDocumentsPersist = async (row: VerificationRecord, requested: string[], note: string) => { if (!row.userId) return false; const roleKey = String(row.roleKey || '').toUpperCase(); const payload = { documents: requested, note: note || 'Please upload the missing documents.' }; const endpoints: string[] = []; if (roleKey && roleKey !== 'COMPANY' && roleKey !== 'CUSTOMER') { endpoints.push(`${API}/api/admin/approvals/profiles/professional/${roleKey}/${row.userId}/request-documents`); } if (roleKey === 'COMPANY') endpoints.push(`${API}/api/admin/approvals/profiles/company/${row.userId}/request-documents`); if (roleKey === 'CUSTOMER') endpoints.push(`${API}/api/admin/approvals/profiles/customer/${row.userId}/request-documents`); endpoints.push(`${API}/api/admin/approvals/submission/${row.userId}/request-documents`); for (const endpoint of endpoints) { try { const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(payload), }); if (res.ok) return true; } catch { // Try the next known endpoint shape. } } return false; }; const flagCase = (row: VerificationRecord) => { setCaseStatus(row.id, 'FLAGGED'); setOpenMenuId(null); }; const toggleRequestedDocument = (name: string) => { setRequestingDocs((prev) => ( prev.includes(name) ? prev.filter((v) => v !== name) : [...prev, name] )); }; const requestMissingDocuments = async (row: VerificationRecord) => { const selected = requestingDocs(); if (!selected.length) return; const persisted = await requestDocumentsPersist(row, selected, requestNote().trim()); requestReuploadCase(row, selected); setActionNote( persisted ? 'Document request sent successfully.' : 'Document request updated in dashboard state. API endpoint for persistence was not available.', ); setRequestNote(''); }; const quickRequestDocuments = async (row: VerificationRecord, selected: string[]) => { if (!selected.length) return; const persisted = await requestDocumentsPersist(row, selected, ''); requestReuploadCase(row, selected); setActionNote( persisted ? 'Document request sent successfully.' : 'Document request updated in dashboard state. API endpoint for persistence was not available.', ); }; const exportRows = () => { const data = filteredRows(); const headers = ['Verification ID', 'Applicant', 'User Type', 'Verification Type', 'Documents', 'Priority', 'Status', 'Submitted Date']; const lines = data.map((row) => [ row.id, row.applicantName || '', row.userType, row.verificationType, String(row.documentsCount || 0), row.priority, row.status, formatDate(row.submittedDate || row.updatedAt), ]); const csv = [headers, ...lines] .map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `verification-cases-${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const openView = (row: VerificationRecord) => { setActionNote(''); setViewingCase(row); setDetailTab('overview'); setListTab('view'); setOpenMenuId(null); setRequestNote(''); setRequestingDocs(row.requestedDocuments?.length ? row.requestedDocuments : (row.documents || []).filter((d) => d.state === 'MISSING').map((d) => d.name)); void enrichCaseFromSubmission(row); }; return ( {/* Page header */} Verification Management Manage user identity and business verification workflows {error()} {actionNote()} {/* ── LIST VIEW ── */} { setListTab('all'); setStatusFilter('all'); setViewingCase(null); }} style={`display:flex;align-items:center;justify-content:space-between;height:38px;border-radius:8px;padding:0 12px;font-size:12.5px;font-weight:500;border:none;cursor:pointer;${listTab() === 'all' ? 'background:#FFF3EE;color:#FF5E13' : 'background:white;color:#6B7280'}`} > Verification Queue {rows().length} { setListTab('view'); }} style={`display:flex;align-items:center;justify-content:space-between;height:38px;border-radius:8px;padding:0 12px;font-size:12.5px;font-weight:500;border:none;cursor:pointer;${listTab() === 'view' ? 'background:#FFF3EE;color:#FF5E13' : 'background:white;color:#6B7280'}`} > View Verification { setListTab('flagged'); setStatusFilter('flagged'); setViewingCase(null); }} style={`display:flex;align-items:center;justify-content:space-between;height:38px;border-radius:8px;padding:0 12px;font-size:12.5px;font-weight:500;border:none;cursor:pointer;${listTab() === 'flagged' ? 'background:#FFF3EE;color:#FF5E13' : 'background:white;color:#6B7280'}`} > Flagged Cases {flaggedCount()} Pending Review: {reviewCount()} No verification selected Click the ⋮ menu on any row and choose View Verification. {viewingCase()!.applicantName} ID: {viewingCase()!.id} • {viewingCase()!.verificationType} • Submitted: {formatDate(viewingCase()!.submittedDate)} markVerifiedCase(viewingCase()!)} 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 requestMissingDocuments(viewingCase()!)} style={`height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:${requestingDocs().length ? 'pointer' : 'not-allowed'};opacity:${requestingDocs().length ? 1 : 0.55}`} > Request Documents {(['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 Syncing linked submission details... Applicant Name{viewingCase()!.applicantName} User Type{viewingCase()!.userType} Primary Service{viewingCase()!.primaryService || '—'} Area / Place{viewingCase()!.area || '—'} Assigned Verifier{viewingCase()!.assignedVerifier} Documents{Number(viewingCase()!.documentsCount || 0)} Registered Roles / Services {(tag) => ( {tag} )} Request Missing Documents Select missing documents and send one clear request to the user. {(doc) => ( toggleRequestedDocument(doc.name)} style="width:14px;height:14px;accent-color:#FF5E13" /> {doc.name} )} setRequestNote(e.currentTarget.value)} placeholder="Optional note for user (e.g. upload clear PDF/JPG, full document visible)..." style="margin-top:10px;width:100%;height:72px;border-radius:8px;border:1px solid #E5E7EB;padding:10px;font-size:12px;resize:none" /> requestMissingDocuments(viewingCase()!)} disabled={!requestingDocs().length} style={`height:34px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 14px;font-size:12px;font-weight:700;cursor:${requestingDocs().length ? 'pointer' : 'not-allowed'};opacity:${requestingDocs().length ? 1 : 0.55}`} > Send Document Request 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.name} {doc.fileName} Preview toggleRequestedDocument(doc.name)} style="font-size:12px;color:#0D0D2A;background:none;border:none;cursor:pointer;font-weight:600"> {requestingDocs().includes(doc.name) ? 'Unselect' : 'Request'} )} {[ '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.area ? ` • ${row.area}` : ''} {(row.roleTags || []).join(', ')} {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 void quickRequestDocuments(row, (row.documents || []).filter((d) => d.state === 'MISSING' || d.state === 'REQUESTED').map((d) => d.name))} 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">Request Documents markVerifiedCase(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">Mark Verified flagCase(row)} 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 )} ); }
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)}
Syncing linked submission details...
Applicant Name
{viewingCase()!.applicantName}
User Type
{viewingCase()!.userType}
Primary Service
{viewingCase()!.primaryService || '—'}
Area / Place
{viewingCase()!.area || '—'}
Assigned Verifier
{viewingCase()!.assignedVerifier}
Documents
{Number(viewingCase()!.documentsCount || 0)}
Registered Roles / Services
Select missing documents and send one clear request to the user.
{step.l}
{doc.name}
{doc.fileName}
Case Review Started
Verifier started reviewing documents • 2 hours ago
Verification Request Submitted
System received applicant data • 1 day ago
{row.applicantName}
{row.userType}{row.area ? ` • ${row.area}` : ''}
{(row.roleTags || []).join(', ')}