feat(frontend): Koeniginnenbewertung (Modul 31)

Bewertungsformular (Volksstaerke, Sanftmut, Wabensitz, Schwarm-
trieb, Brutbild, Honigertrag je 1-5) plus Verlauf auf der
Koenigin-Detailseite. Live verifiziert.
This commit is contained in:
Nick Szaunig committed 2026-08-22 23:01:28 +02:00
1 parent 50268fa591
commit fb2b09a929
5 files changed
+197 -1

No files matched your search

@@ -7,6 +7,7 @@ import { QrCodeDialog } from "@/components/qr-code-dialog";
import { QueenEditCard } from "./queen-edit-card";
import { AssignColonyDialog } from "./assign-colony-dialog";
import { QueenHistoryCard } from "./queen-history";
import { QueenEvaluationsCard } from "./queen-evaluations";
export default function QueenDetailPage() {
const params = useParams<{ id: string }>();
@@ -33,6 +34,7 @@ export default function QueenDetailPage() {
</div>
<QueenEditCard key={queen.id} queen={queen} />
<AssignColonyDialog queenId={queen.id} />
<QueenEvaluationsCard queenId={queen.id} />
<QueenHistoryCard queenId={queen.id} />
</main>
);
@@ -0,0 +1,151 @@
"use client";
import { FormEvent, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { addQueenEvaluation, fetchQueenEvaluations } from "@/lib/queens/api";
import { CreateQueenEvaluationInput } from "@/lib/queens/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const CRITERIA: { key: keyof Omit<CreateQueenEvaluationInput, "date" | "colonyId" | "notes">; label: string }[] = [
{ key: "colonyStrength", label: "Volksstärke" },
{ key: "gentleness", label: "Sanftmut" },
{ key: "combBuild", label: "Wabensitz" },
{ key: "swarmingTendency", label: "Schwarmtrieb" },
{ key: "broodPattern", label: "Brutbild" },
{ key: "honeyYield", label: "Honigertrag" },
];
function todayIsoDate(): string {
return new Date().toISOString().slice(0, 10);
}
export function QueenEvaluationsCard({ queenId }: { queenId: string }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(todayIsoDate());
const [scores, setScores] = useState<Record<string, string>>({
colonyStrength: "3",
gentleness: "3",
combBuild: "3",
swarmingTendency: "3",
broodPattern: "3",
honeyYield: "3",
});
const [error, setError] = useState<string | null>(null);
const { data: evaluations, isLoading } = useQuery({
queryKey: ["queens", queenId, "evaluations"],
queryFn: () => fetchQueenEvaluations(queenId),
});
const mutation = useMutation({
mutationFn: (input: CreateQueenEvaluationInput) => addQueenEvaluation(queenId, input),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["queens", queenId, "evaluations"] });
setOpen(false);
setError(null);
},
onError: () => {
setError("Bewertung konnte nicht gespeichert werden.");
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
mutation.mutate({
date: new Date(date).toISOString(),
colonyStrength: Number(scores.colonyStrength),
gentleness: Number(scores.gentleness),
combBuild: Number(scores.combBuild),
swarmingTendency: Number(scores.swarmingTendency),
broodPattern: Number(scores.broodPattern),
honeyYield: Number(scores.honeyYield),
});
}
return (
<Card className="max-w-lg">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Bewertung</CardTitle>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button size="sm">Neue Bewertung</Button>} />
<DialogContent>
<DialogHeader>
<DialogTitle>Königin bewerten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="evaluation-date">Datum</Label>
<Input
id="evaluation-date"
type="date"
required
value={date}
onChange={(event) => setDate(event.target.value)}
/>
</div>
{CRITERIA.map((criterion) => (
<div key={criterion.key} className="flex flex-col gap-2">
<Label htmlFor={`evaluation-${criterion.key}`}>
{criterion.label} (1–5)
</Label>
<Input
id={`evaluation-${criterion.key}`}
type="number"
min={1}
max={5}
required
value={scores[criterion.key]}
onChange={(event) =>
setScores((prev) => ({ ...prev, [criterion.key]: event.target.value }))
}
/>
</div>
))}
{error ? (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
) : null}
<DialogFooter>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Wird gespeichert…" : "Speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent className="flex flex-col gap-3 text-sm">
{isLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
{!isLoading && (!evaluations || evaluations.length === 0) ? (
<p className="text-muted-foreground">Noch keine Bewertung dokumentiert.</p>
) : null}
{evaluations?.map((evaluation) => (
<div key={evaluation.id} className="rounded-md border p-3">
<p className="font-medium">
{new Date(evaluation.date).toLocaleDateString("de-DE")}
</p>
<p className="text-muted-foreground">
Stärke {evaluation.colonyStrength} · Sanftmut {evaluation.gentleness} · Wabensitz{" "}
{evaluation.combBuild} · Schwarmtrieb {evaluation.swarmingTendency} · Brutbild{" "}
{evaluation.broodPattern} · Honigertrag {evaluation.honeyYield}
</p>
</div>
))}
</CardContent>
</Card>
);
}