Phase 2: Wartungs- und Pruefmanagement

Wartungsvorlagen mit Checklisten, Prueftermine aus einem naechtlichen Lauf,
Prueffristen-Dashboard, Pruefwizard mit Foto und Unterschrift, unveraenderliche
Protokolle als PDF, Maengel-Workflow, Rueckrufe und Lebensdauerueberwachung.

Elf neue Tabellen, alle mit organisationId und Row Level Security. Die
Unveraenderbarkeit abgeschlossener Protokolle setzt ein Datenbanktrigger
durch, nicht der Anwendungscode: eine Korrektur erzeugt eine neue Pruefung
mit Verweis auf die alte, beide bleiben lesbar.

79 neue Tests (47 Unit, 10 gegen eine echte Datenbank, 22 E2E).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-03 12:07:11 +02:00
co-authored by Claude Opus 5
parent 7f866ac35c
commit e503aafc18
97 changed files with 13367 additions and 133 deletions
@@ -0,0 +1,340 @@
"use client";
import * as React from "react";
import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
import { ChecklistenpunktTyp } from "@prisma/client";
import { trpc } from "@/lib/trpc/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Fehlermeldung, NichtsGefunden } from "@/components/stammdaten/anzeige";
import { PUNKTTYP_TEXT } from "@/lib/wartung/texte";
/**
* Checklisten-Editor.
*
* Die Reihenfolge ist fachlich wichtig: der Prüfwizard arbeitet die Punkte
* genau so ab, wie sie hier stehen. Ein Gerätewart legt sie in der Reihenfolge
* an, in der er das Gerät in die Hand nimmt — erst außen, dann innen, zuletzt
* die Messwerte.
*
* Umsortiert wird mit Pfeiltasten statt Ziehen und Fallenlassen: das
* funktioniert auch mit Handschuhen auf einem Tablet und mit der Tastatur.
*/
const LEERER_PUNKT = {
text: "",
typ: ChecklistenpunktTyp.JA_NEIN as ChecklistenpunktTyp,
pflicht: true,
sollwert: "",
einheit: "",
minWert: "",
maxWert: "",
hinweis: "",
};
export function ChecklistenEditor({
vorlageId,
darfBearbeiten,
}: {
vorlageId: string;
darfBearbeiten: boolean;
}) {
const [neu, setNeu] = React.useState(LEERER_PUNKT);
const [fehler, setFehler] = React.useState<string | null>(null);
const werkzeuge = trpc.useUtils();
const vorlage = trpc.wartungsvorlage.einzeln.useQuery({ id: vorlageId });
const aktualisieren = () => void werkzeuge.wartungsvorlage.einzeln.invalidate({ id: vorlageId });
const punktAnlegen = trpc.wartungsvorlage.punktAnlegen.useMutation({
onSuccess: () => {
setNeu(LEERER_PUNKT);
aktualisieren();
},
onError: (problem) => setFehler(problem.message),
});
const punktLoeschen = trpc.wartungsvorlage.punktLoeschen.useMutation({
onSuccess: aktualisieren,
onError: (problem) => setFehler(problem.message),
});
const sortieren = trpc.wartungsvorlage.punkteSortieren.useMutation({
onSuccess: aktualisieren,
onError: (problem) => setFehler(problem.message),
});
const punkte = vorlage.data?.punkte ?? [];
function verschieben(index: number, richtung: -1 | 1) {
const ziel = index + richtung;
if (ziel < 0 || ziel >= punkte.length) return;
const reihenfolge = punkte.map((punkt) => punkt.id);
[reihenfolge[index], reihenfolge[ziel]] = [reihenfolge[ziel]!, reihenfolge[index]!];
sortieren.mutate({ vorlageId, reihenfolge });
}
function punktHinzufuegen(ereignis: React.FormEvent) {
ereignis.preventDefault();
setFehler(null);
punktAnlegen.mutate({
vorlageId,
text: neu.text,
typ: neu.typ,
pflicht: neu.pflicht,
sollwert: neu.sollwert || null,
einheit: neu.einheit || null,
minWert: neu.minWert === "" ? null : Number(neu.minWert),
maxWert: neu.maxWert === "" ? null : Number(neu.maxWert),
hinweis: neu.hinweis || null,
sortierung: 0,
});
}
if (vorlage.isLoading) {
return <p className="text-sm text-[var(--muted-foreground)]">Wird geladen …</p>;
}
if (vorlage.error) return <Fehlermeldung meldung={vorlage.error.message} />;
if (!vorlage.data) return null;
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">{vorlage.data.name}</h1>
<p className="text-sm text-[var(--muted-foreground)]">
alle {vorlage.data.intervallMonate} Monate
{vorlage.data.geraetetyp && ` · ${vorlage.data.geraetetyp.name}`}
{vorlage.data.pruefvorschrift && ` · ${vorlage.data.pruefvorschrift}`}
{" · "}
{vorlage.data._count.pruefungen} Prüfungen bisher
</p>
</div>
{fehler && <Fehlermeldung meldung={fehler} />}
<Card>
<CardHeader>
<CardTitle>Checkliste</CardTitle>
<CardDescription>
Der Prüfwizard fragt die Punkte in dieser Reihenfolge ab. Pflichtpunkte müssen
beantwortet sein, bevor sich eine Prüfung abschließen lässt — und ein durchgefallener
Pflichtpunkt bedeutet „nicht bestanden".
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{punkte.length === 0 && (
<NichtsGefunden
titel="Noch kein Prüfpunkt"
hinweis="Eine Vorlage ohne Punkte erzeugt Termine, aber keine sinnvolle Prüfung."
/>
)}
<ol className="space-y-2">
{punkte.map((punkt, index) => (
<li
key={punkt.id}
className="flex flex-wrap items-start gap-3 rounded-md border border-[var(--border)] p-3"
>
<span className="tabellenzahlen w-6 shrink-0 pt-1 text-sm text-[var(--muted-foreground)]">
{index + 1}
</span>
<div className="min-w-48 flex-1">
<p className="text-sm font-medium">
{punkt.text}
{punkt.pflicht && (
<span className="ml-1 text-[var(--gesperrt)]" title="Pflichtpunkt">
*
</span>
)}
</p>
<p className="text-xs text-[var(--muted-foreground)]">
{PUNKTTYP_TEXT[punkt.typ]}
{punkt.sollwert && ` · Soll: ${punkt.sollwert}`}
{(punkt.minWert !== null || punkt.maxWert !== null) &&
` · zulässig ${punkt.minWert ?? "…"} bis ${punkt.maxWert ?? "…"}${
punkt.einheit ? ` ${punkt.einheit}` : ""
}`}
</p>
{punkt.hinweis && (
<p className="mt-1 text-xs text-[var(--muted-foreground)]">{punkt.hinweis}</p>
)}
</div>
{darfBearbeiten && (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => verschieben(index, -1)}
disabled={index === 0}
aria-label={`${punkt.text} nach oben`}
>
<ArrowUp aria-hidden />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => verschieben(index, 1)}
disabled={index === punkte.length - 1}
aria-label={`${punkt.text} nach unten`}
>
<ArrowDown aria-hidden />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
if (window.confirm(`Prüfpunkt "${punkt.text}" entfernen?`)) {
punktLoeschen.mutate({ id: punkt.id });
}
}}
aria-label={`${punkt.text} entfernen`}
>
<Trash2 aria-hidden />
</Button>
</div>
)}
</li>
))}
</ol>
</CardContent>
</Card>
{darfBearbeiten && (
<Card>
<CardHeader>
<CardTitle>Prüfpunkt ergänzen</CardTitle>
<CardDescription>
Bei einem Messwert entscheidet der zulässige Bereich über „in Ordnung" — gerechnet
wird das auf dem Server, nicht im Browser.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={punktHinzufuegen} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="punkttext">Frage oder Prüfschritt</Label>
<Input
id="punkttext"
required
value={neu.text}
onChange={(ereignis) => setNeu({ ...neu, text: ereignis.target.value })}
placeholder="Maskenkörper auf Risse und Verformung prüfen"
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="punkttyp">Art der Antwort</Label>
<Select
value={neu.typ}
onValueChange={(wert) =>
setNeu({ ...neu, typ: wert as ChecklistenpunktTyp })
}
>
<SelectTrigger id="punkttyp">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(PUNKTTYP_TEXT).map(([wert, text]) => (
<SelectItem key={wert} value={wert}>
{text}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="sollwert">Sollwert im Klartext</Label>
<Input
id="sollwert"
value={neu.sollwert}
onChange={(ereignis) => setNeu({ ...neu, sollwert: ereignis.target.value })}
placeholder="mindestens 270 bar"
/>
</div>
</div>
{neu.typ === ChecklistenpunktTyp.MESSWERT && (
<div className="grid gap-4 sm:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="minwert">kleinster zulässiger Wert</Label>
<Input
id="minwert"
type="number"
step="any"
value={neu.minWert}
onChange={(ereignis) => setNeu({ ...neu, minWert: ereignis.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxwert">größter zulässiger Wert</Label>
<Input
id="maxwert"
type="number"
step="any"
value={neu.maxWert}
onChange={(ereignis) => setNeu({ ...neu, maxWert: ereignis.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="einheit">Einheit</Label>
<Input
id="einheit"
value={neu.einheit}
onChange={(ereignis) => setNeu({ ...neu, einheit: ereignis.target.value })}
placeholder="bar"
/>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="hinweis">Hinweis für den Prüfer</Label>
<Input
id="hinweis"
value={neu.hinweis}
onChange={(ereignis) => setNeu({ ...neu, hinweis: ereignis.target.value })}
placeholder="Sichtprüfung bei gutem Licht, Maske dabei dehnen"
/>
</div>
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={neu.pflicht}
onCheckedChange={(an) => setNeu({ ...neu, pflicht: an === true })}
/>
Pflichtpunkt — davon hängt die Einsatzbereitschaft ab
</label>
{punktAnlegen.error && <Fehlermeldung meldung={punktAnlegen.error.message} />}
<Button type="submit" disabled={punktAnlegen.isPending}>
<Plus aria-hidden />
{punktAnlegen.isPending ? "wird ergänzt …" : "Punkt ergänzen"}
</Button>
</form>
</CardContent>
</Card>
)}
</div>
);
}
@@ -0,0 +1,37 @@
import { redirect } from "next/navigation";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { aktuelleSitzung } from "@/lib/auth";
import { hatRecht } from "@/lib/authz";
import { Button } from "@/components/ui/button";
import { ChecklistenEditor } from "@/app/(app)/pruefungen/vorlagen/[id]/checkliste";
export const metadata = { title: "Checkliste" };
/**
* Eine Vorlage mit ihrer Checkliste.
*
* Der Editor ist eine Client-Komponente: Punkte werden hinzugefügt, umsortiert
* und geändert, ohne dass die Seite dazwischen neu lädt.
*/
export default async function VorlageSeite({ params }: { params: Promise<{ id: string }> }) {
const sitzung = await aktuelleSitzung();
if (!sitzung) redirect("/anmelden");
const { id } = await params;
return (
<div className="space-y-6">
<div>
<Button variant="ghost" size="sm" asChild className="-ml-2 mb-1">
<Link href="/pruefungen/vorlagen">
<ArrowLeft aria-hidden />
Zurück zu den Vorlagen
</Link>
</Button>
</div>
<ChecklistenEditor vorlageId={id} darfBearbeiten={hatRecht(sitzung, "pruefung.planen")} />
</div>
);
}
@@ -0,0 +1,39 @@
import { redirect } from "next/navigation";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { aktuelleSitzung } from "@/lib/auth";
import { hatRecht } from "@/lib/authz";
import { Button } from "@/components/ui/button";
import { VorlagenVerwaltung } from "@/app/(app)/pruefungen/vorlagen/vorlagen-verwaltung";
export const metadata = { title: "Wartungsvorlagen" };
/**
* Wartungsvorlagen.
*
* Hier steht, was in welchem Takt geprüft wird. Ohne Vorlage entstehen keine
* Termine — deshalb ist das die erste Seite, die eine Wehr in Phase 2 ausfüllt.
*/
export default async function VorlagenSeite() {
const sitzung = await aktuelleSitzung();
if (!sitzung) redirect("/anmelden");
return (
<div className="space-y-6">
<div>
<Button variant="ghost" size="sm" asChild className="-ml-2 mb-1">
<Link href="/pruefungen">
<ArrowLeft aria-hidden />
Zurück zu den Fristen
</Link>
</Button>
<h1 className="text-2xl font-semibold tracking-tight">Wartungsvorlagen</h1>
<p className="text-sm text-[var(--muted-foreground)]">
Was an einem Gerätetyp regelmäßig zu prüfen ist — Intervall, Vorschrift und Checkliste.
</p>
</div>
<VorlagenVerwaltung darfBearbeiten={hatRecht(sitzung, "pruefung.planen")} />
</div>
);
}
@@ -0,0 +1,381 @@
"use client";
import * as React from "react";
import Link from "next/link";
import { Plus, Search, Trash2 } from "lucide-react";
import { trpc } from "@/lib/trpc/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Fehlermeldung, LadeZeilen, NichtsGefunden } from "@/components/stammdaten/anzeige";
/**
* Verwaltung der Wartungsvorlagen.
*
* Die Liste zeigt, was jede Vorlage kostet und bringt: Intervall, Zahl der
* Prüfpunkte, Zahl der offenen Termine. Die Checkliste selbst wird auf der
* Detailseite gepflegt — sie ist zu umfangreich für eine Tabellenzeile.
*/
const LEER = {
name: "",
geraetetypId: "",
intervallMonate: 12,
vorwarnzeitTage: 30,
pruefvorschrift: "",
standardDauerMinuten: "",
standardExtern: false,
automatisch: true,
beschreibung: "",
};
export function VorlagenVerwaltung({ darfBearbeiten }: { darfBearbeiten: boolean }) {
const [suche, setSuche] = React.useState("");
const [offen, setOffen] = React.useState(false);
const [formular, setFormular] = React.useState(LEER);
const [fehler, setFehler] = React.useState<string | null>(null);
const werkzeuge = trpc.useUtils();
const vorlagen = trpc.wartungsvorlage.liste.useQuery({
seite: 1,
seitengroesse: 100,
richtung: "asc",
suche: suche || undefined,
});
const geraetetypen = trpc.geraetetyp.liste.useQuery({
seite: 1,
seitengroesse: 200,
richtung: "asc",
});
const anlegen = trpc.wartungsvorlage.anlegen.useMutation({
onSuccess: () => {
setOffen(false);
setFormular(LEER);
void werkzeuge.wartungsvorlage.liste.invalidate();
},
onError: (problem) => setFehler(problem.message),
});
const loeschen = trpc.wartungsvorlage.loeschen.useMutation({
onSuccess: () => void werkzeuge.wartungsvorlage.liste.invalidate(),
onError: (problem) => setFehler(problem.message),
});
function absenden(ereignis: React.FormEvent) {
ereignis.preventDefault();
setFehler(null);
anlegen.mutate({
name: formular.name,
geraetetypId: formular.geraetetypId || null,
intervallMonate: Number(formular.intervallMonate),
vorwarnzeitTage: Number(formular.vorwarnzeitTage),
pruefvorschrift: formular.pruefvorschrift || null,
standardDauerMinuten: formular.standardDauerMinuten
? Number(formular.standardDauerMinuten)
: null,
standardExtern: formular.standardExtern,
automatisch: formular.automatisch,
beschreibung: formular.beschreibung || null,
aktiv: true,
});
}
const eintraege = vorlagen.data?.eintraege ?? [];
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
<div className="relative min-w-56 flex-1">
<Search
className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--muted-foreground)]"
aria-hidden
/>
<Input
value={suche}
onChange={(ereignis) => setSuche(ereignis.target.value)}
placeholder="Name oder Prüfvorschrift …"
className="pl-8"
aria-label="Vorlagen durchsuchen"
/>
</div>
{darfBearbeiten && (
<Button onClick={() => setOffen(true)}>
<Plus aria-hidden />
Neue Vorlage
</Button>
)}
</div>
{fehler && <Fehlermeldung meldung={fehler} />}
{vorlagen.error && <Fehlermeldung meldung={vorlagen.error.message} />}
<div className="rounded-lg border border-[var(--border)]">
<Table>
<TableHeader>
<TableRow>
<TableHead>Bezeichnung</TableHead>
<TableHead>Gerätetyp</TableHead>
<TableHead>Vorschrift</TableHead>
<TableHead className="text-right">Intervall</TableHead>
<TableHead className="text-right">Punkte</TableHead>
<TableHead className="text-right">offene Termine</TableHead>
<TableHead className="w-24" aria-label="Aktionen" />
</TableRow>
</TableHeader>
<TableBody>
{vorlagen.isLoading && <LadeZeilen spalten={7} />}
{eintraege.map((vorlage) => (
<TableRow key={vorlage.id}>
<TableCell className="font-medium">
<Link
href={`/pruefungen/vorlagen/${vorlage.id}`}
className="underline-offset-4 hover:underline"
>
{vorlage.name}
</Link>
{!vorlage.aktiv && (
<span className="ml-2 text-xs text-[var(--muted-foreground)]">(inaktiv)</span>
)}
{!vorlage.automatisch && (
<div className="text-xs text-[var(--muted-foreground)]">
keine automatische Planung
</div>
)}
</TableCell>
<TableCell className="text-sm">{vorlage.geraetetyp?.name ?? "—"}</TableCell>
<TableCell className="text-sm text-[var(--muted-foreground)]">
{vorlage.pruefvorschrift ?? "—"}
</TableCell>
<TableCell className="text-right tabellenzahlen">
{vorlage.intervallMonate} Mon.
</TableCell>
<TableCell className="text-right tabellenzahlen">{vorlage._count.punkte}</TableCell>
<TableCell className="text-right tabellenzahlen">{vorlage._count.termine}</TableCell>
<TableCell>
{darfBearbeiten && (
<Button
variant="ghost"
size="sm"
onClick={() => {
if (
window.confirm(
`Vorlage "${vorlage.name}" löschen? Offene Termine dazu entfallen.`,
)
) {
loeschen.mutate({ id: vorlage.id });
}
}}
aria-label={`Vorlage ${vorlage.name} löschen`}
>
<Trash2 aria-hidden />
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{!vorlagen.isLoading && eintraege.length === 0 && (
<div className="p-6">
<NichtsGefunden
titel="Noch keine Wartungsvorlage"
hinweis="Eine Vorlage sagt, was an einem Gerätetyp in welchem Takt zu prüfen ist. Ohne sie entstehen keine Termine — und ohne Termine kein Fristen-Dashboard."
aktion={
darfBearbeiten ? (
<Button onClick={() => setOffen(true)}>
<Plus aria-hidden />
Erste Vorlage anlegen
</Button>
) : undefined
}
/>
</div>
)}
</div>
<Dialog open={offen} onOpenChange={setOffen}>
<DialogContent>
<form onSubmit={absenden} className="space-y-4">
<DialogHeader>
<DialogTitle>Neue Wartungsvorlage</DialogTitle>
<DialogDescription>
Die Prüfpunkte kommen im nächsten Schritt dazu — erst die Regel, dann die Fragen.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="name">Bezeichnung</Label>
<Input
id="name"
required
value={formular.name}
onChange={(ereignis) =>
setFormular({ ...formular, name: ereignis.target.value })
}
placeholder="Sichtprüfung Pressluftatmer"
/>
</div>
<div className="space-y-2">
<Label htmlFor="geraetetyp">Gilt für Gerätetyp</Label>
<Select
value={formular.geraetetypId || "keiner"}
onValueChange={(wert) =>
setFormular({ ...formular, geraetetypId: wert === "keiner" ? "" : wert })
}
>
<SelectTrigger id="geraetetyp">
<SelectValue placeholder="Gerätetyp wählen" />
</SelectTrigger>
<SelectContent>
<SelectItem value="keiner">keiner — nur von Hand zuweisbar</SelectItem>
{geraetetypen.data?.eintraege.map((typ) => (
<SelectItem key={typ.id} value={typ.id}>
{typ.name}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-[var(--muted-foreground)]">
Ohne Gerätetyp plant der nächtliche Lauf keine Termine.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="intervall">Intervall in Monaten</Label>
<Input
id="intervall"
type="number"
min={1}
max={600}
required
value={formular.intervallMonate}
onChange={(ereignis) =>
setFormular({ ...formular, intervallMonate: Number(ereignis.target.value) })
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="vorwarn">Vorwarnzeit in Tagen</Label>
<Input
id="vorwarn"
type="number"
min={0}
max={365}
value={formular.vorwarnzeitTage}
onChange={(ereignis) =>
setFormular({ ...formular, vorwarnzeitTage: Number(ereignis.target.value) })
}
/>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="vorschrift">Prüfvorschrift</Label>
<Input
id="vorschrift"
value={formular.pruefvorschrift}
onChange={(ereignis) =>
setFormular({ ...formular, pruefvorschrift: ereignis.target.value })
}
placeholder="DGUV Vorschrift 49"
/>
</div>
<div className="space-y-2">
<Label htmlFor="dauer">Übliche Dauer in Minuten</Label>
<Input
id="dauer"
type="number"
min={1}
value={formular.standardDauerMinuten}
onChange={(ereignis) =>
setFormular({ ...formular, standardDauerMinuten: ereignis.target.value })
}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="beschreibung">Beschreibung</Label>
<Textarea
id="beschreibung"
rows={2}
value={formular.beschreibung}
onChange={(ereignis) =>
setFormular({ ...formular, beschreibung: ereignis.target.value })
}
/>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={formular.automatisch}
onCheckedChange={(an) => setFormular({ ...formular, automatisch: an === true })}
/>
Termine nachts automatisch planen
</label>
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={formular.standardExtern}
onCheckedChange={(an) =>
setFormular({ ...formular, standardExtern: an === true })
}
/>
Wird üblicherweise von einer Fachfirma durchgeführt
</label>
</div>
{anlegen.error && <Fehlermeldung meldung={anlegen.error.message} />}
<DialogFooter>
<Button type="button" variant="ghost" onClick={() => setOffen(false)}>
Abbrechen
</Button>
<Button type="submit" disabled={anlegen.isPending}>
{anlegen.isPending ? "wird angelegt …" : "Anlegen"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}