Workstream 7: Wehr-Bereich — Fuhrpark & Benutzer (Phase 4)
Implementiert den auf die eigene brigadeId beschränkten Wehr-Bereich: Profil (inkl. Inline-Geocoding via geocodeAddress), Fuhrpark (Fahrzeug per Vorlage oder frei, typisierter Merkmal-Editor), Geräte (Kategorie, Werte, Zuordnung Fahrzeug/„im Gerätehaus") und Benutzerkonten (wehr_admin/wehr_read). - Schema importiert (nicht neu definiert); ASCII-Property wehrfuehrer. - Default-deny dreifach: Layout-Guard requireWehrAdmin() + jede Server Action beginnt mit requireWehrAdmin(); fremde Entities -> notFound() (404). - Validierung an der Grenze (Zod): buildMerkmalValuesSchema validiert Werte typgerecht gegen die serverseitig aufgelösten Definitionen; Rolle auf wehr_admin|wehr_read beschränkt (platform_admin abgelehnt). - upsertMerkmalValues delete-then-insert mit typisierter Drizzle-Tx (kein any); boolean false/num 0 gelten als gesetzt. - argon2id-Einmalpasswort beim Benutzeranlegen; Selbst-Deaktivierung verhindert. - Audit vollständig: brigade.profile_update, vehicle.create/update/delete/status, equipment.create/update/delete/status, user.create/deactivate. - Vorgabewerte aus drei typisierten Spalten (vorgabewert_num/_text/_bool). - i18n via zentraler de.ts; loading/empty/error-konforme Listen. Tests: 22 neue Unit-Tests (vehicle/equipment/brigade-user-Validierung, upsertMerkmalValues) grün; Playwright-Specs verwaltung-fuhrpark + -scoping geschrieben (deferred: kein Server/DB in der Sandbox). Verifikation offline: tsc --noEmit clean, eslint clean, vitest 147 passed, next build exit 0 (alle /verwaltung/*-Routen), drizzle-kit check ohne Drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
55
src/app/(app)/verwaltung/fahrzeuge/page.tsx
Normal file
55
src/app/(app)/verwaltung/fahrzeuge/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import Link from "next/link";
|
||||
import { requireWehrAdmin } from "@/lib/auth/guards";
|
||||
import { listVehiclesForBrigade } from "@/server/data/vehicles";
|
||||
import { de } from "@/lib/i18n/de";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StatusBadge } from "@/components/ui/badge";
|
||||
|
||||
export default async function FahrzeugeListePage() {
|
||||
const s = await requireWehrAdmin();
|
||||
const items = await listVehiclesForBrigade(s.user.brigadeId);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
<h1 className="font-display text-2xl font-semibold text-navy">
|
||||
{de.verwaltung.navFahrzeuge}
|
||||
</h1>
|
||||
<Button asChild>
|
||||
<Link href="/verwaltung/fahrzeuge/neu">
|
||||
{de.verwaltung.fahrzeugAnlegen}
|
||||
</Link>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<p className="rounded border border-rand bg-white p-6 text-sm text-anthrazit/60">
|
||||
{de.verwaltung.keineFahrzeuge}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-rand rounded border border-rand bg-white">
|
||||
{items.map((v) => (
|
||||
<li
|
||||
key={v.id}
|
||||
className="flex items-center justify-between gap-4 px-4 py-3"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<Link
|
||||
href={`/verwaltung/fahrzeuge/${v.id}`}
|
||||
className="font-medium text-navy hover:underline"
|
||||
>
|
||||
{v.name}
|
||||
</Link>
|
||||
<p className="truncate text-sm text-anthrazit/60">
|
||||
{v.funkrufname ?? de.search.keinFunkrufname}
|
||||
{v.templateName ? ` · ${v.templateName}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge status={v.status} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user