feat(admin): add jobs and verification parity routes

This commit is contained in:
Ashwin Kumar 2026-03-19 14:21:49 +01:00
parent 7e65337c60
commit 6c6667b8be
5 changed files with 407 additions and 0 deletions

View file

@ -0,0 +1,118 @@
import { A, useParams } from '@solidjs/router';
import { createMemo, createResource, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type Job = {
id: string;
title?: string;
description?: string;
requiredSkills?: string[];
required_skills?: string[];
experienceLevel?: string;
experience_level?: string;
status?: string;
clientName?: string;
client_name?: string;
companyName?: string;
company_name?: string;
location?: string;
hourlyRateMin?: number;
hourlyRateMax?: number;
hourly_rate_min?: number;
hourly_rate_max?: number;
availability?: string;
durationDays?: number;
duration_days?: number;
};
async function fetchJob(id: string): Promise<Job | null> {
try {
const res = await fetch(`${API}/api/jobs/${id}`);
if (!res.ok) return null;
const data = await res.json();
return data.job || data;
} catch {
return null;
}
}
export default function JobDetailPage() {
const params = useParams();
const [job] = createResource(() => params.id, fetchJob);
const skills = createMemo(() => job()?.requiredSkills || job()?.required_skills || []);
const client = createMemo(() => job()?.clientName || job()?.client_name || job()?.companyName || job()?.company_name || '—');
const exp = createMemo(() => job()?.experienceLevel || job()?.experience_level || '—');
const rateMin = createMemo(() => job()?.hourlyRateMin ?? job()?.hourly_rate_min);
const rateMax = createMemo(() => job()?.hourlyRateMax ?? job()?.hourly_rate_max);
const duration = createMemo(() => job()?.durationDays ?? job()?.duration_days);
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div>
<h1 class="page-title">Job Management</h1>
<p class="page-subtitle">Review one live backend job in the same detail-first style as other admin modules.</p>
</div>
<A class="btn" href="/admin/jobs">Back to Jobs</A>
</div>
<Show when={job.loading}>
<div class="card"><p class="notice">Loading job...</p></div>
</Show>
<Show when={!job.loading && !job()}>
<div class="card"><p class="notice">Job not found.</p></div>
</Show>
<Show when={job()}>
<section class="card">
<div class="field-grid-2">
<div>
<p class="hint">Title</p>
<p style="margin:6px 0 0;font-weight:700;color:#0f172a">{job()!.title || '—'}</p>
</div>
<div>
<p class="hint">Status</p>
<p style="margin:6px 0 0;color:#334155">{job()!.status || '—'}</p>
</div>
<div>
<p class="hint">Client</p>
<p style="margin:6px 0 0;color:#334155">{client()}</p>
</div>
<div>
<p class="hint">Experience</p>
<p style="margin:6px 0 0;color:#334155">{exp()}</p>
</div>
<div>
<p class="hint">Rate</p>
<p style="margin:6px 0 0;color:#334155">{rateMin() != null ? `${rateMin()} - ₹${rateMax() ?? rateMin()}` : '—'}</p>
</div>
<div>
<p class="hint">Location</p>
<p style="margin:6px 0 0;color:#334155">{job()!.location || '—'}</p>
</div>
<div>
<p class="hint">Availability</p>
<p style="margin:6px 0 0;color:#334155">{job()!.availability || '—'}</p>
</div>
<div>
<p class="hint">Duration</p>
<p style="margin:6px 0 0;color:#334155">{duration() != null ? `${duration()} days` : '—'}</p>
</div>
<div style="grid-column:1/-1">
<p class="hint">Required Skills</p>
<p style="margin:6px 0 0;color:#334155">{skills().length > 0 ? skills().join(', ') : '—'}</p>
</div>
</div>
<div style="margin-top:18px">
<p class="hint">Description</p>
<p style="margin:6px 0 0;color:#334155;white-space:pre-wrap">{job()!.description || '—'}</p>
</div>
</section>
</Show>
</AdminShell>
);
}

View file

@ -0,0 +1,101 @@
import { A } from '@solidjs/router';
import { createMemo, createResource, For, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type ApprovalRow = {
id: string;
requestType?: string;
type?: string;
requestStatus?: string;
status?: string;
requester?: { name?: string; email?: string };
requesterName?: string;
requesterEmail?: string;
createdAt?: string;
created_at?: string;
};
async function fetchApprovals(): Promise<ApprovalRow[]> {
try {
const res = await fetch(`${API}/api/admin/approvals`);
if (!res.ok) return [];
const data = await res.json();
return Array.isArray(data) ? data : (data.approvals || []);
} catch {
return [];
}
}
export default function VerificationStatusPage() {
const [rows] = createResource(fetchApprovals);
const normalized = createMemo(() => (rows() || []).map((r) => ({
...r,
status: (r.requestStatus || r.status || 'UNKNOWN').toUpperCase(),
type: (r.requestType || r.type || 'PROFILE').toUpperCase(),
requesterName: r.requester?.name || r.requesterName || 'Unknown',
requesterEmail: r.requester?.email || r.requesterEmail || '—',
createdAt: r.createdAt || r.created_at || '',
})));
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div>
<h1 class="page-title">Verification Status</h1>
<p class="page-subtitle">Track request status states and open a specific record for follow-up.</p>
</div>
<A class="btn" href="/admin/approval">Open Approval Center</A>
</div>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Requester</th>
<th>Status</th>
<th>Submitted</th>
<th class="align-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={rows.loading}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#64748b">Loading verification statuses...</td></tr>
</Show>
<Show when={!rows.loading && normalized().length === 0}>
<tr><td colspan="6" style="text-align:center;padding:32px;color:#94a3b8">No verification status records found.</td></tr>
</Show>
<Show when={!rows.loading && normalized().length > 0}>
<For each={normalized()}>
{(item) => (
<tr>
<td style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#64748b">{item.id.slice(0, 8)}...</td>
<td style="color:#475569">{item.type}</td>
<td>
<div style="font-weight:600;color:#0f172a">{item.requesterName}</div>
<div style="font-size:12px;color:#64748b">{item.requesterEmail}</div>
</td>
<td><span class="status-chip">{item.status}</span></td>
<td style="color:#475569">{item.createdAt ? new Date(item.createdAt).toLocaleString() : '—'}</td>
<td>
<div class="table-actions">
<A class="action-icon-btn" href={`/admin/verification-status/${item.id}`} title="Open Status Detail"></A>
<A class="action-icon-btn" href={`/admin/approval/${item.id}`} title="Open Approval Detail">👁</A>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</AdminShell>
);
}

View file

@ -0,0 +1,84 @@
import { A, useParams } from '@solidjs/router';
import { createMemo, createResource, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type ApprovalDetail = {
id: string;
requestType?: string;
type?: string;
requestStatus?: string;
status?: string;
requester?: { name?: string; email?: string };
requesterName?: string;
requesterEmail?: string;
createdAt?: string;
created_at?: string;
requestReason?: string;
};
async function fetchApproval(id: string): Promise<ApprovalDetail | null> {
try {
const res = await fetch(`${API}/api/admin/approvals/${id}`);
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
export default function VerificationStatusDetailPage() {
const params = useParams();
const [detail] = createResource(() => params.id, fetchApproval);
const status = createMemo(() => (detail()?.requestStatus || detail()?.status || 'UNKNOWN').toUpperCase());
const type = createMemo(() => (detail()?.requestType || detail()?.type || 'PROFILE').toUpperCase());
const requester = createMemo(() => detail()?.requester?.name || detail()?.requesterName || 'Unknown');
const email = createMemo(() => detail()?.requester?.email || detail()?.requesterEmail || '—');
const submitted = createMemo(() => detail()?.createdAt || detail()?.created_at || '');
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div>
<h1 class="page-title">Verification Status Detail</h1>
<p class="page-subtitle">Open one verification status request and jump into approval review when needed.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/verification-status">Back to Status List</A>
<A class="btn navy" href={`/admin/approval/${params.id}`}>Open Approval Detail</A>
</div>
</div>
<Show when={detail.loading}>
<div class="card"><p class="notice">Loading verification status...</p></div>
</Show>
<Show when={!detail.loading && !detail()}>
<div class="card"><p class="notice">Verification status record not found.</p></div>
</Show>
<Show when={detail()}>
<div class="grid" style="margin-top:0">
<section class="card">
<h2 style="margin-bottom:8px">Request</h2>
<p class="notice" style="margin:0"><strong>ID:</strong> {detail()!.id}</p>
<p class="notice" style="margin:8px 0 0"><strong>Type:</strong> {type()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Status:</strong> {status()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Submitted:</strong> {submitted() ? new Date(submitted()).toLocaleString() : '—'}</p>
</section>
<section class="card">
<h2 style="margin-bottom:8px">Requester</h2>
<p class="notice" style="margin:0"><strong>Name:</strong> {requester()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Email:</strong> {email()}</p>
</section>
</div>
<section class="card" style="margin-top:16px">
<h2 style="margin-bottom:10px">Request Reason Payload</h2>
<pre class="json">{detail()!.requestReason || 'No requestReason payload found.'}</pre>
</section>
</Show>
</AdminShell>
);
}

View file

@ -0,0 +1,24 @@
import { A } from '@solidjs/router';
import AdminShell from '~/components/AdminShell';
export default function VerificationManagementPage() {
return (
<AdminShell>
<div class="page-hero-card">
<h1 class="page-title">Approval Management</h1>
<p class="page-subtitle" style="margin-top:8px">
Admin review now lives under Approval Management. Verification remains a user-facing status concept.
</p>
</div>
<section class="card">
<p class="notice" style="margin:0">
Use Approval Management to review companies, customers, candidates, and professional submissions.
</p>
<div class="actions">
<A class="btn navy" href="/admin/approval">Open Approval Management</A>
</div>
</section>
</AdminShell>
);
}

View file

@ -0,0 +1,80 @@
import { A, useParams } from '@solidjs/router';
import { createMemo, createResource, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type Approval = {
id: string;
requestStatus?: string;
status?: string;
requestType?: string;
type?: string;
requester?: { name?: string; email?: string };
requesterName?: string;
requesterEmail?: string;
requestReason?: string;
};
async function fetchApproval(id: string): Promise<Approval | null> {
try {
const res = await fetch(`${API}/api/admin/approvals/${id}`);
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
export default function VerificationDetailPage() {
const params = useParams();
const [approval] = createResource(() => params.id, fetchApproval);
const status = createMemo(() => (approval()?.requestStatus || approval()?.status || 'PENDING').toUpperCase());
const type = createMemo(() => (approval()?.requestType || approval()?.type || 'PROFILE').toUpperCase());
const requester = createMemo(() => approval()?.requester?.name || approval()?.requesterName || 'Unknown');
const email = createMemo(() => approval()?.requester?.email || approval()?.requesterEmail || '—');
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div>
<h1 class="page-title">Verification Review</h1>
<p class="page-subtitle">Review submission context, documents, and verification decision state.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/verification">Back to Verification</A>
<A class="btn navy" href={`/admin/approval/${params.id}`}>Open Approval Detail</A>
</div>
</div>
<Show when={approval.loading}>
<div class="card"><p class="notice">Loading verification detail...</p></div>
</Show>
<Show when={!approval.loading && !approval()}>
<div class="card"><p class="notice">Verification request not found.</p></div>
</Show>
<Show when={approval()}>
<div class="grid" style="margin-top:0">
<section class="card">
<h2 style="margin-bottom:8px">Summary</h2>
<p class="notice" style="margin:0"><strong>Approval ID:</strong> {approval()!.id}</p>
<p class="notice" style="margin:8px 0 0"><strong>Type:</strong> {type()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Status:</strong> {status()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Requester:</strong> {requester()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Email:</strong> {email()}</p>
</section>
<section class="card">
<h2 style="margin-bottom:8px">Remark Snapshot</h2>
<p class="notice" style="margin:0">
This route mirrors the Next.js verification detail entry point and delegates action workflow to Approval Management.
</p>
<div class="actions">
<A class="btn primary" href={`/admin/approval/${params.id}`}>Review & Take Action</A>
</div>
</section>
</div>
</Show>
</AdminShell>
);
}