Geraeteformular zum Anlegen und Bearbeiten ergaenzen

Schliesst die letzte offene Luecke aus Phase 1. Bis dahin liessen sich Geraete
nur ueber den Import erfassen — fuer einen Bestand richtig, fuer ein einzelnes
nachbeschafftes Geraet umstaendlich.

Neu:
- /geraete/neu und /geraete/<id>/bearbeiten, beide ueber dieselbe Komponente
- Die Lagerung ist ein Umschalter statt zweier Auswahlfelder. Standort und
  Fahrzeug schliessen sich aus; so kann der Fehler gar nicht erst entstehen,
  den die Datenbank sonst abweisen wuerde. Beim Wechsel des Fahrzeugs wird die
  Raumangabe zurueckgesetzt.
- Prueftermin und Lebensdauerende werden beim Tippen vorgerechnet, mit
  denselben Funktionen wie auf dem Server. Beim Speichern rechnet der Server
  erneut und behaelt das letzte Wort.
- Gibt es nur einen Standort, ist er beim Anlegen vorbelegt.
- Knopf "Neues Geraet" in der Liste, "Bearbeiten" auf der Detailseite

Behoben:
- Das Formular stuerzte beim ersten Rechnen ab. Die Vorgabewerte lagen in einem
  "use client"-Modul und wurden von einer Server-Komponente importiert; dort
  kommt nur eine Client-Referenz an, kein Objekt, und das Spreaden ergibt lauter
  undefined. Die Werte liegen jetzt in geraet-formular-werte.ts ohne
  "use client".
- Das Sternchen an Pflichtfeldern wurde von Sprachausgaben mitgelesen
  ("Bezeichnung Stern"). Es traegt jetzt aria-hidden; dass ein Feld ausgefuellt
  werden muss, sagt bereits das required-Attribut.
- Fehlermeldungen in Formularen sind eindeutig ansteuerbar. Next.js haengt
  einen eigenen Routen-Ansager mit der Rolle "alert" in jede Seite.

Tests:
- 7 neue Playwright-Tests zum Formular, darunter die Lagerungsregel und die
  Fristenvorschau
- Zwei davon laufen nicht unter Touch-Emulation: Chromium nimmt dort keine
  zeichenweise Eingabe in Datumsfelder an. Begruendet uebersprungen, die
  Rechnung selbst deckt qr-und-fristen.test.ts ab.
- Insgesamt 104 Unit-Tests und 56 E2E-Tests gruen

Anleitung, Changelog, Devlog, Aufwandsschaetzung und Roadmap fortgeschrieben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-03 09:20:30 +02:00
co-authored by Claude Opus 5
parent 47b8e36a38
commit 1744403620
26 changed files with 1237 additions and 22 deletions
+55
View File
@@ -0,0 +1,55 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { ArrowLeft } from "lucide-react";
import { aktuelleSitzung } from "@/lib/auth";
import { hatRecht } from "@/lib/authz";
import { fuerOrganisation } from "@/lib/db";
import { Button } from "@/components/ui/button";
import { GeraetFormular } from "@/components/stammdaten/geraet-formular";
import { LEERES_GERAET } from "@/components/stammdaten/geraet-formular-werte";
export const metadata = { title: "Neues Gerät" };
/**
* Ein einzelnes Gerät anlegen.
*
* Der übliche Weg für einen Bestand ist der Import; dieses Formular ist für
* den Neuzugang zwischendurch — die eine Motorsäge, die nachbeschafft wurde.
*/
export default async function NeuesGeraetSeite() {
const sitzung = await aktuelleSitzung();
if (!sitzung) redirect("/anmelden");
if (!hatRecht(sitzung, "geraet.bearbeiten")) redirect("/geraete");
// Gibt es nur einen Standort, wird er vorbelegt — das ist bei den meisten
// Wehren der Fall, und ein Pflichtfeld weniger ist ein Klick weniger.
const standorte = await fuerOrganisation(sitzung.organisationId, (db) =>
db.standort.findMany({
where: { geloeschtAm: null, aktiv: true },
select: { id: true },
take: 2,
}),
);
const vorgabe = {
...LEERES_GERAET,
standortId: standorte.length === 1 ? (standorte[0]?.id ?? null) : null,
};
return (
<div className="mx-auto max-w-4xl space-y-4">
<Button variant="ghost" size="sm" asChild className="-ml-2">
<Link href="/geraete">
<ArrowLeft aria-hidden />
Zurück zur Liste
</Link>
</Button>
<GeraetFormular
vorgabe={vorgabe}
titel="Neues Gerät"
beschreibung="Für einzelne Neuzugänge. Größere Bestände gehen schneller über den Import."
/>
</div>
);
}