feat(stops): align with products page design
Deploy to route.crispygoat.com / deploy (push) Successful in 4m4s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m4s
- Use StopTableClient in server page component - Add view mode toggle (table/card) matching products page - Match stats cards styling to products page - Add consistent filter bar layout with AdminViewModeTabs - Add card view for stops with inline delete confirm - Improve row actions with Edit button + dropdown menu - Add 'Today' badge and past stop dimming in card view
This commit is contained in:
@@ -2,7 +2,7 @@ import { pool } from "@/lib/db";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import StopTableClient from "@/components/admin/StopTableClient";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const StopIcon = () => (
|
||||
@@ -25,7 +25,6 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
|
||||
let stops: unknown[] = [];
|
||||
let error: string | null = null;
|
||||
let totalCount = 0;
|
||||
|
||||
// Resolve active brand from cookie/URL (respects platform_admin "All brands" = null)
|
||||
let activeBrandId: string | null = null;
|
||||
@@ -40,11 +39,6 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
? "1=1"
|
||||
: `brand_id IN ('${(adminUser.brand_ids ?? []).join("','")}')`;
|
||||
|
||||
const countResult = await pool.query<{ count: string }>(
|
||||
`SELECT count(*) as count FROM stops WHERE ${brandCondition}`
|
||||
);
|
||||
totalCount = parseInt(countResult.rows[0]?.count ?? "0", 10);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT s.id, s.city, s.state, s.date, s."time", s.location, s.status,
|
||||
s.brand_id, s.address, s.zip, s.cutoff_date,
|
||||
@@ -56,7 +50,7 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
LIMIT 200`
|
||||
);
|
||||
stops = rows;
|
||||
console.log("[admin/stops] Fetched", rows.length, "stops, total:", totalCount);
|
||||
console.log("[admin/stops] Fetched", rows.length, "stops");
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : String(e);
|
||||
console.error("[admin/stops] Query error:", error);
|
||||
@@ -88,76 +82,28 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Transform stops for the client component
|
||||
const stopsForClient = stops.map((s: any) => ({
|
||||
id: s.id,
|
||||
city: s.city,
|
||||
state: s.state,
|
||||
date: s.date instanceof Date ? s.date.toISOString().split("T")[0] : String(s.date).split("T")[0],
|
||||
time: s.time || "",
|
||||
location: s.location,
|
||||
active: s.status === "active",
|
||||
brand_id: s.brand_id,
|
||||
status: s.status,
|
||||
address: s.address,
|
||||
zip: s.zip,
|
||||
cutoff_time: s.cutoff_date,
|
||||
brands: s.brand_name ? { name: s.brand_name } : null,
|
||||
}));
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="px-4 sm:px-6 md:px-8 py-6 sm:py-8">
|
||||
<PageHeader
|
||||
breadcrumb={[
|
||||
{ label: "Admin", href: "/admin" },
|
||||
{ label: "Stops & Routes" }
|
||||
]}
|
||||
icon={<StopIcon />}
|
||||
title="Stops & Routes"
|
||||
subtitle={`${totalCount} stops total`}
|
||||
/>
|
||||
<StopTableClient stops={stopsForClient} />
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 md:px-8">
|
||||
{stops.length === 0 ? (
|
||||
<div className="rounded-2xl border border-dashed border-stone-300 bg-white p-16 text-center">
|
||||
<div className="mx-auto mb-4 h-14 w-14 rounded-full bg-stone-100 flex items-center justify-center">
|
||||
<svg className="h-7 w-7 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="font-display text-2xl font-medium text-stone-950">No stops found</h3>
|
||||
<p className="mt-2 text-stone-500">
|
||||
{activeBrandId
|
||||
? "No stops for the selected brand. Upload a schedule or add stops manually."
|
||||
: "No stops in the database."}
|
||||
</p>
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<a
|
||||
href="/admin/stops/new"
|
||||
className="rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-emerald-500"
|
||||
>
|
||||
Add Stop
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-stone-200 bg-white overflow-hidden">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-stone-50 border-b border-stone-200">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-semibold text-stone-600">City</th>
|
||||
<th className="px-4 py-3 font-semibold text-stone-600">Location</th>
|
||||
<th className="px-4 py-3 font-semibold text-stone-600">Date</th>
|
||||
<th className="px-4 py-3 font-semibold text-stone-600">Status</th>
|
||||
<th className="px-4 py-3 font-semibold text-stone-600">Brand</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-stone-100">
|
||||
{stops.map((s: any) => (
|
||||
<tr key={s.id} className="hover:bg-stone-50">
|
||||
<td className="px-4 py-3 font-medium text-stone-900">{s.city}, {s.state}</td>
|
||||
<td className="px-4 py-3 text-stone-600">{s.location}</td>
|
||||
<td className="px-4 py-3 font-mono text-stone-500">{String(s.date)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
s.status === "active" ? "bg-emerald-100 text-emerald-700" :
|
||||
s.status === "draft" ? "bg-amber-100 text-amber-700" :
|
||||
"bg-stone-100 text-stone-500"
|
||||
}`}>{s.status}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-stone-500 text-xs">{s.brand_name ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user