From 9b9643e2c7ff3e7c7b63545907910cff73d17a2f Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 17 Jun 2026 14:44:24 -0600 Subject: [PATCH] feat(admin): add v2 orders list (CardList with status pill + relative time) --- src/app/admin/v2/orders/page.tsx | 119 +++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/app/admin/v2/orders/page.tsx diff --git a/src/app/admin/v2/orders/page.tsx b/src/app/admin/v2/orders/page.tsx new file mode 100644 index 0000000..b9a3aab --- /dev/null +++ b/src/app/admin/v2/orders/page.tsx @@ -0,0 +1,119 @@ +import { getAdminUser } from "@/lib/admin-permissions"; +import { getAdminOrders } from "@/actions/orders"; +import PageHeader from "@/components/admin/PageHeader"; +import { CardList, CardListItem } from "@/components/admin/CardList"; +import StatusPill, { type StatusKind } from "@/components/admin/StatusPill"; +import EmptyState from "@/components/admin/EmptyState"; +import OrdersFilterButton from "@/components/admin/orders/OrdersFilterButton"; +import PullToRefresh from "@/components/admin/PullToRefresh"; +import { formatRelativeTime } from "@/lib/format-date"; + +export const dynamic = "force-dynamic"; + +/** + * Map the legacy `orders.status` + `pickup_complete` pair onto the + * StatusPill's `StatusKind` union. The legacy schema uses + * ('pending' | 'confirmed' | 'fulfilled' | 'canceled') and a boolean + * `pickup_complete`; the v2 design uses the simpler + * (placed | ready | picked-up | cancelled) vocabulary. + */ +function statusKind(o: { status: string; pickup_complete: boolean }): StatusKind { + if (o.status === "canceled") return "cancelled"; + if (o.pickup_complete || o.status === "fulfilled") return "picked-up"; + if (o.status === "confirmed") return "ready"; + return "placed"; +} + +export default async function OrdersV2Page({ + searchParams, +}: { + searchParams: Promise<{ status?: string }>; +}) { + const adminUser = await getAdminUser(); + if (!adminUser) return null; + + const result = await getAdminOrders(); + const allOrders = result.success ? result.orders : []; + const params = await searchParams; + + // The legacy RPC doesn't accept a status filter — apply it client-side. + // The shape we filter on matches the StatusPill vocabulary (placed/ready/ + // picked-up/cancelled), not the legacy DB status string, so the user + // intent is the same. + const filtered = params.status + ? allOrders.filter((o) => statusKind(o) === params.status) + : allOrders; + + // Show at most 50 — the plan's example limit. Anything more is paginated + // (PR follow-up). + const orders = filtered.slice(0, 50); + + if (orders.length === 0) { + return ( +
+ } /> +
+ ); + } + + return ( +
+ } + /> + + + {orders.map((order) => { + const total = Number(order.subtotal ?? 0); + return ( + +
+ + + #{order.id.slice(0, 8)} + + + {formatRelativeTime(order.created_at)} + +
+
+ {order.customer_name ?? "Customer"} +
+
+ + ${total.toFixed(2)} + + +
+
+ ); + })} +
+
+
+ ); +}