Files
craftvia/src/components/field/order-card.tsx
T
msolarczekandClaude Opus 5 95f41b29f5 L4 Einsatz mobil: Mobile Shell und Einsatz-Oberfläche
- /m in eigene Route-Group (field)/m verschoben (emergency-Platzhalter mit),
  Zugriffsprüfungen aus (app)/layout.tsx nach server/app-access.ts extrahiert
  und von Backoffice- und Mobile-Shell gemeinsam genutzt
- Mobile Shell mit Bottom-Nav (Heute · Aufträge · Notdienst · Sync · Profil)
  und Online/Offline-Badge; Startseite rollenabhängig (Feldrollen → /m),
  Login-Default-Redirect auf /
- Heute, Auftragsliste mit Tabs, Auftragsdetail mit einer Primäraktion je
  Zustand, Unterseiten Fotos (Kamera, Kompression, Upload-Fortschritt),
  Notizen + Sprachaufnahme, Material mit Stepper, Checkliste, Zeiten mit
  Korrektur, Profil; Sync-Platzhalter für L7
- Client-Wrapper submitOp (lib/field/client-ops.ts), Upload mit Fortschritt,
  Bildkompression, Formatierung; Texte in messages/{de,en}/field.json

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:30:03 +02:00

62 lines
2.9 KiB
TypeScript

import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
import { ChevronRight, Clock, MapPin, Navigation, Siren } from "lucide-react";
import { cn } from "@/lib/utils";
import { fmtWindow } from "@/lib/field/format";
import type { OrderCard as OrderCardData } from "@/server/services/field/queries";
import { StatusBadge } from "./status-badge";
import { btnPrimary, toneClasses } from "./ui";
/** Large order card (Spec §22): number, customer, site address with map link, time window, status, primary button. */
export function OrderCard({ order }: { order: OrderCardData }) {
const t = useTranslations("field.card");
const locale = useLocale();
const window = fmtWindow(order.plannedStart, order.plannedEnd, locale);
return (
<article className={cn("rounded-xl border border-l-4 bg-card p-4 shadow-card", toneClasses(order.statusGroup).edge)}>
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="font-mono text-[13px] font-semibold text-muted-foreground">{order.number}</span>
<StatusBadge status={order.status} />
</div>
<h2 className="mt-2 text-[18px] leading-snug">{order.title}</h2>
<p className="mt-0.5 text-[15px] font-semibold text-foreground">{order.customerName}</p>
{(order.isEmergency || order.priority === "urgent" || order.priority === "high") && (
<p className="mt-1.5 inline-flex items-center gap-1.5 text-[13px] font-semibold text-[var(--risk)]">
<Siren className="size-4" aria-hidden />
{order.isEmergency ? t("emergency") : order.priority === "urgent" ? t("urgent") : t("high")}
</p>
)}
<dl className="mt-3 space-y-2 text-[15px]">
<div className="flex items-start gap-2">
<Clock className="mt-0.5 size-4.5 shrink-0 text-muted-foreground" aria-hidden />
<dd>{window ?? t("noDate")}</dd>
</div>
{order.address && (
<div className="flex items-start gap-2">
<MapPin className="mt-0.5 size-4.5 shrink-0 text-muted-foreground" aria-hidden />
<dd className="flex-1">
{order.siteName && <span className="block text-[13px] text-muted-foreground">{order.siteName}</span>}
{order.address}
</dd>
{order.mapsUrl && (
<a
href={order.mapsUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-12 min-w-12 items-center justify-center gap-1 rounded-xl border px-3 text-[13px] font-semibold text-primary"
>
<Navigation className="size-4" aria-hidden />
{t("route")}
</a>
)}
</div>
)}
</dl>
<Link href={`/m/orders/${order.id}`} className={cn(btnPrimary, "mt-4")}>
{t("open")}
<ChevronRight className="size-5" aria-hidden />
</Link>
</article>
);
}