All checks were successful
build-and-release / build (push) Successful in 1m37s
- VerificationStatusPage: showInlineEditors now hides for PENDING and UNDER_REVIEW (only shows when user action is needed: NOT_SUBMITTED, DOCUMENTS_REQUESTED, REVISION_REQUESTED, REJECTED). Previously the old ProfilePage form with validation errors appeared after submission. - VerificationStatusPage: wrap onVerificationStatusChange passed to ProfilePage so the local status signal also updates when the wizard calls onSubmitted. Previously the status card kept showing NOT_SUBMITTED until a page reload. - VerificationStatusPage progress tracker: inline signal reads directly in JSX instead of capturing them in local variables. In Solid.js, local const done = signal() inside a For callback is computed once and goes stale — the step circles stayed grey even after status changed to PENDING. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
649 lines
29 KiB
XML
649 lines
29 KiB
XML
/**
|
||
* 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<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 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<TabKey>('approval_status');
|
||
const [inlineSection, setInlineSection] = createSignal<'profile' | 'portfolio'>('profile');
|
||
const [status, setStatus] = createSignal('NOT_SUBMITTED');
|
||
const [referenceNumber, setReferenceNumber] = createSignal<string | null>(null);
|
||
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('');
|
||
const [activityLog, setActivityLog] = createSignal<Array<{ date: string; action: string; details: string }>>([]);
|
||
const [documents, setDocuments] = createSignal<VerificationDocument[]>([]);
|
||
|
||
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 (
|
||
<div style={{ 'max-width': '640px' }}>
|
||
<div
|
||
style={{
|
||
background: NAVY,
|
||
"border-radius": "12px",
|
||
padding: "16px 20px",
|
||
display: "flex",
|
||
"align-items": "center",
|
||
gap: "12px",
|
||
"margin-bottom": "14px",
|
||
}}
|
||
>
|
||
<span style={{ color: ORANGE }}>
|
||
<ShieldCheck size={24} />
|
||
</span>
|
||
<div>
|
||
<p style={{ margin: "0", "font-size": "18px", "font-weight": "800", color: "#fff" }}>
|
||
Verification Portal
|
||
</p>
|
||
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "rgba(255,255,255,0.65)" }}>
|
||
Track verification progress, documents, and updates.
|
||
</p>
|
||
<Show when={referenceNumber()}>
|
||
<p style={{ margin: "6px 0 0", "font-size": "13px", "font-family": "monospace", color: "rgba(255,255,255,0.85)" }}>
|
||
{referenceNumber()}
|
||
</p>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div style={{ display: 'flex', gap: '0', 'border-bottom': '1px solid #E5E7EB', 'margin-bottom': '14px' }}>
|
||
<For each={TABS}>
|
||
{(tab) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab(tab.key)}
|
||
style={{
|
||
padding: "10px 16px",
|
||
"border-bottom": activeTab() === tab.key ? '2px solid #FF5E13' : '2px solid transparent',
|
||
background: "transparent",
|
||
color: activeTab() === tab.key ? "#FF5E13" : "#6B7280",
|
||
"font-size": "14px",
|
||
"font-weight": activeTab() === tab.key ? "700" : "500",
|
||
cursor: "pointer",
|
||
display: "flex",
|
||
"align-items": "center",
|
||
gap: "6px",
|
||
}}
|
||
>
|
||
<tab.Icon size={16} />
|
||
{tab.label}
|
||
</button>
|
||
)}
|
||
</For>
|
||
</div>
|
||
|
||
<Show when={loading()}>
|
||
<div style={{ ...CARD, 'text-align': 'center', padding: '32px', color: '#9CA3AF' }}>
|
||
Loading verification status…
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={!loading()}>
|
||
{/* ── Approval Status Tab ─────────────────────────────────────── */}
|
||
<Show when={activeTab() === 'approval_status'}>
|
||
{/* 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={!['APPROVED', 'COMPLETED'].includes(status())}>
|
||
<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' }}>
|
||
<For each={FLOW_STEPS}>
|
||
{(step, idx) => (
|
||
<>
|
||
<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: currentStep() > 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}
|
||
</div>
|
||
<p style={{ margin: '4px 0 0', 'font-size': '10px', 'font-weight': '600', color: currentStep() > idx() || currentStep() === idx() + 1 ? '#374151' : '#9CA3AF', 'white-space': 'nowrap', 'text-align': 'center' }}>
|
||
{step.label}
|
||
</p>
|
||
</div>
|
||
<Show when={idx() < FLOW_STEPS.length - 1}>
|
||
<div style={{ flex: '1', height: '2px', background: currentStep() > idx() ? '#FF5E13' : '#E5E7EB', 'margin-bottom': '18px' }} />
|
||
</Show>
|
||
</>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
{/* Actions */}
|
||
<div style={{ display: 'flex', gap: '10px', 'flex-wrap': 'wrap' }}>
|
||
<Show when={status() === 'NOT_SUBMITTED'}>
|
||
<button type="button" onClick={() => setInlineSection('profile')} style={BTN_ORANGE}>
|
||
Fill My Profile
|
||
</button>
|
||
<button type="button" onClick={() => setInlineSection('portfolio')} style={BTN_GHOST}>
|
||
Fill My Portfolio
|
||
</button>
|
||
</Show>
|
||
<Show when={canResubmit()}>
|
||
<button type="button" onClick={() => setInlineSection('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>
|
||
|
||
<Show when={['APPROVED', 'COMPLETED'].includes(status())}>
|
||
<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 when={showInlineEditors()}>
|
||
<div style={{ ...CARD, 'margin-top': '16px', padding: '16px' }}>
|
||
<div style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', gap: '12px', 'flex-wrap': 'wrap', 'margin-bottom': '14px' }}>
|
||
<div>
|
||
<p style={{ margin: '0', 'font-size': '16px', 'font-weight': '800', color: '#111827' }}>
|
||
Complete Verification Details Here
|
||
</p>
|
||
<p style={{ margin: '6px 0 0', 'font-size': '13px', color: '#6B7280' }}>
|
||
Fill the required fields directly on this page, then submit for verification.
|
||
</p>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: '8px', 'flex-wrap': 'wrap' }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setInlineSection('profile')}
|
||
style={inlineSection() === 'profile' ? BTN_ORANGE : BTN_GHOST}
|
||
>
|
||
Profile Fields
|
||
</button>
|
||
<Show when={hasPortfolio()}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setInlineSection('portfolio')}
|
||
style={inlineSection() === 'portfolio' ? BTN_ORANGE : BTN_GHOST}
|
||
>
|
||
Portfolio Fields
|
||
</button>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
|
||
<Show when={inlineSection() === 'profile'}>
|
||
<ProfilePage
|
||
roleKey={props.roleKey}
|
||
runtimeFields={props.runtimeFields || []}
|
||
onVerificationStatusChange={(s) => {
|
||
setStatus(s);
|
||
props.onVerificationStatusChange?.(s);
|
||
}}
|
||
onNavigate={props.onNavigate}
|
||
/>
|
||
</Show>
|
||
|
||
<Show when={inlineSection() === 'portfolio' && hasPortfolio()}>
|
||
<PortfolioPage
|
||
roleKey={props.roleKey}
|
||
runtimeTabs={props.runtimeTabs || []}
|
||
runtimeFields={props.runtimeFields || []}
|
||
/>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
</Show>
|
||
|
||
{/* ── Documents Tab ───────────────────────────────────────────── */}
|
||
<Show when={activeTab() === 'documents'}>
|
||
<div style={CARD}>
|
||
<p style={{ margin: '0 0 14px', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>
|
||
Submitted Documents
|
||
</p>
|
||
<Show when={!docRequest() && !rejectionReason() && status() === 'NOT_SUBMITTED'}>
|
||
<p style={{ margin: '0', 'font-size': '13px', color: '#6B7280' }}>
|
||
No documents submitted yet. Complete your profile to submit documents for verification.
|
||
</p>
|
||
</Show>
|
||
<Show when={docRequest()}>
|
||
<div style={{ background: '#FFF7ED', border: '1px solid #FED7AA', 'border-radius': '10px', padding: '14px', 'margin-bottom': '12px' }}>
|
||
<p style={{ margin: '0 0 6px', 'font-size': '13px', 'font-weight': '700', color: '#C2410C' }}>
|
||
Document Request
|
||
</p>
|
||
<p style={{ margin: '0', 'font-size': '13px', color: '#374151' }}>{docRequest()}</p>
|
||
</div>
|
||
</Show>
|
||
<Show when={rejectionReason()}>
|
||
<div style={{ background: '#FEF2F2', border: '1px solid #FECACA', 'border-radius': '10px', padding: '14px', 'margin-bottom': '12px' }}>
|
||
<p style={{ margin: '0 0 6px', 'font-size': '13px', 'font-weight': '700', color: '#B91C1C' }}>
|
||
Rejection Reason
|
||
</p>
|
||
<p style={{ margin: '0', 'font-size': '13px', color: '#374151' }}>{rejectionReason()}</p>
|
||
</div>
|
||
</Show>
|
||
<Show when={status() !== 'NOT_SUBMITTED'}>
|
||
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}>
|
||
<For each={documents().length > 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 (
|
||
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '10px', padding: '12px', background: '#FCFCFD' }}>
|
||
<div style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', gap: '10px', 'flex-wrap': 'wrap' }}>
|
||
<div>
|
||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '600', color: '#111827' }}>{fieldLabel()}</p>
|
||
<Show
|
||
when={url()}
|
||
fallback={
|
||
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>No file uploaded</p>
|
||
}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={async () => {
|
||
const signed = await presignDocumentUrl(url()!);
|
||
window.open(signed, '_blank', 'noopener,noreferrer');
|
||
}}
|
||
style={{
|
||
margin: '4px 0 0', 'font-size': '12px', color: '#FF5E13',
|
||
'text-decoration': 'underline', display: 'inline-block',
|
||
background: 'none', border: 'none', padding: '0', cursor: 'pointer',
|
||
}}
|
||
>
|
||
View document
|
||
</button>
|
||
</Show>
|
||
</div>
|
||
<p style={{
|
||
margin: '0',
|
||
'font-size': '12px',
|
||
'font-weight': '600',
|
||
color: doc.status === 'APPROVED' ? '#059669' : doc.status === 'REJECTED' ? '#DC2626' : '#6B7280',
|
||
}}>
|
||
{doc.status === 'APPROVED' ? '✓ Verified' : doc.status === 'REJECTED' ? '✕ Rejected' : doc.status === 'SUBMITTED' ? '◌ Submitted' : '◌ Pending review'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}}
|
||
</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
|
||
{/* ── Activity Tab ────────────────────────────────────────────── */}
|
||
<Show when={activeTab() === 'activity'}>
|
||
<div style={CARD}>
|
||
<p style={{ margin: '0 0 14px', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>
|
||
Verification Activity
|
||
</p>
|
||
<Show when={activityLog().length === 0}>
|
||
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '10px' }}>
|
||
<Show when={status() === 'NOT_SUBMITTED'}>
|
||
<div style={{ display: 'flex', gap: '12px', 'align-items': 'flex-start' }}>
|
||
<div style={{ width: '8px', height: '8px', 'border-radius': '50%', background: '#E5E7EB', 'margin-top': '6px', 'flex-shrink': '0' }} />
|
||
<div>
|
||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '600', color: '#374151' }}>Profile created</p>
|
||
<p style={{ margin: '2px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>{updatedAt() ? new Date(updatedAt()!).toLocaleString('en-IN') : 'Just now'}</p>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
<Show when={!['NOT_SUBMITTED'].includes(status())}>
|
||
<div style={{ display: 'flex', gap: '12px', 'align-items': 'flex-start' }}>
|
||
<div style={{ width: '8px', height: '8px', 'border-radius': '50%', background: '#FDE68A', 'margin-top': '6px', 'flex-shrink': '0' }} />
|
||
<div>
|
||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '600', color: '#374151' }}>Profile submitted for verification</p>
|
||
<p style={{ margin: '2px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>{updatedAt() ? new Date(updatedAt()!).toLocaleString('en-IN') : 'Recently'}</p>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
<Show when={['APPROVED', 'COMPLETED'].includes(status())}>
|
||
<div style={{ display: 'flex', gap: '12px', 'align-items': 'flex-start' }}>
|
||
<div style={{ width: '8px', height: '8px', 'border-radius': '50%', background: '#10B981', 'margin-top': '6px', 'flex-shrink': '0' }} />
|
||
<div>
|
||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '600', color: '#374151' }}>Profile approved</p>
|
||
<p style={{ margin: '2px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>{updatedAt() ? new Date(updatedAt()!).toLocaleString('en-IN') : 'Recently'}</p>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
<Show when={activityLog().length > 0}>
|
||
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '12px' }}>
|
||
<For each={activityLog()}>
|
||
{(item) => (
|
||
<div style={{ display: 'flex', gap: '12px', 'align-items': 'flex-start' }}>
|
||
<div style={{ width: '8px', height: '8px', 'border-radius': '50%', background: ORANGE, 'margin-top': '6px', 'flex-shrink': '0' }} />
|
||
<div>
|
||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '600', color: '#374151' }}>{item.action}</p>
|
||
<p style={{ margin: '2px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>{item.date}</p>
|
||
<Show when={item.details}>
|
||
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>{item.details}</p>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
</Show>
|
||
</div>
|
||
);
|
||
}
|