diagnostic: simplify stops page to bare query + debug info
Deploy to route.crispygoat.com / deploy (push) Successful in 4m3s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m3s
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
import StopsDashboardClient from "@/components/admin/stops/StopsDashboardClient";
|
||||
import StopsHeaderActions from "@/components/admin/StopsHeaderActions";
|
||||
import { pool } from "@/lib/db";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
import { pool } from "@/lib/db";
|
||||
import { type Stop } from "@/components/admin/stops/types";
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const StopIcon = () => (
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-current" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -22,77 +17,43 @@ interface PageProps {
|
||||
|
||||
export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const page = Math.max(1, parseInt(params.page ?? "1", 10) || 1);
|
||||
const offset = (page - 1) * PAGE_SIZE;
|
||||
|
||||
const adminUser = await getAdminUser();
|
||||
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
if (!adminUser.can_manage_stops) redirect("/admin/pickup");
|
||||
|
||||
if (!adminUser.can_manage_stops) {
|
||||
redirect("/admin/pickup");
|
||||
}
|
||||
|
||||
let stops: Stop[] = [];
|
||||
let stops: unknown[] = [];
|
||||
let error: string | null = null;
|
||||
let totalCount = 0;
|
||||
let error: { message: string } | null = null;
|
||||
|
||||
try {
|
||||
const brandFilter = adminUser.brand_id
|
||||
? `AND brand_id = '${adminUser.brand_id}'`
|
||||
: "";
|
||||
// Platform admin sees all stops, brand admin sees their brand's stops
|
||||
const brandCondition = adminUser.brand_id
|
||||
? `brand_id = '${adminUser.brand_id}'`
|
||||
: "1=1";
|
||||
|
||||
// Get total count for pagination
|
||||
const countResult = await pool.query<{ count: string }>(`
|
||||
SELECT count(*) as count FROM stops WHERE status = 'active' ${brandFilter}
|
||||
`);
|
||||
const countResult = await pool.query<{ count: string }>(
|
||||
`SELECT count(*) as count FROM stops WHERE ${brandCondition}`
|
||||
);
|
||||
totalCount = parseInt(countResult.rows[0]?.count ?? "0", 10);
|
||||
|
||||
// Get paginated stops
|
||||
const { rows } = await pool.query<Stop & { brand_name: string } & { cutoff_date: string | null }>(`
|
||||
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,
|
||||
b.name as brand_name
|
||||
FROM stops s
|
||||
LEFT JOIN brands b ON b.id = s.brand_id
|
||||
WHERE s.status = 'active'
|
||||
${brandFilter}
|
||||
ORDER BY s.date ASC
|
||||
LIMIT ${PAGE_SIZE} OFFSET ${offset}
|
||||
`);
|
||||
|
||||
stops = rows.map((r) => ({
|
||||
id: r.id,
|
||||
city: r.city ?? "",
|
||||
state: r.state ?? "",
|
||||
date: r.date ? String(r.date) : "",
|
||||
time: r.time ?? "",
|
||||
location: r.location ?? "",
|
||||
active: r.status === "active",
|
||||
brand_id: r.brand_id,
|
||||
address: r.address,
|
||||
zip: r.zip,
|
||||
cutoff_time: r.cutoff_date,
|
||||
status: r.status ?? "active",
|
||||
brands: r.brand_name ? { name: r.brand_name } : { name: "" },
|
||||
}));
|
||||
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,
|
||||
b.name as brand_name
|
||||
FROM stops s
|
||||
LEFT JOIN brands b ON b.id = s.brand_id
|
||||
WHERE ${brandCondition}
|
||||
ORDER BY s.date ASC
|
||||
LIMIT 200`
|
||||
);
|
||||
stops = rows;
|
||||
console.log("[admin/stops] Fetched", rows.length, "stops, total:", totalCount);
|
||||
} catch (e) {
|
||||
console.error("[admin/stops] Failed to load stops:", e);
|
||||
error = { message: e instanceof Error ? e.message : String(e) };
|
||||
error = e instanceof Error ? e.message : String(e);
|
||||
console.error("[admin/stops] Query error:", error);
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(totalCount / PAGE_SIZE);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
@@ -103,12 +64,16 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
<span className="text-[var(--admin-text-muted)]">/</span>
|
||||
<span className="text-[var(--admin-text-primary)] font-medium">Stops & Routes</span>
|
||||
</nav>
|
||||
<h1 className="text-2xl sm:text-3xl font-black text-red-600 tracking-tight">
|
||||
Error loading stops
|
||||
</h1>
|
||||
<pre className="mt-4 rounded-xl bg-white border border-[var(--admin-border)] p-4 text-sm text-[var(--admin-text-secondary)]">
|
||||
{error.message}
|
||||
<h1 className="text-2xl sm:text-3xl font-black text-red-600 tracking-tight">Error loading stops</h1>
|
||||
<pre className="mt-4 rounded-xl bg-white border border-[var(--admin-border)] p-4 text-sm text-[var(--admin-text-secondary)] overflow-auto">
|
||||
{error}
|
||||
</pre>
|
||||
<div className="mt-4 p-4 rounded-xl bg-stone-100 text-sm">
|
||||
<p className="font-semibold">Debug info:</p>
|
||||
<p>adminUser.brand_id: {adminUser.brand_id ?? "null"}</p>
|
||||
<p>adminUser.role: {adminUser.role}</p>
|
||||
<p>adminUser.email: {adminUser.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -125,19 +90,65 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
]}
|
||||
icon={<StopIcon />}
|
||||
title="Stops & Routes"
|
||||
subtitle={adminUser?.brand_id ? "Managing stops for your brand." : "Manage routes, pickup locations, dates, and cutoff times."}
|
||||
actions={<StopsHeaderActions brandId={adminUser?.brand_id ?? ""} />}
|
||||
subtitle={`${totalCount} stops total`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
|
||||
<StopsDashboardClient
|
||||
stops={stops}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
totalCount={totalCount}
|
||||
/>
|
||||
<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">
|
||||
{adminUser.brand_id
|
||||
? "No stops for your brand yet. Upload a schedule or add stops manually."
|
||||
: "No stops in the database. Import the Tuxedo Tour schedule."}
|
||||
</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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user