nxtgauge-admin-solid/src/routes/admin/jobs.tsx

182 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import type { CrudRecord } from '~/lib/admin/types';
const API = '/api/gateway';
type JobRecord = CrudRecord & {
title: string;
company?: string;
location?: string;
rate?: string;
status: 'ACTIVE' | 'DRAFT' | 'PENDING_APPROVAL' | 'CLOSED' | 'EXPIRED';
postedDate?: string;
};
const FALLBACK_JOBS: JobRecord[] = [
{ id: 'j1', name: 'Senior UI/UX Designer', title: 'Senior UI/UX Designer', company: 'Creative Designs', location: 'Remote', rate: '₹800₹1200/hr', status: 'ACTIVE', postedDate: '2026-03-25', updatedAt: '2026-03-27' },
{ id: 'j2', name: 'Full Stack Developer', title: 'Full Stack Developer', company: 'Tech Solutions Inc', location: 'Mumbai', rate: '₹1000₹1500/hr', status: 'PENDING_APPROVAL', postedDate: '2026-03-24', updatedAt: '2026-03-27' },
{ id: 'j3', name: 'Marketing Specialist', title: 'Marketing Specialist', company: 'Global Brands', location: 'Delhi', rate: '₹500₹700/hr', status: 'DRAFT', postedDate: '2026-03-26', updatedAt: '2026-03-27' },
];
function StatusBadge(props: { status: string }) {
const getColors = () => {
switch (props.status) {
case 'ACTIVE': return { border: '#B7E4C7', bg: '#DEF7E8', text: '#0B8A4A', dot: '#0B8A4A' };
case 'PENDING_APPROVAL': return { border: '#F6D78F', bg: '#FFF3D6', text: '#B7791F', dot: '#B7791F' };
case 'DRAFT': return { border: '#D1D5DB', bg: '#F3F4F6', text: '#4B5563', dot: '#9CA3AF' };
case 'CLOSED': return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
case 'EXPIRED': return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
default: return { border: '#D1D5DB', bg: '#F3F4F6', text: '#4B5563', dot: '#9CA3AF' };
}
};
const colors = getColors();
const label = props.status.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' ');
return (
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${colors.border};background:${colors.bg};color:${colors.text};padding:2px 10px;font-size:11px;font-weight:600`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${colors.dot};margin-right:5px;flex-shrink:0`} />
{label}
</span>
);
}
export default function JobsManagementPage() {
const [view, setView] = createSignal<'list' | 'detail'>('list');
const [listTab, setListTab] = createSignal<'all' | 'active' | 'pending'>('all');
const [search, setSearch] = createSignal('');
const [rows, setRows] = createSignal<JobRecord[]>([]);
const [selectedJob, setSelectedJob] = createSignal<JobRecord | null>(null);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const load = async () => {
try {
const res = await fetch(`${API}/api/jobs?limit=100`);
if (!res.ok) throw new Error();
const data = await res.json();
const list = Array.isArray(data) ? data : (data.jobs || []);
if (list.length === 0) setRows(FALLBACK_JOBS);
else setRows(list.map(j => ({
id: j.id,
name: j.title || '—',
title: j.title || '—',
company: j.company_name || j.client_name || '—',
location: j.location || '—',
rate: j.hourly_rate_min ? `${j.hourly_rate_min}–₹${j.hourly_rate_max ?? j.hourly_rate_min}/hr` : '—',
status: (j.status || 'ACTIVE').toUpperCase(),
postedDate: j.created_at ? new Date(j.created_at).toLocaleDateString() : '—',
updatedAt: j.updated_at || ''
} as JobRecord)));
} catch {
setRows(FALLBACK_JOBS);
}
};
onMount(() => void load());
const filteredRows = createMemo(() => {
let list = rows();
if (listTab() === 'active') list = list.filter(r => r.status === 'ACTIVE');
if (listTab() === 'pending') list = list.filter(r => r.status === 'PENDING_APPROVAL');
const q = search().toLowerCase();
if (q) list = list.filter(r => r.title.toLowerCase().includes(q) || r.company?.toLowerCase().includes(q));
return list;
});
const openDetail = (row: JobRecord) => { setSelectedJob(row); setView('detail'); setOpenMenuId(null); };
return (
<AdminShell>
<div class="w-full space-y-6 pb-8">
<div style="margin-bottom: 1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Jobs Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Review and manage live job postings from companies and clients</p>
</div>
<Show when={view() === 'list'}>
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
{ key: 'all', label: 'All Jobs', action: () => setListTab('all') },
{ key: 'active', label: 'Active', action: () => setListTab('active') },
{ key: 'pending', label: 'Pending Approval', action: () => setListTab('pending') },
] as const).map((tab) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
>
{tab.label}
</button>
))}
</div>
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
placeholder="Search jobs..."
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
/>
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer">Filters</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Job Title', 'Company', 'Rate', 'Location', 'Status', 'Actions'].map(h => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
))}
</tr>
</thead>
<tbody>
<For each={filteredRows()}>
{(row) => (
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">{row.title}</td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.company || '—'}</td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.rate || '—'}</td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.location || '—'}</td>
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
<td style="padding:12px 20px;position:relative">
<button type="button" onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#9CA3AF;background:none;border:none;cursor:pointer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
</button>
<Show when={openMenuId() === row.id}>
<div style="position:absolute;right:20px;top:44px;z-index:20;width:180px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
<button type="button" onClick={() => openDetail(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">View Details</button>
<button type="button" style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">Close Job</button>
</div>
</Show>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
</Show>
<Show when={view() === 'detail' && selectedJob()}>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
<div>
<h2 style="font-size:20px;font-weight:700;color:#111827">{selectedJob()!.title}</h2>
<p style="font-size:14px;color:#6B7280;margin-top:2px">{selectedJob()!.company} Posted {selectedJob()!.postedDate}</p>
</div>
<div style="display:flex;gap:10px">
<StatusBadge status={selectedJob()!.status} />
<button type="button" onClick={() => setView('list')} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
</div>
</div>
<div style="padding:24px">
<p style="font-size:14px;color:#6B7280">Full job description, applicant list, and status history will be displayed here.</p>
</div>
</div>
</Show>
</div>
</AdminShell>
);
}