151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
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;
|