From fb2b09a929b413f2fbe859c5f7d0781f0b0a69eb Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sat, 22 Aug 2026 23:01:28 +0200 Subject: [PATCH] feat(frontend): Koeniginnenbewertung (Modul 31) Bewertungsformular (Volksstaerke, Sanftmut, Wabensitz, Schwarm- trieb, Brutbild, Honigertrag je 1-5) plus Verlauf auf der Koenigin-Detailseite. Live verifiziert. --- .../src/app/(app)/queens/[id]/page.tsx | 2 + .../(app)/queens/[id]/queen-evaluations.tsx | 151 ++++++++++++++++++ ApiCore-Frontend/src/lib/queens/api.ts | 16 ++ ApiCore-Frontend/src/lib/queens/types.ts | 27 ++++ TASKS.md | 2 +- 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 ApiCore-Frontend/src/app/(app)/queens/[id]/queen-evaluations.tsx diff --git a/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx index 058837a..3a226c4 100644 --- a/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/queens/[id]/page.tsx @@ -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() { + ); diff --git a/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-evaluations.tsx b/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-evaluations.tsx new file mode 100644 index 0000000..dc64618 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/queens/[id]/queen-evaluations.tsx @@ -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; 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>({ + colonyStrength: "3", + gentleness: "3", + combBuild: "3", + swarmingTendency: "3", + broodPattern: "3", + honeyYield: "3", + }); + const [error, setError] = useState(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) { + 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 ( + + + Bewertung + + Neue Bewertung} /> + + + Königin bewerten + +
+
+ + setDate(event.target.value)} + /> +
+ {CRITERIA.map((criterion) => ( +
+ + + setScores((prev) => ({ ...prev, [criterion.key]: event.target.value })) + } + /> +
+ ))} + {error ? ( +

+ {error} +

+ ) : null} + + + +
+
+
+
+ + {isLoading ?

Lädt…

: null} + {!isLoading && (!evaluations || evaluations.length === 0) ? ( +

Noch keine Bewertung dokumentiert.

+ ) : null} + {evaluations?.map((evaluation) => ( +
+

+ {new Date(evaluation.date).toLocaleDateString("de-DE")} +

+

+ Stärke {evaluation.colonyStrength} · Sanftmut {evaluation.gentleness} · Wabensitz{" "} + {evaluation.combBuild} · Schwarmtrieb {evaluation.swarmingTendency} · Brutbild{" "} + {evaluation.broodPattern} · Honigertrag {evaluation.honeyYield} +

+
+ ))} +
+
+ ); +} diff --git a/ApiCore-Frontend/src/lib/queens/api.ts b/ApiCore-Frontend/src/lib/queens/api.ts index 658aad4..61aea59 100644 --- a/ApiCore-Frontend/src/lib/queens/api.ts +++ b/ApiCore-Frontend/src/lib/queens/api.ts @@ -1,9 +1,11 @@ import { apiFetch } from "@/lib/auth/api-client"; import { AssignColonyInput, + CreateQueenEvaluationInput, CreateQueenInput, Queen, QueenColonyHistoryEntry, + QueenEvaluation, QueenStatus, UpdateQueenInput, } from "./types"; @@ -47,3 +49,17 @@ export function assignQueenColony(id: string, input: AssignColonyInput): Promise body: JSON.stringify(input), }); } + +export function fetchQueenEvaluations(id: string): Promise { + return apiFetch(`/queens/${id}/evaluations`); +} + +export function addQueenEvaluation( + id: string, + input: CreateQueenEvaluationInput, +): Promise { + return apiFetch(`/queens/${id}/evaluations`, { + method: "POST", + body: JSON.stringify(input), + }); +} diff --git a/ApiCore-Frontend/src/lib/queens/types.ts b/ApiCore-Frontend/src/lib/queens/types.ts index f079578..9d863fd 100644 --- a/ApiCore-Frontend/src/lib/queens/types.ts +++ b/ApiCore-Frontend/src/lib/queens/types.ts @@ -55,3 +55,30 @@ export interface QueenColonyHistoryEntry { endDate: string | null; notes: string | null; } + +export interface QueenEvaluation { + id: string; + queenId: string; + colonyId: string | null; + date: string; + colonyStrength: number; + gentleness: number; + combBuild: number; + swarmingTendency: number; + broodPattern: number; + honeyYield: number; + notes: string | null; + createdAt: string; +} + +export interface CreateQueenEvaluationInput { + colonyId?: string; + date: string; + colonyStrength: number; + gentleness: number; + combBuild: number; + swarmingTendency: number; + broodPattern: number; + honeyYield: number; + notes?: string; +} diff --git a/TASKS.md b/TASKS.md index 6e8e5d4..86cfebe 100644 --- a/TASKS.md +++ b/TASKS.md @@ -252,7 +252,7 @@ Reihenfolge = Bau-Reihenfolge. Gleiche Haken-Regel wie MVP: erst setzen, wenn Li - Endpunkte: `GET/POST /api/v1/queens/:id/evaluations` - Frontend: Bewertungsformular + Verlauf auf der Königin-Detailseite - [x] Backend-Modul fertig, getestet, grün, committet -- [ ] Frontend-Views fertig, getestet, grün, committet +- [x] Frontend-Views fertig, getestet, grün, committet ### 32. Lagerbehälter-Umlagerung