feat(admin): wire verification actions, remove onboarding management, fix kb.tsx

- verification/[id].tsx: approve, reject, request-documents, request-revision
  all wired to real API endpoints with loading states and feedback banners
- Delete onboarding-management/ (flow moved to dashboard My Profile/Portfolio)
- kb.tsx: remove stray closing brace (TS1128 syntax error)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-04-06 17:20:41 +02:00
parent 005572cdd2
commit 0c757cf5bd
5 changed files with 162 additions and 44 deletions

View file

@ -497,7 +497,6 @@ export default function KnowledgeBasePage() {
</div>
);
}
}
async function loadCategories(): Promise<{id: string; name: string; slug: string}[]> {
try {

View file

@ -1,5 +0,0 @@
import { Navigate } from '@solidjs/router';
export default function OnboardingManagementDetailAliasPage() {
return <Navigate href="/admin" />;
}

View file

@ -1,5 +0,0 @@
import { Navigate } from '@solidjs/router';
export default function OnboardingManagementAliasPage() {
return <Navigate href="/admin" />;
}

View file

@ -1,5 +0,0 @@
import { Navigate } from '@solidjs/router';
export default function OnboardingManagementCreateAliasPage() {
return <Navigate href="/admin" />;
}

View file

@ -1,5 +1,14 @@
import { A, useSearchParams, useParams } from '@solidjs/router';
import { For, Show, createMemo, createSignal } from 'solid-js';
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
const API = '/api/gateway';
async function adminFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
});
}
type Status = 'UNDER_REVIEW' | 'DOCUMENTS_REQUESTED' | 'REVISION_REQUESTED' | 'APPROVED' | 'REJECTED';
@ -35,6 +44,22 @@ export default function VerificationReviewDetailPage() {
const [status, setStatus] = createSignal<Status>('UNDER_REVIEW');
const [tab, setTab] = createSignal<'overview' | 'submitted' | 'documents' | 'missing' | 'requested' | 'activity'>('submitted');
// ── Real data loaded from backend ─────────────────────────────────────────
const [verif, setVerif] = createSignal<any>(null);
const [actionLoading, setActionLoading] = createSignal(false);
const [actionMsg, setActionMsg] = createSignal('');
const [rejectReason, setRejectReason] = createSignal('');
const [showRejectInput, setShowRejectInput] = createSignal(false);
onMount(async () => {
const res = await adminFetch(`/api/admin/verifications/${params.id}`);
if (res.ok) {
const d = await res.json();
setVerif(d);
if (d.status) setStatus(d.status as Status);
}
});
const [docs, setDocs] = createSignal<DocRequestRow[]>([
{ key: 'identity', title: 'Identity Proof', hint: 'Passport, DL or National ID', enabled: true, reason: 'Expired', note: 'Please upload a current valid ID with clear expiry date.' },
{ key: 'address', title: 'Address Proof', hint: 'Utility bill or bank statement (last 3 months)', enabled: false, reason: 'Not Readable', note: '' },
@ -103,30 +128,112 @@ export default function VerificationReviewDetailPage() {
setSearchParams(next);
};
const sendDocumentRequest = () => {
setStatus('DOCUMENTS_REQUESTED');
setTab('requested');
clearActionMode();
setActivityNotes((prev) => [...prev, 'Document request sent to applicant.']);
const sendDocumentRequest = async () => {
const selected = docs().filter((d) => d.enabled);
if (selected.length === 0) { setActionMsg('Select at least one document to request.'); return; }
const message = selected
.map((d) => `${d.title}${d.reason}${d.note ? ': ' + d.note : ''}`)
.join(' | ');
setActionLoading(true);
setActionMsg('');
try {
const res = await adminFetch(`/api/admin/verifications/${params.id}/request-documents`, {
method: 'POST',
body: JSON.stringify({ message }),
});
if (res.ok) {
setStatus('DOCUMENTS_REQUESTED');
setTab('requested');
clearActionMode();
setActivityNotes((prev) => [...prev, `Document request sent: ${message}`]);
setActionMsg('Document request sent to applicant.');
} else {
const d = await res.json().catch(() => ({}));
setActionMsg(d.error ?? 'Failed to send document request.');
}
} catch {
setActionMsg('Network error. Please try again.');
} finally {
setActionLoading(false);
}
};
const sendRevisionRequest = () => {
setStatus('REVISION_REQUESTED');
setTab('requested');
clearActionMode();
setActivityNotes((prev) => [...prev, 'Profile revision request sent to applicant.']);
const sendRevisionRequest = async () => {
const message = revisionRows()
.map((r) => `${r.field} (${r.reason}): ${r.instruction}`)
.join(' | ');
setActionLoading(true);
setActionMsg('');
try {
const res = await adminFetch(`/api/admin/verifications/${params.id}/request-documents`, {
method: 'POST',
body: JSON.stringify({ message: `Revision required — ${message}` }),
});
if (res.ok) {
setStatus('REVISION_REQUESTED');
setTab('requested');
clearActionMode();
setActivityNotes((prev) => [...prev, 'Profile revision request sent to applicant.']);
setActionMsg('Revision request sent to applicant.');
} else {
const d = await res.json().catch(() => ({}));
setActionMsg(d.error ?? 'Failed to send revision request.');
}
} catch {
setActionMsg('Network error. Please try again.');
} finally {
setActionLoading(false);
}
};
const approveSubmission = () => {
setStatus('APPROVED');
clearActionMode();
setActivityNotes((prev) => [...prev, 'Verification completed and moved to Approval Management.']);
const approveSubmission = async () => {
setActionLoading(true);
setActionMsg('');
try {
const res = await adminFetch(`/api/admin/verifications/${params.id}/approve`, {
method: 'POST',
body: JSON.stringify({ notes: reviewerNote() || undefined }),
});
if (res.ok) {
setStatus('APPROVED');
clearActionMode();
setActivityNotes((prev) => [...prev, 'Verification approved. Role activated.']);
setActionMsg('Approved. User role has been activated.');
} else {
const d = await res.json().catch(() => ({}));
setActionMsg(d.error ?? 'Approval failed. Please try again.');
}
} catch {
setActionMsg('Network error. Please try again.');
} finally {
setActionLoading(false);
}
};
const rejectSubmission = () => {
setStatus('REJECTED');
clearActionMode();
setActivityNotes((prev) => [...prev, 'Submission rejected by reviewer.']);
const rejectSubmission = async () => {
if (!rejectReason().trim()) { setShowRejectInput(true); return; }
setActionLoading(true);
setActionMsg('');
try {
const res = await adminFetch(`/api/admin/verifications/${params.id}/reject`, {
method: 'POST',
body: JSON.stringify({ reason: rejectReason(), notes: rejectReason() }),
});
if (res.ok) {
setStatus('REJECTED');
setShowRejectInput(false);
clearActionMode();
setActivityNotes((prev) => [...prev, `Submission rejected: ${rejectReason()}`]);
setActionMsg('Submission rejected. User has been notified.');
} else {
const d = await res.json().catch(() => ({}));
setActionMsg(d.error ?? 'Rejection failed. Please try again.');
}
} catch {
setActionMsg('Network error. Please try again.');
} finally {
setActionLoading(false);
}
};
const addRevisionField = () => {
@ -153,18 +260,45 @@ export default function VerificationReviewDetailPage() {
<span style={`display:inline-flex;align-items:center;height:32px;padding:0 12px;border-radius:999px;border:1px solid ${tone().border};background:${tone().bg};color:${tone().text};font-size:12px;font-weight:700`}>
{tone().label}
</span>
<button type="button" onClick={openRequestDocuments} style="height:36px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#374151">Request Documents</button>
<button type="button" onClick={openRequestChanges} style="height:36px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#374151">Request Changes</button>
<button type="button" onClick={rejectSubmission} style="height:36px;border-radius:10px;border:1px solid #FECACA;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#B91C1C">Reject</button>
<button type="button" onClick={approveSubmission} style="height:36px;border-radius:10px;border:none;background:#0D0D2A;padding:0 14px;font-size:13px;font-weight:700;color:white">Approve Submission</button>
<button type="button" onClick={openRequestDocuments} disabled={actionLoading()} style="height:36px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#374151">Request Documents</button>
<button type="button" onClick={openRequestChanges} disabled={actionLoading()} style="height:36px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#374151">Request Changes</button>
<button type="button" onClick={rejectSubmission} disabled={actionLoading()} style="height:36px;border-radius:10px;border:1px solid #FECACA;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#B91C1C">Reject</button>
<button type="button" onClick={approveSubmission} disabled={actionLoading()} style={`height:36px;border-radius:10px;border:none;background:#0D0D2A;padding:0 14px;font-size:13px;font-weight:700;color:white;opacity:${actionLoading() ? '0.6' : '1'}`}>{actionLoading() ? 'Processing…' : 'Approve Submission'}</button>
</div>
</div>
{/* ── Reject reason input ── */}
<Show when={showRejectInput()}>
<div style="margin-top:12px;background:#FEF2F2;border:1px solid #FECACA;border-radius:12px;padding:14px;display:flex;flex-direction:column;gap:10px">
<p style="margin:0;font-size:14px;font-weight:700;color:#B91C1C">Enter Rejection Reason</p>
<textarea
rows={3}
placeholder="Explain why this submission is being rejected…"
value={rejectReason()}
onInput={(e) => setRejectReason(e.currentTarget.value)}
style="border:1px solid #FECACA;border-radius:8px;padding:8px 10px;resize:vertical;font-size:13px;color:#374151;background:white"
/>
<div style="display:flex;gap:8px">
<button type="button" onClick={rejectSubmission} disabled={actionLoading() || !rejectReason().trim()} style={`height:36px;border-radius:10px;border:none;background:#B91C1C;padding:0 14px;font-size:13px;font-weight:700;color:white;opacity:${!rejectReason().trim() ? '0.5' : '1'}`}>
{actionLoading() ? 'Rejecting…' : 'Confirm Reject'}
</button>
<button type="button" onClick={() => setShowRejectInput(false)} style="height:36px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#374151">Cancel</button>
</div>
</div>
</Show>
{/* ── Action feedback ── */}
<Show when={actionMsg()}>
<div style={`margin-top:10px;padding:10px 14px;border-radius:10px;font-size:13px;font-weight:600;${actionMsg().includes('fail') || actionMsg().includes('error') || actionMsg().includes('Failed') || actionMsg().includes('Error') ? 'background:#FEF2F2;color:#B91C1C;border:1px solid #FECACA' : 'background:#ECFDF5;color:#065F46;border:1px solid #6EE7B7'}`}>
{actionMsg()}
</div>
</Show>
<div style="margin-top:14px;display:grid;grid-template-columns:2fr 1fr;gap:12px">
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px">
<div><p style="margin:0;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.04em">Applicant</p><p style="margin:6px 0 0;font-size:30px;font-weight:800;color:#111827;line-height:1.1">Sarah Jenkins</p></div>
<div><p style="margin:0;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.04em">Type</p><p style="margin:6px 0 0;font-size:18px;font-weight:700;color:#111827">{roleLabel()}</p></div>
<div><p style="margin:0;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.04em">Location</p><p style="margin:6px 0 0;font-size:18px;font-weight:700;color:#111827">Chennai, India</p></div>
<div><p style="margin:0;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.04em">Applicant</p><p style="margin:6px 0 0;font-size:30px;font-weight:800;color:#111827;line-height:1.1">{verif()?.payload?.first_name ?? verif()?.payload?.company_name ?? '—'} {verif()?.payload?.last_name ?? ''}</p></div>
<div><p style="margin:0;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.04em">Role</p><p style="margin:6px 0 0;font-size:18px;font-weight:700;color:#111827">{verif()?.role_key ?? roleLabel()}</p></div>
<div><p style="margin:0;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.04em">Submitted</p><p style="margin:6px 0 0;font-size:18px;font-weight:700;color:#111827">{verif()?.created_at ? new Date(verif().created_at).toLocaleDateString('en-IN') : '—'}</p></div>
</div>
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px">
@ -248,7 +382,7 @@ export default function VerificationReviewDetailPage() {
<div style="margin-top:12px;display:flex;justify-content:flex-end;gap:8px">
<button type="button" onClick={clearActionMode} style="height:36px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#475569">Cancel</button>
<button type="button" onClick={sendDocumentRequest} style="height:36px;border-radius:10px;border:none;background:#0D0D2A;padding:0 12px;font-size:13px;font-weight:700;color:white">Send Request</button>
<button type="button" onClick={sendDocumentRequest} disabled={actionLoading()} style={`height:36px;border-radius:10px;border:none;background:#0D0D2A;padding:0 12px;font-size:13px;font-weight:700;color:white;opacity:${actionLoading() ? '0.6' : '1'}`}>{actionLoading() ? 'Sending…' : 'Send Request'}</button>
</div>
</div>
</Show>
@ -293,7 +427,7 @@ export default function VerificationReviewDetailPage() {
<div style="margin-top:12px;display:flex;justify-content:flex-end;gap:8px">
<button type="button" onClick={clearActionMode} style="height:36px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;font-weight:700;color:#475569">Cancel</button>
<button type="button" onClick={sendRevisionRequest} style="height:36px;border-radius:10px;border:none;background:#0D0D2A;padding:0 12px;font-size:13px;font-weight:700;color:white">Submit Revision Request</button>
<button type="button" onClick={sendRevisionRequest} disabled={actionLoading()} style={`height:36px;border-radius:10px;border:none;background:#0D0D2A;padding:0 12px;font-size:13px;font-weight:700;color:white;opacity:${actionLoading() ? '0.6' : '1'}`}>{actionLoading() ? 'Sending…' : 'Submit Revision Request'}</button>
</div>
</div>
</Show>