feat(admin): add v2 order detail fulfillment timeline (Placed → Ready → Picked up)

This commit is contained in:
Tyler
2026-06-17 14:52:45 -06:00
parent c67df118e2
commit 4c428fd9da
2 changed files with 157 additions and 0 deletions
+7
View File
@@ -6,6 +6,7 @@ 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";
import FulfillmentTimeline from "@/components/admin/orders/FulfillmentTimeline";
export const dynamic = "force-dynamic";
@@ -109,6 +110,12 @@ export default async function OrderDetailV2Page({
</section>
)}
<FulfillmentTimeline
status={kind}
placedAt={order.created_at}
pickedUpAt={order.pickup_completed_at}
/>
<section
className="rounded-2xl p-4"
style={{ backgroundColor: "var(--color-surface-2)" }}
@@ -0,0 +1,150 @@
import { formatDateTime } from "@/lib/format-date";
/**
* Horizontal stepper showing the order's progression:
* Placed → Ready → Picked up.
*
* Derived from the legacy `orders` row:
* - Placed: always set (created_at)
* - Ready: orders.status = 'confirmed' (no dedicated timestamp in the
* legacy schema; we surface "—" rather than fabricating one)
* - Picked up: orders.pickup_complete = true → pickup_completed_at
*
* Cancelled orders get a single "Cancelled" pill instead of the stepper
* — the timeline doesn't make sense for a voided order.
*/
export type FulfillmentStatus = "placed" | "ready" | "picked-up" | "cancelled";
interface FulfillmentTimelineProps {
status: FulfillmentStatus;
placedAt: string;
pickedUpAt: string | null;
}
const STEPS: { key: FulfillmentStatus; label: string }[] = [
{ key: "placed", label: "Placed" },
{ key: "ready", label: "Ready" },
{ key: "picked-up", label: "Picked up" },
];
function stepState(
step: FulfillmentStatus,
current: FulfillmentStatus,
): "done" | "current" | "upcoming" {
const order = STEPS.findIndex((s) => s.key === step);
const currentIdx = STEPS.findIndex((s) => s.key === current);
if (currentIdx === -1) return "upcoming";
if (order < currentIdx) return "done";
if (order === currentIdx) return "current";
return "upcoming";
}
export function FulfillmentTimeline({
status,
placedAt,
pickedUpAt,
}: FulfillmentTimelineProps) {
if (status === "cancelled") {
return (
<section
className="rounded-2xl p-4"
style={{ backgroundColor: "var(--color-surface-2)" }}
>
<h3
className="text-label font-semibold mb-2"
style={{ color: "var(--color-text-muted)", letterSpacing: "0.02em" }}
>
STATUS
</h3>
<div
className="text-h2"
style={{ fontWeight: 700, color: "var(--color-text-muted)" }}
>
Cancelled
</div>
</section>
);
}
return (
<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" }}
>
FULFILLMENT
</h3>
<ol className="flex items-start gap-2" aria-label="Fulfillment timeline">
{STEPS.map((step, i) => {
const state = stepState(step.key, status);
const isLast = i === STEPS.length - 1;
// Pick the right timestamp to display. The "Ready" step has no
// dedicated column in the legacy schema, so we show "—" — the
// user can read the mark-ready time from the audit log.
const ts =
step.key === "placed"
? placedAt
: step.key === "picked-up"
? pickedUpAt
: null;
const dotBg =
state === "done"
? "var(--color-accent)"
: state === "current"
? "var(--color-accent-2)"
: "var(--color-surface-3)";
const labelColor =
state === "upcoming" ? "var(--color-text-faint)" : "var(--color-text)";
return (
<li
key={step.key}
className="flex-1 min-w-0"
aria-current={state === "current" ? "step" : undefined}
>
<div className="flex items-center gap-2">
<span
className="inline-block rounded-full shrink-0"
style={{
width: "12px",
height: "12px",
backgroundColor: dotBg,
}}
aria-hidden="true"
/>
{!isLast && (
<span
aria-hidden="true"
className="flex-1 h-0.5"
style={{
backgroundColor:
state === "done"
? "var(--color-accent)"
: "var(--color-surface-3)",
}}
/>
)}
</div>
<div
className="text-label font-semibold mt-2"
style={{ color: labelColor, letterSpacing: "0.02em" }}
>
{step.label}
</div>
<div
className="text-mono mt-1"
style={{ color: "var(--color-text-faint)" }}
>
{ts ? formatDateTime(ts) : "—"}
</div>
</li>
);
})}
</ol>
</section>
);
}
export default FulfillmentTimeline;