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 { QueenEditCard } from "./queen-edit-card";
|
||||||
import { AssignColonyDialog } from "./assign-colony-dialog";
|
import { AssignColonyDialog } from "./assign-colony-dialog";
|
||||||
import { QueenHistoryCard } from "./queen-history";
|
import { QueenHistoryCard } from "./queen-history";
|
||||||
|
import { QueenEvaluationsCard } from "./queen-evaluations";
|
||||||
|
|
||||||
export default function QueenDetailPage() {
|
export default function QueenDetailPage() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
@@ -33,6 +34,7 @@ export default function QueenDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<QueenEditCard key={queen.id} queen={queen} />
|
<QueenEditCard key={queen.id} queen={queen} />
|
||||||
<AssignColonyDialog queenId={queen.id} />
|
<AssignColonyDialog queenId={queen.id} />
|
||||||
|
<QueenEvaluationsCard queenId={queen.id} />
|
||||||
<QueenHistoryCard queenId={queen.id} />
|
<QueenHistoryCard queenId={queen.id} />
|
||||||
</main>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { apiFetch } from "@/lib/auth/api-client";
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
import {
|
import {
|
||||||
AssignColonyInput,
|
AssignColonyInput,
|
||||||
|
CreateQueenEvaluationInput,
|
||||||
CreateQueenInput,
|
CreateQueenInput,
|
||||||
Queen,
|
Queen,
|
||||||
QueenColonyHistoryEntry,
|
QueenColonyHistoryEntry,
|
||||||
|
QueenEvaluation,
|
||||||
QueenStatus,
|
QueenStatus,
|
||||||
UpdateQueenInput,
|
UpdateQueenInput,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -47,3 +49,17 @@ export function assignQueenColony(id: string, input: AssignColonyInput): Promise
|
|||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchQueenEvaluations(id: string): Promise<QueenEvaluation[]> {
|
||||||
|
return apiFetch<QueenEvaluation[]>(`/queens/${id}/evaluations`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addQueenEvaluation(
|
||||||
|
id: string,
|
||||||
|
input: CreateQueenEvaluationInput,
|
||||||
|
): Promise<QueenEvaluation> {
|
||||||
|
return apiFetch<QueenEvaluation>(`/queens/${id}/evaluations`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -55,3 +55,30 @@ export interface QueenColonyHistoryEntry {
|
|||||||
endDate: string | null;
|
endDate: string | null;
|
||||||
notes: 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;
|
||||||
|
}
|
||||||
@@ -252,7 +252,7 @@ Reihenfolge = Bau-Reihenfolge. Gleiche Haken-Regel wie MVP: erst setzen, wenn Li
|
|||||||
- Endpunkte: `GET/POST /api/v1/queens/:id/evaluations`
|
- Endpunkte: `GET/POST /api/v1/queens/:id/evaluations`
|
||||||
- Frontend: Bewertungsformular + Verlauf auf der Königin-Detailseite
|
- Frontend: Bewertungsformular + Verlauf auf der Königin-Detailseite
|
||||||
- [x] Backend-Modul fertig, getestet, grün, committet
|
- [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
|
### 32. Lagerbehälter-Umlagerung
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user