nxtgauge-frontend-solid/src/components/dashboard/VerificationStatusPage.tsx
Ashwin Kumar bbf11b91e1 feat(dashboard): real My Profile, My Portfolio, Verification pages
- DashboardShell: sticky sidebar + header wrapper with shared style tokens
- ProfilePage: 3-tab form (Basic, Documents, Settings) per role, save/submit-for-verification
- PortfolioPage: full CRUD wired to /api/:prefix/portfolio/me endpoints
- VerificationStatusPage: 7-state status display with progress timeline and resubmit flow
- dashboard.tsx: REAL_PAGES routing intercepts these three sidebar items and renders
  real components instead of DashboardDesignPreview mock

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 17:20:48 +02:00

363 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* VerificationStatusPage — shows the user their current verification state.
* Handles: NOT_SUBMITTED, PENDING, UNDER_REVIEW, DOCUMENTS_REQUESTED,
* REVISION_REQUESTED, APPROVED, REJECTED.
*/
import { Show, createSignal, onMount } from 'solid-js';
import { CARD, BTN_ORANGE, BTN_GHOST } from '~/components/DashboardShell';
const API = '/api/gateway';
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
});
}
// ── Status config ─────────────────────────────────────────────────────────────
const STATUS_CONFIG: Record<string, {
emoji: string;
label: string;
color: string;
bg: string;
border: string;
description: string;
}> = {
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 2448 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.',
},
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; // still at review stage
case 'APPROVED': return 4;
default: return 1;
}
}
// ── Component ─────────────────────────────────────────────────────────────────
interface Props {
roleKey: string;
onNavigate?: (sidebar: string) => void;
}
export default function VerificationStatusPage(props: Props) {
const [status, setStatus] = createSignal('NOT_SUBMITTED');
const [docRequest, setDocRequest] = createSignal<string | null>(null);
const [rejectionReason, setRejectionReason] = createSignal<string | null>(null);
const [updatedAt, setUpdatedAt] = createSignal<string | null>(null);
const [loading, setLoading] = createSignal(true);
const [resubmitting, setResubmitting] = createSignal(false);
const [resubmitMsg, setResubmitMsg] = createSignal('');
onMount(async () => {
try {
const res = await apiFetch(`/api/me/verification-status?roleKey=${props.roleKey}`);
if (res.ok) {
const d = await res.json();
setStatus(d.status ?? 'NOT_SUBMITTED');
setDocRequest(d.document_request ?? null);
setRejectionReason(d.rejection_reason ?? null);
setUpdatedAt(d.updated_at ?? null);
}
} finally {
setLoading(false);
}
});
const cfg = () => STATUS_CONFIG[status()] ?? STATUS_CONFIG.NOT_SUBMITTED;
const currentStep = () => stepIndex(status());
const canResubmit = () => ['DOCUMENTS_REQUESTED', 'REVISION_REQUESTED', 'REJECTED'].includes(status());
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');
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);
}
};
return (
<div style={{ 'max-width': '640px' }}>
<Show when={loading()}>
<div style={{ ...CARD, 'text-align': 'center', padding: '32px', color: '#9CA3AF' }}>
Loading verification status
</div>
</Show>
<Show when={!loading()}>
{/* ── Main status card ─────────────────────────────────────────── */}
<div style={{
...CARD,
background: cfg().bg,
border: `1px solid ${cfg().border}`,
'margin-bottom': '16px',
display: 'flex',
'flex-direction': 'column',
gap: '12px',
}}>
<div style={{ display: 'flex', 'align-items': 'center', gap: '12px' }}>
<span style={{ 'font-size': '36px', 'line-height': '1' }}>{cfg().emoji}</span>
<div>
<p style={{ margin: '0', 'font-size': '11px', 'text-transform': 'uppercase', 'letter-spacing': '0.08em', color: cfg().color, 'font-weight': '700' }}>
Verification Status
</p>
<p style={{ margin: '2px 0 0', 'font-size': '22px', 'font-weight': '800', color: cfg().color }}>
{cfg().label}
</p>
</div>
</div>
<p style={{ margin: '0', 'font-size': '13px', color: '#374151', 'line-height': '1.6' }}>
{cfg().description}
</p>
<Show when={updatedAt()}>
<p style={{ margin: '0', 'font-size': '11px', color: '#9CA3AF' }}>
Last updated: {new Date(updatedAt()!).toLocaleString('en-IN')}
</p>
</Show>
</div>
{/* ── Doc request / rejection reason ──────────────────────────── */}
<Show when={docRequest()}>
<div style={{
...CARD,
background: '#FFF7ED',
border: '1px solid #FED7AA',
'margin-bottom': '16px',
}}>
<p style={{ margin: '0 0 6px', 'font-size': '12px', 'font-weight': '700', 'text-transform': 'uppercase', 'letter-spacing': '0.06em', color: '#C2410C' }}>
Document Request from Admin
</p>
<p style={{ margin: '0', 'font-size': '14px', color: '#374151', 'line-height': '1.6' }}>
{docRequest()}
</p>
</div>
</Show>
<Show when={rejectionReason()}>
<div style={{
...CARD,
background: '#FEF2F2',
border: '1px solid #FECACA',
'margin-bottom': '16px',
}}>
<p style={{ margin: '0 0 6px', 'font-size': '12px', 'font-weight': '700', 'text-transform': 'uppercase', 'letter-spacing': '0.06em', color: '#B91C1C' }}>
Rejection Reason
</p>
<p style={{ margin: '0', 'font-size': '14px', color: '#374151', 'line-height': '1.6' }}>
{rejectionReason()}
</p>
</div>
</Show>
{/* ── Progress timeline ──────────────────────────────────────── */}
<Show when={status() !== 'APPROVED'}>
<div style={{ ...CARD, 'margin-bottom': '16px' }}>
<p style={{ margin: '0 0 14px', 'font-size': '14px', 'font-weight': '700', color: '#111827' }}>
Verification Progress
</p>
<div style={{ display: 'flex', 'align-items': 'center', gap: '0' }}>
{FLOW_STEPS.map((step, idx) => {
const done = currentStep() > idx;
const active = currentStep() === idx + 1;
return (
<>
<div style={{ display: 'flex', 'flex-direction': 'column', 'align-items': 'center', 'flex-shrink': '0' }}>
<div style={{
width: '28px',
height: '28px',
'border-radius': '999px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
'font-size': '11px',
'font-weight': '800',
background: done ? '#FF5E13' : active ? '#FFF3EE' : '#F3F4F6',
color: done ? '#fff' : active ? '#FF5E13' : '#9CA3AF',
border: active ? '2px solid #FF5E13' : '2px solid transparent',
}}>
{done ? '✓' : idx + 1}
</div>
<p style={{
margin: '4px 0 0',
'font-size': '10px',
'font-weight': '600',
color: done || active ? '#374151' : '#9CA3AF',
'white-space': 'nowrap',
'text-align': 'center',
}}>
{step.label}
</p>
</div>
{idx < FLOW_STEPS.length - 1 && (
<div style={{
flex: '1',
height: '2px',
background: done ? '#FF5E13' : '#E5E7EB',
'margin-bottom': '18px',
}} />
)}
</>
);
})}
</div>
</div>
</Show>
{/* ── Actions ───────────────────────────────────────────────── */}
<div style={{ display: 'flex', gap: '10px', 'flex-wrap': 'wrap' }}>
<Show when={status() === 'NOT_SUBMITTED'}>
<button
type="button"
onClick={() => props.onNavigate?.('My Profile')}
style={BTN_ORANGE}
>
Fill My Profile
</button>
<button
type="button"
onClick={() => props.onNavigate?.('My Portfolio')}
style={BTN_GHOST}
>
Fill My Portfolio
</button>
</Show>
<Show when={canResubmit()}>
<button
type="button"
onClick={() => props.onNavigate?.('My Profile')}
style={BTN_GHOST}
>
Update My Profile
</button>
<button
type="button"
onClick={handleResubmit}
disabled={resubmitting()}
style={{ ...BTN_ORANGE, opacity: resubmitting() ? '0.7' : '1' }}
>
{resubmitting() ? 'Resubmitting…' : 'Resubmit for Verification'}
</button>
</Show>
</div>
<Show when={resubmitMsg()}>
<p style={{
margin: '12px 0 0',
'font-size': '13px',
'font-weight': '600',
color: resubmitMsg().includes('successfully') ? '#10B981' : '#EF4444',
}}>
{resubmitMsg()}
</p>
</Show>
{/* ── Approved state: success message ───────────────────────── */}
<Show when={status() === 'APPROVED'}>
<div style={{ ...CARD, background: '#ECFDF5', border: '1px solid #6EE7B7', 'text-align': 'center', padding: '32px' }}>
<p style={{ margin: '0', 'font-size': '48px' }}>🎉</p>
<p style={{ margin: '12px 0 4px', 'font-size': '20px', 'font-weight': '800', color: '#065F46' }}>
You're Verified!
</p>
<p style={{ margin: '0', 'font-size': '14px', color: '#047857', 'line-height': '1.6' }}>
Your profile is approved. Start exploring opportunities on Nxtgauge.
</p>
</div>
</Show>
</Show>
</div>
);
}