- /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>
62 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
}
|