Files
route-commerce/src/app/admin/v2/orders/[id]/page.tsx
T

190 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { notFound } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAdminOrderDetail, type AdminOrder } from "@/actions/orders";
import { formatDate } from "@/lib/format-date";
import PageHeader from "@/components/admin/PageHeader";
import StatusPill, { type StatusKind } from "@/components/admin/StatusPill";
import StickyActionBar from "@/components/admin/StickyActionBar";
import OrderActionButtons from "@/components/admin/orders/OrderActionButtons";
export const dynamic = "force-dynamic";
/**
* Map the legacy `orders.status` + `pickup_complete` pair onto the
* StatusPill's `StatusKind` union. Must match the helper in
* `orders/page.tsx` so the list and detail pages agree on the
* pill vocabulary. Kept inlined here to avoid a shared-types file
* until we know the mapping is stable.
*/
type OrderStatusKind = "placed" | "ready" | "picked-up" | "cancelled";
function statusKind(o: { status: string; pickup_complete: boolean }): OrderStatusKind {
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 OrderDetailV2Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const adminUser = await getAdminUser();
if (!adminUser) return null;
const order = await getAdminOrderDetail(id);
if (!order) notFound();
const kind = statusKind(order);
const subtotal = Number(order.subtotal ?? 0);
const taxAmount = Number(order.tax_amount ?? 0);
const discountAmount = Number(order.discount_amount ?? 0);
const total = subtotal + taxAmount - discountAmount;
const itemCount = order.order_items?.length ?? 0;
return (
<main className="pb-40">
<PageHeader
title={`Order #${order.id.slice(0, 8).toUpperCase()}`}
subtitle={formatDate(order.created_at)}
actions={<StatusPill status={kind as StatusKind} />}
/>
<div className="px-4 space-y-4">
<section
className="rounded-2xl p-4"
style={{ backgroundColor: "var(--color-surface-2)" }}
>
<h2
className="text-h1 font-display"
style={{ fontWeight: 600 }}
>
{order.customer_name}
</h2>
<div className="mt-2 space-y-1">
{order.customer_phone && (
<a
href={`tel:${order.customer_phone}`}
className="block text-body"
style={{ color: "var(--color-accent)" }}
>
{order.customer_phone}
</a>
)}
{order.customer_email && (
<a
href={`mailto:${order.customer_email}`}
className="block text-body"
style={{ color: "var(--color-accent)" }}
>
{order.customer_email}
</a>
)}
</div>
</section>
{order.stops && (
<section
className="rounded-2xl p-4"
style={{ backgroundColor: "var(--color-surface-2)" }}
>
<h3
className="text-label font-semibold mb-1"
style={{ color: "var(--color-text-muted)", letterSpacing: "0.02em" }}
>
PICKUP
</h3>
<div className="text-h2" style={{ fontWeight: 700, lineHeight: 1.3 }}>
{order.stops.city}, {order.stops.state}
</div>
{order.stops.date && (
<div
className="text-meta mt-1"
style={{ color: "var(--color-text-muted)" }}
>
{formatDate(order.stops.date)}
</div>
)}
</section>
)}
<section
className="rounded-2xl p-4"
style={{ backgroundColor: "var(--color-surface-2)" }}
>
<h3
className="text-label font-semibold mb-3"
style={{ color: "var(--color-text-muted)", letterSpacing: "0.02em" }}
>
ITEMS ({itemCount})
</h3>
{itemCount > 0 ? (
<ul className="space-y-3">
{order.order_items?.map((item: NonNullable<AdminOrder["order_items"]>[number]) => {
const unitPrice = Number(item.price ?? 0);
const lineTotal = unitPrice * item.quantity;
return (
<li key={item.id} className="flex items-center gap-3">
<div className="flex-1 min-w-0">
<div className="text-h2" style={{ fontWeight: 700 }}>
{item.products?.name ?? "Unknown product"}
</div>
<div
className="text-meta"
style={{ color: "var(--color-text-muted)" }}
>
{item.quantity} × ${unitPrice.toFixed(2)}
</div>
</div>
<div className="text-body font-semibold">
${lineTotal.toFixed(2)}
</div>
</li>
);
})}
</ul>
) : (
<div
className="text-body"
style={{ color: "var(--color-text-muted)" }}
>
No items
</div>
)}
<div
className="mt-4 pt-3 border-t flex items-center justify-between"
style={{ borderColor: "var(--color-surface-3)" }}
>
<span
className="text-label font-semibold"
style={{ color: "var(--color-text-muted)", letterSpacing: "0.02em" }}
>
TOTAL
</span>
<span
className="text-h1 font-display"
style={{ fontWeight: 600 }}
>
${total.toFixed(2)}
</span>
</div>
</section>
{order.internal_notes && (
<section
className="rounded-2xl p-4 italic"
style={{ backgroundColor: "var(--color-surface-2)" }}
>
<p className="text-body">&ldquo;{order.internal_notes}&rdquo;</p>
</section>
)}
</div>
<StickyActionBar>
<OrderActionButtons orderId={order.id} status={kind} />
</StickyActionBar>
</main>
);
}