feat(admin): add jobs and verification parity routes
This commit is contained in:
parent
7e65337c60
commit
6c6667b8be
5 changed files with 407 additions and 0 deletions
118
src/routes/admin/jobs/[id].tsx
Normal file
118
src/routes/admin/jobs/[id].tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
src/routes/admin/verification-status.tsx
Normal file
101
src/routes/admin/verification-status.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
src/routes/admin/verification-status/[id].tsx
Normal file
84
src/routes/admin/verification-status/[id].tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/routes/admin/verification.tsx
Normal file
24
src/routes/admin/verification.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/routes/admin/verification/[id].tsx
Normal file
80
src/routes/admin/verification/[id].tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue