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:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user