/** * VerificationStatusPage — shows the user their current verification state. * Handles: NOT_SUBMITTED, PENDING, UNDER_REVIEW, DOCUMENTS_REQUESTED, * REVISION_REQUESTED, APPROVED, REJECTED. * Tabs: approval status, documents, activity */ import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; import { ShieldCheck, FileText, Activity } from 'lucide-solid'; import { CARD, BTN_ORANGE, BTN_GHOST } from '~/components/DashboardShell'; import ProfilePage from '~/components/dashboard/ProfilePage'; import PortfolioPage from '~/components/dashboard/PortfolioPage'; import { getDocFields } from '~/lib/profile-fields-config'; import { presignDocumentUrl } from '~/lib/api'; const NAVY = '#0D0D2A'; const ORANGE = '#FF5E13'; type TabKey = 'approval_status' | 'documents' | 'activity'; // The K8s ingress routes /api/* directly to the Rust gateway service in // production — there is no /api/gateway rewrite layer, so paths (already // fully-qualified /api/... from callers) are hit as-is. async function apiFetch(path: string, opts?: RequestInit) { const token = typeof window !== "undefined" ? window.sessionStorage.getItem("nxtgauge_access_token") || "" : ""; return fetch(path, { ...opts, credentials: "include", headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(opts?.headers ?? {}), }, }); } // ── Status config ───────────────────────────────────────────────────────────── const STATUS_CONFIG: Record = { NOT_SUBMITTED: { emoji: '📋', label: 'Not Submitted', color: '#6B7280', bg: '#F9FAFB', border: '#E5E7EB', description: 'Complete your My Profile and My Portfolio, then submit for verification to start using the platform.', }, PENDING: { emoji: '⏳', label: 'Pending Review', color: '#92400E', bg: '#FFFBEB', border: '#FDE68A', description: 'Your profile has been submitted and is in the review queue. We typically respond within 24–48 hours.', }, UNDER_REVIEW: { emoji: '🔍', label: 'Under Review', color: '#1E40AF', bg: '#EEF2FF', border: '#BFDBFE', description: 'Our team is actively reviewing your submission. You will be notified once a decision is made.', }, DOCUMENTS_REQUESTED: { emoji: '📎', label: 'Documents Requested', color: '#C2410C', bg: '#FFF7ED', border: '#FED7AA', description: 'Admin has requested additional or clearer documents. Please review the message below and resubmit.', }, REVISION_REQUESTED: { emoji: '✏️', label: 'Revision Requested', color: '#C2410C', bg: '#FFF7ED', border: '#FED7AA', description: 'Admin has requested changes to your profile information. Please update and resubmit.', }, APPROVED: { emoji: '✅', label: 'Approved', color: '#065F46', bg: '#ECFDF5', border: '#6EE7B7', description: 'Your profile has been verified and approved. You now have full access to the platform.', }, COMPLETED: { emoji: '✅', label: 'Approved', color: '#065F46', bg: '#ECFDF5', border: '#6EE7B7', description: 'Your profile has been verified and approved. You now have full access to the platform.', }, REJECTED: { emoji: '❌', label: 'Rejected', color: '#991B1B', bg: '#FEF2F2', border: '#FECACA', description: 'Your verification was rejected. Please review the reason below, update your profile, and resubmit.', }, }; const FLOW_STEPS = [ { key: 'submit', label: 'Submit Profile' }, { key: 'review', label: 'Under Review' }, { key: 'verify', label: 'Verified' }, { key: 'approved', label: 'Approved' }, ]; function stepIndex(status: string): number { switch (status) { case 'NOT_SUBMITTED': return 0; case 'PENDING': return 1; case 'UNDER_REVIEW': return 2; case 'DOCUMENTS_REQUESTED': case 'REVISION_REQUESTED': return 2; case 'COMPLETED': case 'APPROVED': return 4; default: return 1; } } // ── Component ───────────────────────────────────────────────────────────────── interface Props { roleKey: string; onNavigate?: (sidebar: string) => void; onVerificationStatusChange?: (status: string) => void; runtimeFields?: string[]; runtimeTabs?: string[]; } interface VerificationDocument { type: string; value: any; status: string; } interface VerificationStatusResponse { status: string; reference_number: string; document_request?: string | null; rejection_reason?: string | null; updated_at?: string | null; activity_log?: Array<{ date: string; action: string; details: string }>; documents: VerificationDocument[]; } // Best-effort extraction of a viewable URL from a document's `value` payload, // which may be a raw URL string or an upload-response object. function docUrl(value: any): string | null { if (!value) return null; if (typeof value === 'string') return value; if (typeof value === 'object') { return value.url ?? value.file_url ?? value.fileUrl ?? value.path ?? null; } return null; } export default function VerificationStatusPage(props: Props) { const [activeTab, setActiveTab] = createSignal('approval_status'); const [inlineSection, setInlineSection] = createSignal<'profile' | 'portfolio'>('profile'); const [status, setStatus] = createSignal('NOT_SUBMITTED'); const [referenceNumber, setReferenceNumber] = createSignal(null); const [docRequest, setDocRequest] = createSignal(null); const [rejectionReason, setRejectionReason] = createSignal(null); const [updatedAt, setUpdatedAt] = createSignal(null); const [loading, setLoading] = createSignal(true); const [resubmitting, setResubmitting] = createSignal(false); const [resubmitMsg, setResubmitMsg] = createSignal(''); const [activityLog, setActivityLog] = createSignal>([]); const [documents, setDocuments] = createSignal([]); onMount(async () => { try { const res = await apiFetch(`/api/me/verification-status?roleKey=${props.roleKey}`); if (res.ok) { const d: VerificationStatusResponse = await res.json(); const nextStatus = String(d.status ?? 'NOT_SUBMITTED'); setStatus(nextStatus); props.onVerificationStatusChange?.(nextStatus); setReferenceNumber(d.reference_number ?? null); setDocRequest(d.document_request ?? null); setRejectionReason(d.rejection_reason ?? null); setUpdatedAt(d.updated_at ?? null); if (d.activity_log) { setActivityLog(d.activity_log); } if (d.documents) { setDocuments(d.documents); } } } finally { setLoading(false); } }); const cfg = () => STATUS_CONFIG[status()] ?? STATUS_CONFIG.NOT_SUBMITTED; const currentStep = () => stepIndex(status()); const docFields = createMemo(() => getDocFields(props.roleKey)); const canResubmit = () => ['DOCUMENTS_REQUESTED', 'REVISION_REQUESTED', 'REJECTED'].includes(status()); const isApproved = () => ['APPROVED', 'COMPLETED'].includes(status()); // Hide the inline form editors once submitted — PENDING/UNDER_REVIEW don't // need editing, only statuses that require user action (NOT_SUBMITTED, // DOCUMENTS_REQUESTED, REVISION_REQUESTED, REJECTED) do. const showInlineEditors = () => ['NOT_SUBMITTED', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED', 'REJECTED'].includes(status()); const hasPortfolio = () => { const role = String(props.roleKey || '').toUpperCase(); return role === 'JOB_SEEKER' || role !== 'COMPANY' && role !== 'CUSTOMER'; }; const handleResubmit = async () => { setResubmitting(true); setResubmitMsg(''); try { const res = await apiFetch('/api/profile/submit-for-verification', { method: 'POST', body: JSON.stringify({ roleKey: props.roleKey }), }); const d = await res.json().catch(() => ({})); if (res.ok) { setStatus('PENDING'); props.onVerificationStatusChange?.('PENDING'); setDocRequest(null); setRejectionReason(null); setResubmitMsg('Resubmitted successfully! We will review your profile.'); } else if (res.status === 409) { setResubmitMsg(d.error ?? 'A verification is already in progress.'); } else { setResubmitMsg(d.error ?? 'Resubmission failed. Please try again.'); } } catch { setResubmitMsg('Network error. Please try again.'); } finally { setResubmitting(false); } }; const TABS: { key: TabKey; label: string; Icon: any }[] = [ { key: 'approval_status', label: 'Approval Status', Icon: ShieldCheck }, { key: 'documents', label: 'Documents', Icon: FileText }, { key: 'activity', label: 'Activity', Icon: Activity }, ]; return (

Verification Portal

Track verification progress, documents, and updates.

{referenceNumber()}

{/* Tabs */}
{(tab) => ( )}
Loading verification status…
{/* ── Approval Status Tab ─────────────────────────────────────── */} {/* Main status card */}
{cfg().emoji}

Verification Status

{cfg().label}

{cfg().description}

Last updated: {new Date(updatedAt()!).toLocaleString('en-IN')}

{/* Doc request / rejection reason */}

Document Request from Admin

{docRequest()}

Rejection Reason

{rejectionReason()}

{/* Progress timeline */}

Verification Progress

{(step, idx) => ( <>
idx() ? '#FF5E13' : currentStep() === idx() + 1 ? '#FFF3EE' : '#F3F4F6', color: currentStep() > idx() ? '#fff' : currentStep() === idx() + 1 ? '#FF5E13' : '#9CA3AF', border: currentStep() === idx() + 1 ? '2px solid #FF5E13' : '2px solid transparent', }}> {currentStep() > idx() ? '✓' : idx() + 1}

idx() || currentStep() === idx() + 1 ? '#374151' : '#9CA3AF', 'white-space': 'nowrap', 'text-align': 'center' }}> {step.label}

idx() ? '#FF5E13' : '#E5E7EB', 'margin-bottom': '18px' }} /> )}
{/* Actions */}

{resubmitMsg()}

🎉

You're Verified!

Your profile is approved. Start exploring opportunities on Nxtgauge.

Complete Verification Details Here

Fill the required fields directly on this page, then submit for verification.

{ setStatus(s); props.onVerificationStatusChange?.(s); }} onNavigate={props.onNavigate} />
{/* ── Documents Tab ───────────────────────────────────────────── */}

Submitted Documents

No documents submitted yet. Complete your profile to submit documents for verification.

Document Request

{docRequest()}

Rejection Reason

{rejectionReason()}

0 ? documents() : docFields().map((f) => ({ type: f.key, value: null, status: status() }))}> {(doc) => { const fieldLabel = () => docFields().find((f) => f.key === doc.type)?.label ?? doc.type; const url = () => docUrl(doc.value); return (

{fieldLabel()}

No file uploaded

} >

{doc.status === 'APPROVED' ? '✓ Verified' : doc.status === 'REJECTED' ? '✕ Rejected' : doc.status === 'SUBMITTED' ? '◌ Submitted' : '◌ Pending review'}

); }}
{/* ── Activity Tab ────────────────────────────────────────────── */}

Verification Activity

Profile created

{updatedAt() ? new Date(updatedAt()!).toLocaleString('en-IN') : 'Just now'}

Profile submitted for verification

{updatedAt() ? new Date(updatedAt()!).toLocaleString('en-IN') : 'Recently'}

Profile approved

{updatedAt() ? new Date(updatedAt()!).toLocaleString('en-IN') : 'Recently'}

0}>
{(item) => (

{item.action}

{item.date}

{item.details}

)}
); }