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

109
src/lib/types.ts Normal file
View File

@@ -0,0 +1,109 @@
export type OrderStatus =
| "neu"
| "uebermittelt"
| "telefonisch"
| "nicht_uebermittelt"
| "rueckruf"
| "storniert"
| "fake";
export type OrderType = "vorbestellung" | "direktanruf";
/** Alles, was eine Bearbeiterin an einer Bestellung auslösen kann. */
export type OrderAction =
| "uebermittelt"
| "gastronom_voice"
| "gastronom_anruf"
| "kunde_anrufen"
| "nicht_uebermittelt"
| "rueckruf"
| "storno"
| "fake"
| "stornieren"
| "sos"
| "telefonisch_uebermittelt";
/** Protokolleintrag einer ausgelösten Aktion, treibt die Timeline. */
export type OrderEvent = { type: OrderAction; at: string };
export type PaymentMethod = "bar" | "ec" | "paypal" | "kreditkarte";
export type Restaurant = {
name: string;
city: string;
/** Öffnungszeiten, z. B. "10:00 bis 23:00" */
hours: string;
phone1: string;
phone2?: string;
mobile?: string;
orderUrl: string;
};
export type Customer = {
name: string;
email: string;
phone: string;
};
export type Order = {
id: string;
/** Fortlaufende Bestellnummer, wird in der Liste als erste Spalte gezeigt */
number: number;
status: OrderStatus;
type: OrderType;
restaurant: Restaurant;
customer: Customer;
/** Zeitpunkt des Bestelleingangs, ISO-String */
receivedAt: string;
/** Gewünschte Liefer- bzw. Abholzeit, ISO-String */
dueAt: string;
total: number;
itemCount: number;
payment: PaymentMethod;
pdfUrl: string;
/** Bestellung im externen Backoffice, öffnet in neuem Tab. */
externalUrl: string;
note?: string;
/** Zeitpunkt, zu dem der Gastronom die Bestellung bestätigt hat. */
confirmedAt?: string;
/** Manuell ausgelöste Schritte, ältester zuerst. */
events: OrderEvent[];
};
export const STATUS_LABEL: Record<OrderStatus, string> = {
neu: "Neu",
uebermittelt: "Übermittelt",
telefonisch: "Telefonisch übermittelt",
nicht_uebermittelt: "Nicht übermittelt",
rueckruf: "Rückrufhinweis",
storniert: "Storniert",
fake: "Fake",
};
export const TYPE_LABEL: Record<OrderType, string> = {
vorbestellung: "Vorbestellung",
direktanruf: "Direktanruf",
};
export const PAYMENT_LABEL: Record<PaymentMethod, string> = {
bar: "Bar",
ec: "EC-Karte",
paypal: "PayPal",
kreditkarte: "Kreditkarte",
};
/** Abstand zwischen den drei automatischen Übermittlungsversuchen. */
export const ATTEMPT_INTERVAL_MS = 5 * 60 * 1000;
export const ATTEMPT_COUNT = 3;
/** Nach dieser Zeit ohne Bearbeitung gilt eine Bestellung als nicht übermittelt. */
export const UNDELIVERED_AFTER_MS = 15 * 60 * 1000;
/**
* Eine unbearbeitete Bestellung kippt nach {@link UNDELIVERED_AFTER_MS} automatisch auf
* `nicht_uebermittelt`. Sobald jemand eine Aktion ausgelöst hat, bleibt der echte Status stehen.
*/
export function effectiveStatus(order: Order, now: number): OrderStatus {
const overdue = now - new Date(order.receivedAt).getTime() >= UNDELIVERED_AFTER_MS;
return order.status === "neu" && overdue ? "nicht_uebermittelt" : order.status;
}