685a1269a4
- Stops list, new, and detail pages use new PageHeader with Operations eyebrow, Stops & Routes title, and editorial subtitle. - StopsCalendarClient: replace hardcoded amber (#f59e0b) and brown (#92400e) draft markers with --admin-warning / --admin-warning-soft tokens. Calendar structure unchanged (drag/click/edit preserved). - Add/Edit/StopDetail modals: replace hardcoded red rgba with --admin-danger-soft and color-mix derived borders. - StopsLocationsTabs: swap hardcoded emerald-* for --admin-primary / --admin-primary-soft tokens. - StopMessagingForm: align with design system (ha-field, ha-eyebrow, tokens). - StopProductAssignment error surface: tokens instead of red rgba. - StopsHeaderActions already tokenized via AdminButton; no change. TS clean. Calendar functionality unchanged.
136 lines
4.7 KiB
TypeScript
136 lines
4.7 KiB
TypeScript
import { pool } from "@/lib/db";
|
|
import { getAdminUser } from "@/lib/admin-permissions";
|
|
import { getActiveBrandId } from "@/lib/brand-scope";
|
|
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
|
import StopTableClient from "@/components/admin/StopTableClient";
|
|
import { PageHeader } from "@/components/admin/design-system";
|
|
import { redirect } from "next/navigation";
|
|
|
|
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">
|
|
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/>
|
|
<circle cx="12" cy="10" r="3"/>
|
|
</svg>
|
|
);
|
|
|
|
interface PageProps {
|
|
searchParams: Promise<{ page?: string }>;
|
|
}
|
|
|
|
export default async function AdminStopsPage({ searchParams }: PageProps) {
|
|
const params = await searchParams;
|
|
const adminUser = await getAdminUser();
|
|
|
|
if (!adminUser) return <AdminAccessDenied />;
|
|
if (!adminUser.can_manage_stops) redirect("/admin/pickup");
|
|
|
|
interface DbStopRow {
|
|
id: string;
|
|
city: string;
|
|
state: string;
|
|
date: Date | string;
|
|
time: string | null;
|
|
location: string;
|
|
status: string;
|
|
brand_id: string;
|
|
address: string | null;
|
|
zip: string | null;
|
|
cutoff_date: string | null;
|
|
brand_name?: string;
|
|
}
|
|
let stops: DbStopRow[] = [];
|
|
let error: string | null = null;
|
|
|
|
// Resolve active brand from cookie/URL (respects platform_admin "All brands" = null)
|
|
let activeBrandId: string | null = null;
|
|
try {
|
|
activeBrandId = await getActiveBrandId(adminUser);
|
|
|
|
// If brand-scoped (not platform_admin) and no active brand, restrict query
|
|
const brandCondition =
|
|
activeBrandId
|
|
? `brand_id = '${activeBrandId}'`
|
|
: adminUser.role === "platform_admin"
|
|
? "1=1"
|
|
: `brand_id IN ('${(adminUser.brand_ids ?? []).join("','")}')`;
|
|
|
|
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");
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : String(e);
|
|
console.error("[admin/stops] Query error:", error);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<main className="min-h-screen bg-[var(--admin-bg)]">
|
|
<div className="px-4 sm:px-6 md:px-8 py-6 sm:py-8">
|
|
<div className="max-w-6xl mx-auto">
|
|
<p className="ha-eyebrow mb-2">Operations</p>
|
|
<PageHeader
|
|
title="Stops & Routes"
|
|
subtitle="Schedule and manage pickup locations and dates."
|
|
icon={<StopIcon />}
|
|
/>
|
|
<div className="rounded-2xl border border-[var(--admin-danger)]/30 bg-[var(--admin-danger-soft)] p-5">
|
|
<h2 className="text-lg font-semibold text-[var(--admin-danger)]">Error loading stops</h2>
|
|
<pre className="mt-3 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-[var(--admin-bg-subtle)] text-sm text-[var(--admin-text-secondary)]">
|
|
<p className="font-semibold text-[var(--admin-text-primary)]">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>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
// Transform stops for the client component
|
|
const stopsForClient = stops.map((s) => ({
|
|
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 (
|
|
<div className="min-h-screen bg-[var(--admin-bg)]">
|
|
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
|
<p className="ha-eyebrow mb-2">Operations</p>
|
|
<PageHeader
|
|
title="Stops & Routes"
|
|
subtitle="Schedule and manage pickup locations and dates."
|
|
icon={<StopIcon />}
|
|
/>
|
|
</div>
|
|
<div className="px-4 sm:px-6 md:px-8 pb-6 sm:pb-8">
|
|
<StopTableClient stops={stopsForClient} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|