feat(admin): add v2 orders list (CardList with status pill + relative time)
This commit is contained in:
@@ -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 (
|
||||||
|
<main>
|
||||||
|
<PageHeader title="Orders" actions={<OrdersFilterButton />} />
|
||||||
|
<EmptyState
|
||||||
|
title="No orders yet"
|
||||||
|
description="When customers place orders, they'll show up here."
|
||||||
|
icon={<span aria-hidden="true">📦</span>}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<PageHeader
|
||||||
|
title="Orders"
|
||||||
|
subtitle={`${orders.length} order${orders.length === 1 ? "" : "s"}`}
|
||||||
|
actions={<OrdersFilterButton />}
|
||||||
|
/>
|
||||||
|
<PullToRefresh>
|
||||||
|
<CardList ariaLabel="Orders">
|
||||||
|
{orders.map((order) => {
|
||||||
|
const total = Number(order.subtotal ?? 0);
|
||||||
|
return (
|
||||||
|
<CardListItem key={order.id} href={`/admin/v2/orders/${order.id}`}>
|
||||||
|
<div className="flex items-start justify-between gap-3 mb-2">
|
||||||
|
<StatusPill status={statusKind(order)} />
|
||||||
|
<span
|
||||||
|
className="text-mono"
|
||||||
|
style={{ color: "var(--color-text-faint)" }}
|
||||||
|
>
|
||||||
|
#{order.id.slice(0, 8)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-meta ml-auto"
|
||||||
|
style={{ color: "var(--color-text-muted)" }}
|
||||||
|
>
|
||||||
|
{formatRelativeTime(order.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-h2"
|
||||||
|
style={{ fontWeight: 700, lineHeight: 1.3 }}
|
||||||
|
>
|
||||||
|
{order.customer_name ?? "Customer"}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<span
|
||||||
|
className="text-h1 font-display"
|
||||||
|
style={{ fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
${total.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ color: "var(--color-text-faint)" }}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardList>
|
||||||
|
</PullToRefresh>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user