nxtgauge-admin-solid/src/routes/admin/applications.tsx
Ashwin Kumar 3b2c09cd4b style: unify all primary buttons to use shared .btn-primary class
Replaced 37 files worth of inconsistent inline Tailwind button classes
(mixed font-medium/semibold, px-4/px-6, with/without shadow-sm, inline-flex
variants) with the shared .btn-primary CSS class. Added :disabled state to
.btn-primary in app.css so disabled buttons visually dim consistently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 08:10:29 +01:00

184 lines
7 KiB
TypeScript

import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type ApplicationRow = {
id: string;
requirementId: string;
professionalId: string;
status: string;
quote?: number;
message?: string;
createdAt?: string;
};
type Requirement = {
id: string;
title?: string;
location?: string;
profession?: string;
customerId?: string;
};
type ApplicationsPayload = {
applications: ApplicationRow[];
requirements: Requirement[];
};
async function fetchApplications(): Promise<ApplicationsPayload> {
const [appRes, reqRes] = await Promise.all([
fetch(`${API}/api/responses`),
fetch(`${API}/api/requirements`),
]);
const appData = appRes.ok ? await appRes.json() : {};
const reqData = reqRes.ok ? await reqRes.json() : {};
return {
applications: appData?.responses || appData?.applications || [],
requirements: reqData?.requirements || [],
};
}
export default function ApplicationsPage() {
const [refreshToken, setRefreshToken] = createSignal(0);
const [busyId, setBusyId] = createSignal('');
const [error, setError] = createSignal('');
const [payload] = createResource(refreshToken, fetchApplications);
const applications = createMemo(() => payload()?.applications || []);
const requirements = createMemo(() => payload()?.requirements || []);
async function updateStatus(id: string, status: string) {
setBusyId(id);
setError('');
try {
const res = await fetch(`${API}/api/responses/${id}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
if (!res.ok) {
const message = await res.text();
throw new Error(message || 'Failed to update status');
}
setRefreshToken((value) => value + 1);
} catch (nextError: any) {
setError(nextError?.message || 'Failed to update status');
} finally {
setBusyId('');
}
}
function requirementFor(id: string): Requirement | undefined {
return requirements().find((item) => item.id === id);
}
function statusBadge(status: string) {
if (status === 'ACCEPTED') return 'bg-green-100 text-green-800';
if (status === 'REJECTED' || status === 'WITHDRAWN') return 'bg-red-100 text-red-700';
return 'bg-yellow-100 text-yellow-800';
}
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
{/* ── Page header ── */}
<div class="bg-white border-b border-gray-200 px-6 py-4">
<h1 class="text-xl font-semibold text-gray-900">Applications</h1>
<p class="text-sm text-gray-500 mt-0.5">Review submitted applications and update acceptance status.</p>
</div>
{/* ── Content ── */}
<div class="p-6">
<Show when={error()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
<Show when={payload.loading}>
<div class="table-card">
<p class="py-10 text-center text-sm text-slate-400">Loading applications</p>
</div>
</Show>
<Show when={!payload.loading && applications().length === 0}>
<div class="table-card">
<p class="py-10 text-center text-sm text-slate-400">No applications found.</p>
</div>
</Show>
<div class="flex flex-col gap-4">
<For each={applications()}>
{(app) => {
const req = createMemo(() => requirementFor(app.requirementId));
return (
<div class="rounded-xl border border-gray-200 bg-white shadow-sm p-5">
<div class="flex items-start justify-between gap-3 flex-wrap">
<div>
<h2 class="text-base font-semibold text-gray-900">{req()?.title || 'Unknown Requirement'}</h2>
<p class="text-xs text-slate-400 mt-1">ID: {app.id}</p>
</div>
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${statusBadge(app.status)}`}>{app.status}</span>
</div>
<div class="mt-3 rounded-lg bg-gray-50 px-4 py-3">
<p class="text-xs font-medium text-gray-500 mb-1">Message</p>
<p class="text-sm text-gray-700">{app.message || 'No message provided.'}</p>
</div>
<div class="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-4">
<div>
<p class="text-xs text-gray-500">Quote</p>
<p class="text-sm font-medium text-gray-900"> {app.quote || 0}</p>
</div>
<div>
<p class="text-xs text-gray-500">Category</p>
<p class="text-sm font-medium text-gray-900">{req()?.profession || '—'}</p>
</div>
<div>
<p class="text-xs text-gray-500">Location</p>
<p class="text-sm font-medium text-gray-900">{req()?.location || '—'}</p>
</div>
<div>
<p class="text-xs text-gray-500">Applied On</p>
<p class="text-sm font-medium text-gray-900">{app.createdAt ? new Date(app.createdAt).toLocaleDateString() : '—'}</p>
</div>
</div>
<div class="mt-4 flex gap-2 flex-wrap">
<Show when={app.status === 'SUBMITTED' || app.status === 'SHORTLISTED'}>
<button
class="btn-primary"
disabled={busyId() === app.id}
onClick={() => updateStatus(app.id, 'ACCEPTED')}
>
Accept Bid
</button>
<button
class="rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={busyId() === app.id}
onClick={() => updateStatus(app.id, 'REJECTED')}
>
Decline
</button>
<button
class="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"
disabled={busyId() === app.id}
onClick={() => updateStatus(app.id, 'WITHDRAWN')}
>
Withdraw
</button>
</Show>
</div>
</div>
);
}}
</For>
</div>
</div>
</div>
</AdminShell>
);
}