import Link from "next/link";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import { pool } from "@/lib/db";
import { getMobileDashboardSummary } from "@/actions/dashboard";
import { getAdminOrders } from "@/actions/orders";
import PageHeader from "@/components/admin/PageHeader";
import EmptyState from "@/components/admin/EmptyState";
import StatusPill, { type StatusKind } from "@/components/admin/StatusPill";
import PullToRefresh from "@/components/admin/PullToRefresh";
import { formatDate, formatRelativeTime } from "@/lib/format-date";
export const dynamic = "force-dynamic";
/**
* Row shape for the "Today's stops" inline query. Slimmer than the
* full v2 stops page shape — the dashboard only needs the time + a
* displayable address. The query mirrors `src/app/admin/v2/stops/page.tsx`'s
* `s.date = $1` pattern (stops uses a `date` DATE column, not
* `scheduled_at`).
*/
interface DashboardStopRow {
id: string;
time: string | null;
location: string;
address: string | null;
city: string | null;
state: string | null;
brand_id: string;
}
/**
* Map the legacy order status onto the StatusPill vocabulary. Mirrors
* the v2 orders page's `statusKind()` — `'pending' | 'confirmed'` is
* the v1 schema's "needs attention" set (everything before fulfillment).
*/
function orderStatusKind(status: string): StatusKind {
if (status === "canceled") return "cancelled";
if (status === "fulfilled") return "picked-up";
if (status === "confirmed") return "ready";
return "placed";
}
/** Format a stop's free-form time string (24h or 12h) for display. */
function formatStopTime(time: string | null): string {
if (!time) return "—";
const t = time.trim();
if (/[ap]m$/i.test(t)) return t.toUpperCase();
const m = t.match(/^(\d{1,2}):(\d{2})$/);
if (m) {
let h = parseInt(m[1], 10);
const min = m[2];
const ampm = h >= 12 ? "PM" : "AM";
if (h === 0) h = 12;
else if (h > 12) h -= 12;
return `${h}:${min} ${ampm}`;
}
return t;
}
/** Pick the best address string we have on a stop row. */
function stopAddress(stop: DashboardStopRow): string {
if (stop.address) return stop.address;
if (stop.location) return stop.location;
const cityState = [stop.city, stop.state].filter(Boolean).join(", ");
return cityState || "Stop";
}
/**
* Resolve the "today" YYYY-MM-DD string in the server's local timezone.
* Kept in sync with `src/app/admin/v2/stops/page.tsx` so a dashboard
* click-through lands on the matching stops list.
*/
function todayDateStr(): string {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
}
export default async function DashboardV2Page() {
const adminUser = await getAdminUser();
if (!adminUser) return null;
const activeBrandId = await getActiveBrandId(adminUser);
// Compute today's date string once per request on the server so the
// PageHeader subtitle is a stable, formatted value (avoids any
// hydration-mismatch risk from rendering `new Date()` inline in JSX).
const todayLabel = formatDate(new Date());
if (!activeBrandId) {
return (
🌾}
/>
);
}
const brandId = activeBrandId;
const dateStr = todayDateStr();
// Parallel fetch: stat summary (RPC), today's stops, and pending
// orders. The stops query mirrors the v2 stops page's pool pattern;
// `getAdminOrders` is the shared legacy RPC.
const [summary, stopsResult, ordersResult] = await Promise.all([
getMobileDashboardSummary(brandId),
pool.query(
`SELECT s.id, s."time", s.location, s.address, s.city, s.state, s.brand_id
FROM stops s
WHERE s.date = $1 AND s.brand_id = $2
ORDER BY s."time" ASC NULLS LAST, s.location ASC
LIMIT 5`,
[dateStr, brandId],
),
getAdminOrders(),
]);
const todayStops = stopsResult.rows;
const allOrders = ordersResult.success ? ordersResult.orders : [];
const pendingOrders = allOrders
.filter((o) => o.status === "pending" || o.status === "confirmed")
.slice(0, 5);
const hasAnyData =
summary.orders_today > 0 ||
summary.revenue_today > 0 ||
summary.pending_fulfillment > 0 ||
summary.stops_today > 0;
return (
{/* "Needs you" callout — surfaces the most actionable count
* at the top of the page. Tapping jumps to the filtered
* orders list. */}
{summary.pending_fulfillment > 0 && (
NEEDS YOU
{summary.pending_fulfillment} order
{summary.pending_fulfillment === 1 ? "" : "s"} waiting
View all →
)}
{/* 2x2 stat grid. The "Pending" card picks up the warning
* accent when there's anything to act on. */}
0}
/>
{/* Today's stops — at-a-glance list of the next 5 runs so
* the operator can see where they need to be today. */}
{todayStops.length > 0 && (
)}
{/* Top 5 pending orders — drilled into from the "Needs you"
* callout above. Each row tappable to the order detail. */}
{pendingOrders.length > 0 && (
)}
{/* Empty dashboard — no orders, no revenue, no stops. Keep
* the page useful instead of showing four zeros. */}
{!hasAnyData && (
🌱}
/>
)}
);
}
/**
* Single stat tile. Mirrors the v2 product/stop cards' rounded-2xl +
* surface-2 background. `accent` swaps the fill + value color to the
* warning pair so the "Pending" card stands out when there's work to do.
*/
function StatCard({
label,
value,
accent = false,
}: {
label: string;
value: string;
accent?: boolean;
}) {
return (