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