Add order management UI
Rebuild the landing page as an order overview for the Speisekarte24 tool: - Two tabs: all orders and "Nicht übermittelt", the latter fed by the 15-minute rule and highlighted while it holds entries - Table, list and grid views behind an icon dropdown, plus search, filters, sorting and pagination - Escalation timeline (three transmissions, tag, voice message, direct call, SOS) rendered in the detail dialog, inline in the list view and condensed to the current step in the table - Action definitions live in one module and feed the row menu, the detail dialog and the card quick-action bar Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
115
src/components/orders/order-timeline.tsx
Normal file
115
src/components/orders/order-timeline.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { Check, CircleAlert, Clock, Minus, TriangleAlert } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { formatTime } from "@/lib/format";
|
||||
import { buildTimeline, type StepState, type TimelineStep } from "@/lib/timeline";
|
||||
import type { Order, OrderAction } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Props = {
|
||||
order: Order;
|
||||
now: number;
|
||||
onAction: (order: Order, action: OrderAction) => void;
|
||||
};
|
||||
|
||||
const ICON: Record<StepState, typeof Check> = {
|
||||
done: Check,
|
||||
due: CircleAlert,
|
||||
pending: Clock,
|
||||
skipped: Minus,
|
||||
};
|
||||
|
||||
export function OrderTimeline({ order, now, onAction }: Props) {
|
||||
const steps = buildTimeline(order, now);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase">Verlauf</p>
|
||||
<ol className="flex flex-col">
|
||||
{steps.map((step, index) => (
|
||||
<Step
|
||||
key={step.id}
|
||||
step={step}
|
||||
last={index === steps.length - 1}
|
||||
order={order}
|
||||
onAction={onAction}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step({
|
||||
step,
|
||||
last,
|
||||
order,
|
||||
onAction,
|
||||
}: {
|
||||
step: TimelineStep;
|
||||
last: boolean;
|
||||
order: Order;
|
||||
onAction: (order: Order, action: OrderAction) => void;
|
||||
}) {
|
||||
const Icon = ICON[step.state];
|
||||
const danger = step.danger && step.state !== "pending" && step.state !== "skipped";
|
||||
|
||||
return (
|
||||
<li className="flex gap-3">
|
||||
<div className="flex flex-col items-center">
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-6 shrink-0 items-center justify-center rounded-full border",
|
||||
step.state === "done" && "border-transparent bg-primary text-primary-foreground",
|
||||
step.state === "due" && "border-transparent bg-amber-500 text-white",
|
||||
step.state === "pending" && "bg-background text-muted-foreground",
|
||||
step.state === "skipped" && "bg-muted text-muted-foreground",
|
||||
danger && "bg-destructive text-white",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
</span>
|
||||
{!last && <span className="w-px flex-1 bg-border" />}
|
||||
</div>
|
||||
|
||||
<div className={cn("flex-1 pb-4", last && "pb-0")}>
|
||||
<div className="flex flex-wrap items-baseline gap-x-2">
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
step.state === "pending" && "text-muted-foreground",
|
||||
step.state === "skipped" && "text-muted-foreground line-through",
|
||||
danger && "text-destructive",
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</p>
|
||||
{step.at !== undefined && (
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{step.state === "pending" ? "ab " : ""}
|
||||
{formatTime(new Date(step.at).toISOString())} Uhr
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{step.description && (
|
||||
<p className="text-xs text-muted-foreground">{step.description}</p>
|
||||
)}
|
||||
|
||||
{step.state === "due" && step.action && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant={step.danger ? "destructive" : "outline"}
|
||||
className="mt-2"
|
||||
onClick={() => onAction(order, step.action!)}
|
||||
>
|
||||
{step.danger && <TriangleAlert />}
|
||||
{step.danger ? "SOS auslösen" : "Jetzt erledigen"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user