Phase 3: Einsatz- und Uebungserfassung
Schnellerfassung im Alarmfall, sechs Zeitstempel mit Ausrueckzeit, Anfahrt und Hilfsfrist, Staerke nach FwDV 3, Personalvorschlag mit Qualifikationsstand, Einsatzbericht als PDF, Jahresexport fuer die Gemeinde, Statistik und Uebungen mit Anwesenheitsliste. Dazu das Einsatzportal fuer Fremdwehren: ein signierter Link ohne Anmeldung, zeitlich und mengenbegrenzt, der Stichwort, Ort, Zeiten und Lagemeldungen zeigt und Meldungen mit Foto annimmt. Es sind die ersten oeffentlich erreichbaren Endpunkte in FLORIAN. Acht neue Tabellen, alle mit organisationId und Row Level Security. Ein freigegebener Einsatzbericht ist unveraenderlich; ein Datenbanktrigger setzt das durch. Koordinaten liegen doppelt — als Dezimalgrad und als PostGIS-Punkt, zusammengehalten von einem Trigger. 57 neue Tests (32 Unit, 14 gegen eine echte Datenbank, 11 E2E). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bc62a9c1e8
commit
91954c402b
@@ -0,0 +1,32 @@
|
||||
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 { UebungDetail } from "@/app/(app)/uebungen/[id]/uebung-detail";
|
||||
|
||||
export const metadata = { title: "Übung" };
|
||||
|
||||
/** Eine Übung mit Anwesenheitsliste. */
|
||||
export default async function UebungSeite({ 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="/uebungen">
|
||||
<ArrowLeft aria-hidden />
|
||||
Zurück zu den Übungen
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<UebungDetail uebungId={id} darfBearbeiten={hatRecht(sitzung, "ausbildung.bearbeiten")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Save } 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Fehlermeldung } from "@/components/stammdaten/anzeige";
|
||||
import { UebungsartAbzeichen } from "@/components/einsatz/anzeige";
|
||||
import { alsDatum } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Übungsdetail mit Anwesenheitsliste.
|
||||
*
|
||||
* Die Liste ist der eigentliche Zweck der Seite: Namen abhaken, fertig. Sie
|
||||
* zeigt deshalb alle aktiven Mitglieder auf einmal und nicht eine Suche, in
|
||||
* der man jeden einzeln nachschlagen müsste — bei einer Übung ist die halbe
|
||||
* Wehr da.
|
||||
*/
|
||||
export function UebungDetail({
|
||||
uebungId,
|
||||
darfBearbeiten,
|
||||
}: {
|
||||
uebungId: string;
|
||||
darfBearbeiten: boolean;
|
||||
}) {
|
||||
const [meldung, setMeldung] = React.useState<string | null>(null);
|
||||
const [anwesend, setAnwesend] = React.useState<Set<string> | null>(null);
|
||||
const [bericht, setBericht] = React.useState<string | null>(null);
|
||||
|
||||
const werkzeuge = trpc.useUtils();
|
||||
const abfrage = trpc.uebung.einzeln.useQuery({ id: uebungId });
|
||||
// 200 ist die Höchstzahl je Seite (siehe gemeinsam.ts). Eine Wehr mit mehr
|
||||
// aktiven Mitgliedern bräuchte hier eine Blätterung — bei einer Freiwilligen
|
||||
// Feuerwehr ist das der theoretische Fall.
|
||||
const mitglieder = trpc.person.liste.useQuery({ seite: 1, seitengroesse: 200, richtung: "asc" });
|
||||
|
||||
// Die vorhandene Teilnahme einmal in den Bearbeitungsstand übernehmen.
|
||||
React.useEffect(() => {
|
||||
if (abfrage.data && anwesend === null) {
|
||||
setAnwesend(new Set(abfrage.data.teilnahmen.map((teilnahme) => teilnahme.personId)));
|
||||
}
|
||||
}, [abfrage.data, anwesend]);
|
||||
|
||||
const teilnahmeSetzen = trpc.uebung.teilnahmeSetzen.useMutation({
|
||||
onSuccess: (ergebnis) => {
|
||||
setMeldung(`${ergebnis.erfasst} Teilnehmer gespeichert.`);
|
||||
void werkzeuge.uebung.invalidate();
|
||||
},
|
||||
onError: (fehler) => setMeldung(fehler.message),
|
||||
});
|
||||
|
||||
const aendern = trpc.uebung.aendern.useMutation({
|
||||
onSuccess: () => {
|
||||
setMeldung("Gespeichert.");
|
||||
void werkzeuge.uebung.invalidate();
|
||||
},
|
||||
onError: (fehler) => setMeldung(fehler.message),
|
||||
});
|
||||
|
||||
if (abfrage.isLoading) {
|
||||
return <p className="text-sm text-[var(--muted-foreground)]">Wird geladen …</p>;
|
||||
}
|
||||
if (abfrage.error) return <Fehlermeldung meldung={abfrage.error.message} />;
|
||||
if (!abfrage.data) return null;
|
||||
|
||||
const uebung = abfrage.data;
|
||||
const personen = mitglieder.data?.eintraege ?? [];
|
||||
const anwesendeMenge = anwesend ?? new Set<string>();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{uebung.thema}</h1>
|
||||
<p className="flex flex-wrap items-center gap-2 text-sm text-[var(--muted-foreground)]">
|
||||
<span className="tabellenzahlen">{uebung.nummer}</span>
|
||||
<UebungsartAbzeichen art={uebung.art} />
|
||||
<span>· {alsDatum(uebung.datum)}</span>
|
||||
{uebung.ort && <span>· {uebung.ort}</span>}
|
||||
{uebung.leiter && (
|
||||
<span>
|
||||
· Leitung {uebung.leiter.nachname}, {uebung.leiter.vorname}
|
||||
</span>
|
||||
)}
|
||||
<span>· {uebung.teilnahmen.length} Teilnehmer</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{meldung && <Fehlermeldung meldung={meldung} kennung="uebungmeldung" />}
|
||||
|
||||
{uebung.lernziele && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lernziele</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="whitespace-pre-line text-sm">{uebung.lernziele}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<CardTitle>Anwesenheit</CardTitle>
|
||||
<CardDescription>
|
||||
{anwesendeMenge.size} von {personen.length} aktiven Mitgliedern
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{darfBearbeiten && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
teilnahmeSetzen.mutate({
|
||||
uebungId,
|
||||
teilnehmer: [...anwesendeMenge].map((personId) => ({ personId })),
|
||||
ersetzen: true,
|
||||
})
|
||||
}
|
||||
disabled={teilnahmeSetzen.isPending}
|
||||
>
|
||||
<Save aria-hidden />
|
||||
Anwesenheit speichern
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="grid gap-1.5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{personen.map((person) => (
|
||||
<label
|
||||
key={person.id}
|
||||
className="flex cursor-pointer items-center gap-2 rounded border border-[var(--border)] px-3 py-2 text-sm"
|
||||
>
|
||||
<Checkbox
|
||||
checked={anwesendeMenge.has(person.id)}
|
||||
disabled={!darfBearbeiten}
|
||||
onCheckedChange={(an) =>
|
||||
setAnwesend((alt) => {
|
||||
const neu = new Set(alt ?? []);
|
||||
if (an) neu.add(person.id);
|
||||
else neu.delete(person.id);
|
||||
return neu;
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
{person.nachname}, {person.vorname}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bericht</CardTitle>
|
||||
<CardDescription>
|
||||
Was gemacht wurde und was aufgefallen ist — die Grundlage für die nächste Übung.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{darfBearbeiten ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="uebungsbericht">Übungsbericht</Label>
|
||||
<Textarea
|
||||
id="uebungsbericht"
|
||||
rows={6}
|
||||
value={bericht ?? uebung.bericht ?? ""}
|
||||
onChange={(ereignis) => setBericht(ereignis.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="beginn">Beginn</Label>
|
||||
<Input
|
||||
id="beginn"
|
||||
type="time"
|
||||
defaultValue={
|
||||
uebung.beginnUm ? uebung.beginnUm.toISOString().slice(11, 16) : ""
|
||||
}
|
||||
onBlur={(ereignis) => {
|
||||
if (!ereignis.target.value) return;
|
||||
const [stunde, minute] = ereignis.target.value.split(":").map(Number);
|
||||
const zeit = new Date(uebung.datum);
|
||||
zeit.setUTCHours(stunde ?? 0, minute ?? 0, 0, 0);
|
||||
aendern.mutate({ id: uebungId, beginnUm: zeit });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ende">Ende</Label>
|
||||
<Input
|
||||
id="ende"
|
||||
type="time"
|
||||
defaultValue={uebung.endeUm ? uebung.endeUm.toISOString().slice(11, 16) : ""}
|
||||
onBlur={(ereignis) => {
|
||||
if (!ereignis.target.value) return;
|
||||
const [stunde, minute] = ereignis.target.value.split(":").map(Number);
|
||||
const zeit = new Date(uebung.datum);
|
||||
zeit.setUTCHours(stunde ?? 0, minute ?? 0, 0, 0);
|
||||
aendern.mutate({ id: uebungId, endeUm: zeit });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => aendern.mutate({ id: uebungId, bericht: bericht ?? "" })}
|
||||
disabled={aendern.isPending || bericht === null}
|
||||
>
|
||||
Bericht speichern
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<p className="whitespace-pre-line text-sm">
|
||||
{uebung.bericht ?? "Noch kein Bericht geschrieben."}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{uebung.dauerMinuten !== null && (
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
Dauer: {uebung.dauerMinuten} Minuten
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { aktuelleSitzung } from "@/lib/auth";
|
||||
import { hatRecht } from "@/lib/authz";
|
||||
import { UebungListe } from "@/app/(app)/uebungen/uebung-liste";
|
||||
|
||||
export const metadata = { title: "Übungen" };
|
||||
|
||||
/**
|
||||
* Übungen und Ausbildung.
|
||||
*
|
||||
* Der Nachweis nach innen: wer war dabei, wie oft, und woran wurde gearbeitet.
|
||||
* Daraus wird in der Jahreshauptversammlung die Teilnahmequote — und bei
|
||||
* Beförderungen die Grundlage.
|
||||
*/
|
||||
export default async function UebungenSeite() {
|
||||
const sitzung = await aktuelleSitzung();
|
||||
if (!sitzung) redirect("/anmelden");
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Übungen</h1>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Thema, Lernziele und Teilnahme — je Übung dokumentiert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UebungListe darfBearbeiten={hatRecht(sitzung, "ausbildung.bearbeiten")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Plus, Search } from "lucide-react";
|
||||
import { UebungsArt } 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 { Textarea } from "@/components/ui/textarea";
|
||||
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";
|
||||
import { UebungsartAbzeichen } from "@/components/einsatz/anzeige";
|
||||
import { UEBUNGSART_TEXT } from "@/lib/einsatz/texte";
|
||||
import { alsDatum } from "@/lib/utils";
|
||||
|
||||
/** Datum von heute im Format, das ein date-Feld erwartet. */
|
||||
function heute(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function UebungListe({ darfBearbeiten }: { darfBearbeiten: boolean }) {
|
||||
const router = useRouter();
|
||||
|
||||
const [suche, setSuche] = React.useState("");
|
||||
const [jahr, setJahr] = React.useState<number | "alle">(new Date().getFullYear());
|
||||
const [offen, setOffen] = React.useState(false);
|
||||
const [meldung, setMeldung] = React.useState<string | null>(null);
|
||||
|
||||
const [thema, setThema] = React.useState("");
|
||||
const [art, setArt] = React.useState<UebungsArt>(UebungsArt.PRAXIS);
|
||||
const [datum, setDatum] = React.useState(heute());
|
||||
const [ort, setOrt] = React.useState("");
|
||||
const [lernziele, setLernziele] = React.useState("");
|
||||
|
||||
const werkzeuge = trpc.useUtils();
|
||||
|
||||
const uebungen = trpc.uebung.liste.useQuery({
|
||||
seite: 1,
|
||||
seitengroesse: 100,
|
||||
richtung: "desc",
|
||||
suche: suche || undefined,
|
||||
jahr: jahr === "alle" ? undefined : jahr,
|
||||
});
|
||||
|
||||
const anlegen = trpc.uebung.anlegen.useMutation({
|
||||
onSuccess: (uebung) => {
|
||||
setOffen(false);
|
||||
setThema("");
|
||||
setLernziele("");
|
||||
void werkzeuge.uebung.invalidate();
|
||||
router.push(`/uebungen/${uebung.id}`);
|
||||
},
|
||||
onError: (fehler) => setMeldung(fehler.message),
|
||||
});
|
||||
|
||||
const eintraege = uebungen.data?.eintraege ?? [];
|
||||
const jahreAuswahl = Array.from({ length: 6 }, (_, index) => new Date().getFullYear() - index);
|
||||
|
||||
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="Thema, Nummer, Ort …"
|
||||
className="pl-8"
|
||||
aria-label="Übungen durchsuchen"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={String(jahr)}
|
||||
onValueChange={(wert) => setJahr(wert === "alle" ? "alle" : Number(wert))}
|
||||
>
|
||||
<SelectTrigger className="w-32" aria-label="Jahr wählen">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="alle">alle Jahre</SelectItem>
|
||||
{jahreAuswahl.map((eintrag) => (
|
||||
<SelectItem key={eintrag} value={String(eintrag)}>
|
||||
{eintrag}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{darfBearbeiten && (
|
||||
<Button onClick={() => setOffen(true)}>
|
||||
<Plus aria-hidden />
|
||||
Übung anlegen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{meldung && <Fehlermeldung meldung={meldung} />}
|
||||
{uebungen.error && <Fehlermeldung meldung={uebungen.error.message} />}
|
||||
|
||||
<div className="rounded-lg border border-[var(--border)]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nummer</TableHead>
|
||||
<TableHead>Thema</TableHead>
|
||||
<TableHead>Art</TableHead>
|
||||
<TableHead>Ort</TableHead>
|
||||
<TableHead>Leitung</TableHead>
|
||||
<TableHead className="text-right">Datum</TableHead>
|
||||
<TableHead className="text-right">Teilnehmer</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{uebungen.isLoading && <LadeZeilen spalten={7} />}
|
||||
|
||||
{eintraege.map((uebung) => (
|
||||
<TableRow key={uebung.id}>
|
||||
<TableCell className="tabellenzahlen font-medium">
|
||||
<Link
|
||||
href={`/uebungen/${uebung.id}`}
|
||||
className="underline-offset-4 hover:underline"
|
||||
>
|
||||
{uebung.nummer}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{uebung.thema}</TableCell>
|
||||
<TableCell>
|
||||
<UebungsartAbzeichen art={uebung.art} />
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{uebung.ort ?? "—"}</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{uebung.leiter ? `${uebung.leiter.nachname}, ${uebung.leiter.vorname}` : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabellenzahlen">
|
||||
{alsDatum(uebung.datum)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabellenzahlen">
|
||||
{uebung._count.teilnahmen}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{!uebungen.isLoading && eintraege.length === 0 && (
|
||||
<div className="p-6">
|
||||
<NichtsGefunden
|
||||
titel="Keine Übung erfasst"
|
||||
hinweis="Thema, Datum und wer dabei war — mehr braucht ein Übungsnachweis nicht."
|
||||
aktion={
|
||||
darfBearbeiten ? (
|
||||
<Button onClick={() => setOffen(true)}>
|
||||
<Plus aria-hidden />
|
||||
Übung anlegen
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={offen} onOpenChange={setOffen}>
|
||||
<DialogContent>
|
||||
<form
|
||||
onSubmit={(ereignis) => {
|
||||
ereignis.preventDefault();
|
||||
setMeldung(null);
|
||||
anlegen.mutate({
|
||||
thema,
|
||||
art,
|
||||
datum: new Date(datum),
|
||||
ort: ort || null,
|
||||
lernziele: lernziele || null,
|
||||
});
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Übung anlegen</DialogTitle>
|
||||
<DialogDescription>
|
||||
Die Teilnehmer werden im nächsten Schritt abgehakt.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="thema">Thema</Label>
|
||||
<Input
|
||||
id="thema"
|
||||
required
|
||||
autoFocus
|
||||
value={thema}
|
||||
onChange={(ereignis) => setThema(ereignis.target.value)}
|
||||
placeholder="Innenangriff mit Atemschutz"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="art">Art</Label>
|
||||
<Select value={art} onValueChange={(wert) => setArt(wert as UebungsArt)}>
|
||||
<SelectTrigger id="art">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(UEBUNGSART_TEXT) as UebungsArt[]).map((wert) => (
|
||||
<SelectItem key={wert} value={wert}>
|
||||
{UEBUNGSART_TEXT[wert]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="datum">Datum</Label>
|
||||
<Input
|
||||
id="datum"
|
||||
type="date"
|
||||
required
|
||||
value={datum}
|
||||
onChange={(ereignis) => setDatum(ereignis.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ort">Ort</Label>
|
||||
<Input
|
||||
id="ort"
|
||||
value={ort}
|
||||
onChange={(ereignis) => setOrt(ereignis.target.value)}
|
||||
placeholder="Gerätehaus, Übungsgelände …"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lernziele">Lernziele</Label>
|
||||
<Textarea
|
||||
id="lernziele"
|
||||
rows={3}
|
||||
value={lernziele}
|
||||
onChange={(ereignis) => setLernziele(ereignis.target.value)}
|
||||
placeholder="Was soll danach sitzen?"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user