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
@@ -0,0 +1,77 @@
import Link from "next/link";
import { notFound, 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,
alsFeldwert,
} from "@/components/stammdaten/geraet-formular-werte";
export const metadata = { title: "Gerät bearbeiten" };
/** Ein vorhandenes Gerät bearbeiten. */
export default async function GeraetBearbeitenSeite({
params,
}: {
params: Promise<{ id: string }>;
}) {
const sitzung = await aktuelleSitzung();
if (!sitzung) redirect("/anmelden");
const { id } = await params;
if (!hatRecht(sitzung, "geraet.bearbeiten")) redirect(`/geraete/${id}`);
const geraet = await fuerOrganisation(sitzung.organisationId, (db) =>
db.geraet.findUnique({ where: { id } }),
);
if (!geraet) notFound();
const vorgabe = {
...LEERES_GERAET,
id: geraet.id,
inventarnummer: geraet.inventarnummer,
bezeichnung: geraet.bezeichnung,
geraetetypId: geraet.geraetetypId,
hersteller: geraet.hersteller ?? "",
modell: geraet.modell ?? "",
seriennummer: geraet.seriennummer ?? "",
herstellungsdatum: alsFeldwert(geraet.herstellungsdatum),
indienststellung: alsFeldwert(geraet.indienststellung),
// Decimal aus Prisma: als deutscher Text ins Feld, damit das Komma bleibt.
anschaffungspreis: geraet.anschaffungspreis
? String(geraet.anschaffungspreis).replace(".", ",")
: "",
status: geraet.status,
pruefintervallMonate: geraet.pruefintervallMonate?.toString() ?? "",
letztePruefung: alsFeldwert(geraet.letztePruefung),
lebensdauerJahre: geraet.lebensdauerJahre?.toString() ?? "",
lagerungsart: (geraet.fahrzeugId ? "fahrzeug" : "standort") as "fahrzeug" | "standort",
standortId: geraet.standortId,
fahrzeugId: geraet.fahrzeugId,
geraeteraumId: geraet.geraeteraumId,
lagerort: geraet.lagerort ?? "",
bemerkung: geraet.bemerkung ?? "",
};
return (
<div className="mx-auto max-w-4xl space-y-4">
<Button variant="ghost" size="sm" asChild className="-ml-2">
<Link href={`/geraete/${geraet.id}`}>
<ArrowLeft aria-hidden />
Zurück zum Gerät
</Link>
</Button>
<GeraetFormular
vorgabe={vorgabe}
titel={geraet.bezeichnung}
beschreibung={`Inventarnummer ${geraet.inventarnummer}. Jede Änderung wird protokolliert.`}
/>
</div>
);
}