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:
parent
005572cdd2
commit
0c757cf5bd
5 changed files with 162 additions and 44 deletions
|
|
@ -497,7 +497,6 @@ export default function KnowledgeBasePage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCategories(): Promise<{id: string; name: string; slug: string}[]> {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function OnboardingManagementDetailAliasPage() {
|
||||
return <Navigate href="/admin" />;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function OnboardingManagementAliasPage() {
|
||||
return <Navigate href="/admin" />;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function OnboardingManagementCreateAliasPage() {
|
||||
return <Navigate href="/admin" />;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue