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:
52
src/components/verwaltung/VerwaltungNav.tsx
Normal file
52
src/components/verwaltung/VerwaltungNav.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { de } from "@/lib/i18n/de";
|
||||
|
||||
/**
|
||||
* Sub-Navigation des Wehr-Bereichs. Client-Komponente nur wegen `usePathname`
|
||||
* (aktiver Zustand). Keine Geschäftslogik. Spiegelt die Admin-Navigation für
|
||||
* ein konsistentes Erscheinungsbild.
|
||||
*/
|
||||
const ITEMS = [
|
||||
{ href: "/verwaltung/profil", label: de.verwaltung.navProfil },
|
||||
{ href: "/verwaltung/fahrzeuge", label: de.verwaltung.navFahrzeuge },
|
||||
{ href: "/verwaltung/geraete", label: de.verwaltung.navGeraete },
|
||||
{ href: "/verwaltung/benutzer", label: de.verwaltung.navBenutzer },
|
||||
] as const;
|
||||
|
||||
export function VerwaltungNav() {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<nav
|
||||
aria-label="Wehr-Verwaltungsnavigation"
|
||||
className="border-b border-rand bg-white"
|
||||
>
|
||||
<div className="mx-auto flex max-w-5xl flex-wrap items-center gap-1 px-6 py-2">
|
||||
<span className="mr-4 font-display text-sm font-semibold text-navy">
|
||||
{de.verwaltung.titel}
|
||||
</span>
|
||||
{ITEMS.map((item) => {
|
||||
const active = pathname.startsWith(item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={cn(
|
||||
"rounded px-3 py-1.5 text-sm font-medium transition-colors",
|
||||
active
|
||||
? "bg-navy text-white"
|
||||
: "text-anthrazit/80 hover:bg-nebel hover:text-anthrazit",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user