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:
2026-08-10 08:54:23 +02:00
parent d2799852fd
commit 401b199978
40 changed files with 7238 additions and 179 deletions

View 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>
);
}