nxtgauge-admin-solid/src/routes/admin/jobs.tsx
Ashwin Kumar 5cfa4b89be ui(step-3): apply reference layout to 14 more management pages
- pricing, credit, coupon, discount, tax, order, invoice: white header,
  data-table/table-card, navy buttons, inline styles removed
- review, leads, jobs, notifications, support, report, ledger: same
  pattern + orange tab underlines where applicable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 05:20:55 +01:00

159 lines
6.7 KiB
TypeScript

import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
import { A } from '@solidjs/router';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type Job = {
id: string;
title: string;
description?: string;
required_skills?: string[];
experience_level?: string;
job_type?: string;
status: string;
client_name?: string;
company_name?: string;
location?: string;
hourly_rate_min?: number;
hourly_rate_max?: number;
created_at?: string;
};
async function loadJobs(): Promise<Job[]> {
try {
const res = await fetch(`${API}/api/jobs?limit=100`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
return Array.isArray(data) ? data : (data.jobs || []);
} catch {
return [];
}
}
const STATUS_OPTIONS = ['All', 'DRAFT', 'ACTIVE', 'PENDING_APPROVAL', 'CLOSED', 'EXPIRED'];
function statusChipClass(status: string): string {
if (status === 'ACTIVE') return 'status-chip active';
if (status === 'PENDING_APPROVAL') return 'status-chip pending';
if (status === 'DRAFT') return 'status-chip draft';
if (status === 'CLOSED' || status === 'EXPIRED') return 'status-chip danger';
return 'status-chip';
}
export default function JobsPage() {
const [jobs] = createResource(loadJobs);
const [search, setSearch] = createSignal('');
const [statusFilter, setStatusFilter] = createSignal('All');
const filtered = createMemo(() => {
const all = jobs() ?? [];
const q = search().toLowerCase();
const st = statusFilter();
return all.filter((job) => {
const matchesSearch =
!q ||
job.title?.toLowerCase().includes(q) ||
job.client_name?.toLowerCase().includes(q) ||
job.company_name?.toLowerCase().includes(q);
const matchesStatus = st === 'All' || job.status === st;
return matchesSearch && matchesStatus;
});
});
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="bg-white border-b border-gray-200 px-6 py-4">
<h1 class="text-xl font-semibold text-gray-900">Jobs Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Review live company job postings</p>
</div>
<div class="flex-1 p-6">
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
<input
type="text"
placeholder="Search by title or company..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:260px"
/>
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
>
<For each={STATUS_OPTIONS}>{(opt) => <option value={opt}>{opt === 'All' ? 'All Statuses' : opt}</option>}</For>
</select>
</div>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Skills</th>
<th>Company / Client</th>
<th>Rate</th>
<th>Location</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={jobs.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!jobs.loading && jobs.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!jobs.loading && !jobs.error && filtered().length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No jobs found.</td></tr>
</Show>
<Show when={!jobs.loading && !jobs.error && filtered().length > 0}>
<For each={filtered()}>
{(job) => (
<tr class="hover:bg-slate-50">
<td>
<div class="font-semibold text-slate-900">{job.title || '—'}</div>
<Show when={job.description}>
<div style="font-size:12px;color:#64748b;margin-top:2px;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
{job.description}
</div>
</Show>
</td>
<td class="text-slate-500">
{job.required_skills?.join(', ') || job.experience_level || '—'}
</td>
<td class="text-slate-500">{job.client_name || job.company_name || '—'}</td>
<td class="text-slate-500">
{job.hourly_rate_min != null
? `${job.hourly_rate_min}–₹${job.hourly_rate_max ?? job.hourly_rate_min}/hr`
: '—'}
</td>
<td class="text-slate-500">{job.location || '—'}</td>
<td>
<span class={statusChipClass(job.status)}>{job.status || '—'}</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/jobs/${job.id}`}>View</A>
<Show when={job.status === 'PENDING_APPROVAL'}>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/approval">Review</A>
</Show>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</div>
</div>
</AdminShell>
);
}