Workstream 8: Detailseiten & Kontakt (Phase 5)
Drei serverseitige Lese-Detailseiten (Fahrzeug, Gerät, Wehr), default-deny:
- src/lib/detail/merkmale.ts: formatMerkmal (de-AT Tausenderpunkt + NBSP,
Ja/Nein, enum-Label, „–"), toEckdaten. ICU-unabhängige Zahl-Formatierung
(formatToParts -> Punkt/Komma), da de-AT je nach ICU-Build U+202F gruppiert.
- src/lib/detail/queries.ts: read-only, wehrübergreifend; loadMerkmalRows
(Join merkmal_values↔merkmale↔merkmal_optionen via wert=value_text),
getFahrzeugDetail (+Beladung, +WehrCard), getGeraetDetail (Fahrzeug-Link
oder „im Gerätehaus"), getWehrDetail (Fuhrpark + Geräte im Haus),
getBrigadeCard. UUID-IDs.
- Komponenten: detail/{DetailHeader,EckdatenGrid,BeladungListe,StatusBadge},
kontakt/{KontaktButton (tel:/mailto:, Telefon ohne Leerzeichen, subject;
Empty-State),WehrCard}.
- Seiten (app)/{fahrzeuge,geraete,wehren}/[id]/page.tsx mit requireSession()
als erster Zeile (Default-deny in der Tiefe) + fahrzeuge/[id]/not-found.tsx.
- i18n-Keys (detail/kontakt/wehr) ergänzt; keine hartkodierten Strings.
Tests: merkmale.test.ts (11), queries.test.ts (3, gemockte DB für
„im Gerätehaus" + not-found). Playwright detail-auth.spec.ts geschrieben
(deferred: kein Server/DB in Sandbox); Detailrouten ins Gating-Manifest
aufgenommen.
Offline verifiziert: vitest src/lib/detail grün; tsc --noEmit ok; eslint
ok; next build erfolgreich (alle drei [id]-Routen vorhanden).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
43
src/app/(app)/fahrzeuge/[id]/page.tsx
Normal file
43
src/app/(app)/fahrzeuge/[id]/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { requireSession } from "@/lib/auth/guards";
|
||||
import { getFahrzeugDetail } from "@/lib/detail/queries";
|
||||
import { DetailHeader } from "@/components/detail/DetailHeader";
|
||||
import { EckdatenGrid } from "@/components/detail/EckdatenGrid";
|
||||
import { BeladungListe } from "@/components/detail/BeladungListe";
|
||||
import { WehrCard } from "@/components/kontakt/WehrCard";
|
||||
import { t } from "@/lib/i18n/de";
|
||||
|
||||
export default async function FahrzeugDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
// Default-deny in der Tiefe (Querschnittsstandard 1): Guard als erste Zeile.
|
||||
await requireSession();
|
||||
const { id } = await params;
|
||||
const v = await getFahrzeugDetail(id);
|
||||
if (!v) notFound();
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-6">
|
||||
<DetailHeader
|
||||
kicker={v.templateName ?? t("nav.fahrzeuge")}
|
||||
titel={v.name}
|
||||
untertitel={v.funkrufname}
|
||||
status={v.status}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-[1fr_18rem]">
|
||||
<div className="flex flex-col gap-6">
|
||||
<EckdatenGrid rows={v.merkmale} />
|
||||
<BeladungListe items={v.beladung} />
|
||||
{v.notiz ? (
|
||||
<p className="whitespace-pre-line text-sm text-anthrazit/70">
|
||||
{v.notiz}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{v.wehr ? <WehrCard wehr={v.wehr} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user