feat: render human-readable reference numbers and show submitted documents
All checks were successful
build-and-release / build (push) Successful in 1m41s
All checks were successful
build-and-release / build (push) Successful in 1m41s
Replaces raw/truncated UUID labels with the backend's new reference_number field across verification, application, and lead request views. Verification status page now renders actual uploaded documents (previously invisible after submission since the API never returned them). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
49b979eb16
commit
142e185fc6
5 changed files with 86 additions and 13 deletions
|
|
@ -25,6 +25,7 @@ interface ApplicationItem {
|
||||||
cover_note?: string | null;
|
cover_note?: string | null;
|
||||||
resume_url?: string | null;
|
resume_url?: string | null;
|
||||||
applied_at?: string;
|
applied_at?: string;
|
||||||
|
reference_number: string;
|
||||||
// Profile snapshot — contact fields excluded by backend
|
// Profile snapshot — contact fields excluded by backend
|
||||||
applicant_name?: string | null;
|
applicant_name?: string | null;
|
||||||
avatar_url?: string | null;
|
avatar_url?: string | null;
|
||||||
|
|
@ -304,7 +305,7 @@ export default function CompanyApplicationsPage() {
|
||||||
</Show>
|
</Show>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ margin: "0", "font-size": "14px", "font-weight": "800", color: "#111827" }}>
|
<p style={{ margin: "0", "font-size": "14px", "font-weight": "800", color: "#111827" }}>
|
||||||
{app.applicant_name || `Applicant #${app.id.slice(0, 8)}`}
|
{app.applicant_name || app.reference_number}
|
||||||
</p>
|
</p>
|
||||||
<Show when={app.headline}>
|
<Show when={app.headline}>
|
||||||
<p style={{ margin: "2px 0 0", "font-size": "12px", color: "#6B7280" }}>{app.headline}</p>
|
<p style={{ margin: "2px 0 0", "font-size": "12px", color: "#6B7280" }}>{app.headline}</p>
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,10 @@ export default function CompanyShortlistedCandidatesPage() {
|
||||||
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '12px', padding: '12px', background: '#FCFCFD' }}>
|
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '12px', padding: '12px', background: '#FCFCFD' }}>
|
||||||
<div style={{ display: 'flex', 'justify-content': 'space-between', gap: '10px', 'flex-wrap': 'wrap' }}>
|
<div style={{ display: 'flex', 'justify-content': 'space-between', gap: '10px', 'flex-wrap': 'wrap' }}>
|
||||||
<div>
|
<div>
|
||||||
|
{/* This label identifies the candidate (job_seeker_id), not the application
|
||||||
|
record, so it is not a candidate for the new application reference_number
|
||||||
|
field. Leaving the truncated id as-is pending backend confirmation of
|
||||||
|
whether a human-readable candidate identifier should be shown instead. */}
|
||||||
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '800', color: '#111827' }}>
|
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '800', color: '#111827' }}>
|
||||||
Candidate #{row.job_seeker_id?.slice(0, 8) || row.id.slice(0, 8)}
|
Candidate #{row.job_seeker_id?.slice(0, 8) || row.id.slice(0, 8)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ type ApplicationItem = {
|
||||||
applied_at?: string;
|
applied_at?: string;
|
||||||
resume_url?: string | null;
|
resume_url?: string | null;
|
||||||
cover_letter?: string | null;
|
cover_letter?: string | null;
|
||||||
|
reference_number: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function apiFetch(path: string, opts?: RequestInit) {
|
async function apiFetch(path: string, opts?: RequestInit) {
|
||||||
|
|
@ -138,7 +139,7 @@ export default function JobSeekerApplicationsPage() {
|
||||||
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '12px', padding: '12px', background: '#FCFCFD' }}>
|
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '12px', padding: '12px', background: '#FCFCFD' }}>
|
||||||
<div style={{ display: 'flex', 'justify-content': 'space-between', gap: '10px', 'flex-wrap': 'wrap' }}>
|
<div style={{ display: 'flex', 'justify-content': 'space-between', gap: '10px', 'flex-wrap': 'wrap' }}>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '800', color: '#111827' }}>Application #{row.id.slice(0, 8)}</p>
|
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '800', color: '#111827' }}>{row.reference_number}</p>
|
||||||
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>
|
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>
|
||||||
Job: {row.job_id?.slice(0, 8) || '—'} {row.applied_at ? `• ${new Date(row.applied_at).toLocaleString('en-IN')}` : ''}
|
Job: {row.job_id?.slice(0, 8) || '—'} {row.applied_at ? `• ${new Date(row.applied_at).toLocaleString('en-IN')}` : ''}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type LeadRequestItem = {
|
||||||
requested_at?: string;
|
requested_at?: string;
|
||||||
expires_at?: string;
|
expires_at?: string;
|
||||||
decision_at?: string;
|
decision_at?: string;
|
||||||
|
reference_number: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function apiFetch(path: string, opts?: RequestInit) {
|
async function apiFetch(path: string, opts?: RequestInit) {
|
||||||
|
|
@ -91,7 +92,7 @@ export default function ProfessionalResponsesPage(props: Props) {
|
||||||
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '12px', padding: '12px', background: '#FCFCFD' }}>
|
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '12px', padding: '12px', background: '#FCFCFD' }}>
|
||||||
<div style={{ display: 'flex', 'justify-content': 'space-between', gap: '10px', 'flex-wrap': 'wrap' }}>
|
<div style={{ display: 'flex', 'justify-content': 'space-between', gap: '10px', 'flex-wrap': 'wrap' }}>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '800', color: '#111827' }}>Lead Request #{row.id.slice(0, 8)}</p>
|
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '800', color: '#111827' }}>{row.reference_number}</p>
|
||||||
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>
|
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>
|
||||||
Requirement: {row.requirement_id?.slice(0, 8) || '—'} {row.requested_at ? `• ${new Date(row.requested_at).toLocaleString('en-IN')}` : ''}
|
Requirement: {row.requirement_id?.slice(0, 8) || '—'} {row.requested_at ? `• ${new Date(row.requested_at).toLocaleString('en-IN')}` : ''}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -128,9 +128,37 @@ interface Props {
|
||||||
onVerificationStatusChange?: (status: string) => void;
|
onVerificationStatusChange?: (status: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
export default function VerificationStatusPage(props: Props) {
|
||||||
const [activeTab, setActiveTab] = createSignal<TabKey>('approval_status');
|
const [activeTab, setActiveTab] = createSignal<TabKey>('approval_status');
|
||||||
const [status, setStatus] = createSignal('NOT_SUBMITTED');
|
const [status, setStatus] = createSignal('NOT_SUBMITTED');
|
||||||
|
const [referenceNumber, setReferenceNumber] = createSignal<string | null>(null);
|
||||||
const [docRequest, setDocRequest] = createSignal<string | null>(null);
|
const [docRequest, setDocRequest] = createSignal<string | null>(null);
|
||||||
const [rejectionReason, setRejectionReason] = createSignal<string | null>(null);
|
const [rejectionReason, setRejectionReason] = createSignal<string | null>(null);
|
||||||
const [updatedAt, setUpdatedAt] = createSignal<string | null>(null);
|
const [updatedAt, setUpdatedAt] = createSignal<string | null>(null);
|
||||||
|
|
@ -138,21 +166,26 @@ export default function VerificationStatusPage(props: Props) {
|
||||||
const [resubmitting, setResubmitting] = createSignal(false);
|
const [resubmitting, setResubmitting] = createSignal(false);
|
||||||
const [resubmitMsg, setResubmitMsg] = createSignal('');
|
const [resubmitMsg, setResubmitMsg] = createSignal('');
|
||||||
const [activityLog, setActivityLog] = createSignal<Array<{ date: string; action: string; details: string }>>([]);
|
const [activityLog, setActivityLog] = createSignal<Array<{ date: string; action: string; details: string }>>([]);
|
||||||
|
const [documents, setDocuments] = createSignal<VerificationDocument[]>([]);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch(`/api/me/verification-status?roleKey=${props.roleKey}`);
|
const res = await apiFetch(`/api/me/verification-status?roleKey=${props.roleKey}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const d = await res.json();
|
const d: VerificationStatusResponse = await res.json();
|
||||||
const nextStatus = String(d.status ?? 'NOT_SUBMITTED');
|
const nextStatus = String(d.status ?? 'NOT_SUBMITTED');
|
||||||
setStatus(nextStatus);
|
setStatus(nextStatus);
|
||||||
props.onVerificationStatusChange?.(nextStatus);
|
props.onVerificationStatusChange?.(nextStatus);
|
||||||
|
setReferenceNumber(d.reference_number ?? null);
|
||||||
setDocRequest(d.document_request ?? null);
|
setDocRequest(d.document_request ?? null);
|
||||||
setRejectionReason(d.rejection_reason ?? null);
|
setRejectionReason(d.rejection_reason ?? null);
|
||||||
setUpdatedAt(d.updated_at ?? null);
|
setUpdatedAt(d.updated_at ?? null);
|
||||||
if (d.activity_log) {
|
if (d.activity_log) {
|
||||||
setActivityLog(d.activity_log);
|
setActivityLog(d.activity_log);
|
||||||
}
|
}
|
||||||
|
if (d.documents) {
|
||||||
|
setDocuments(d.documents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -220,6 +253,11 @@ export default function VerificationStatusPage(props: Props) {
|
||||||
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "rgba(255,255,255,0.65)" }}>
|
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "rgba(255,255,255,0.65)" }}>
|
||||||
Track verification progress, documents, and updates.
|
Track verification progress, documents, and updates.
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -425,15 +463,43 @@ export default function VerificationStatusPage(props: Props) {
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={status() !== 'NOT_SUBMITTED'}>
|
<Show when={status() !== 'NOT_SUBMITTED'}>
|
||||||
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}>
|
||||||
<For each={docFields()}>
|
<For each={documents().length > 0 ? documents() : docFields().map((f) => ({ type: f.key, value: null, status: status() }))}>
|
||||||
{(doc) => (
|
{(doc) => {
|
||||||
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '10px', padding: '12px', background: '#FCFCFD' }}>
|
const fieldLabel = () => docFields().find((f) => f.key === doc.type)?.label ?? doc.type;
|
||||||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '600', color: '#111827' }}>{doc.label}</p>
|
const url = () => docUrl(doc.value);
|
||||||
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>
|
return (
|
||||||
{status() === 'APPROVED' ? '✓ Verified' : status() === 'REJECTED' ? '✕ Rejected' : '◌ Pending review'}
|
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '10px', padding: '12px', background: '#FCFCFD' }}>
|
||||||
</p>
|
<div style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', gap: '10px', 'flex-wrap': 'wrap' }}>
|
||||||
</div>
|
<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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={url()!}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ margin: '4px 0 0', 'font-size': '12px', color: '#FF5E13', 'text-decoration': 'underline', display: 'inline-block' }}
|
||||||
|
>
|
||||||
|
View document
|
||||||
|
</a>
|
||||||
|
</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>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue