feat(admin): add v2 order detail fulfillment timeline (Placed → Ready → Picked up)
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user