feat(frontend): Varroamessungen-Modul (Modul 7)
Mobile-optimiertes Erfassungsformular unter /varroa/new sowie "Varroa"-Ansicht mit Verlaufsgrafik auf der Volk-Detailseite.
This commit is contained in:
1 parent
f363bffed1
commit
66c14cf306
6 files changed
+323
-1
No files matched your search
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchColonyVarroaMeasurements } from "@/lib/varroa/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
const CHART_WIDTH = 280;
|
||||
const CHART_HEIGHT = 80;
|
||||
const CHART_PADDING = 8;
|
||||
|
||||
function VarroaTrendChart({ values }: { values: number[] }) {
|
||||
if (values.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const max = Math.max(...values, 1);
|
||||
const min = Math.min(...values, 0);
|
||||
const range = max - min || 1;
|
||||
const usableWidth = CHART_WIDTH - CHART_PADDING * 2;
|
||||
const usableHeight = CHART_HEIGHT - CHART_PADDING * 2;
|
||||
const points = values
|
||||
.map((value, index) => {
|
||||
const x = CHART_PADDING + (index / (values.length - 1)) * usableWidth;
|
||||
const y = CHART_PADDING + usableHeight - ((value - min) / range) * usableHeight;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`}
|
||||
className="w-full text-primary"
|
||||
role="img"
|
||||
aria-label="Verlauf der Varroamessungen"
|
||||
>
|
||||
<polyline points={points} fill="none" stroke="currentColor" strokeWidth={2} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColonyVarroaCard({ colonyId }: { colonyId: string }) {
|
||||
const { data: measurements, isLoading } = useQuery({
|
||||
queryKey: ["colonies", colonyId, "varroa-measurements"],
|
||||
queryFn: () => fetchColonyVarroaMeasurements(colonyId),
|
||||
});
|
||||
|
||||
const values = (measurements ?? [])
|
||||
.map((measurement) => measurement.miteCount ?? measurement.calculatedValue)
|
||||
.filter((value): value is number => value !== null);
|
||||
|
||||
return (
|
||||
<Card className="max-w-lg">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Varroa</CardTitle>
|
||||
<Button
|
||||
size="sm"
|
||||
nativeButton={false}
|
||||
render={<Link href={`/varroa/new?colonyId=${colonyId}`} />}
|
||||
>
|
||||
Neue Messung
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3 text-sm">
|
||||
{isLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
|
||||
{!isLoading && (!measurements || measurements.length === 0) ? (
|
||||
<p className="text-muted-foreground">Noch keine Varroamessung dokumentiert.</p>
|
||||
) : null}
|
||||
<VarroaTrendChart values={values} />
|
||||
{measurements?.map((measurement) => (
|
||||
<div key={measurement.id} className="rounded-md border p-3">
|
||||
<p className="font-medium">
|
||||
{new Date(measurement.date).toLocaleDateString("de-DE")} · {measurement.method}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{measurement.miteCount !== null ? `${measurement.miteCount} Milben` : "—"}
|
||||
{measurement.calculatedValue !== null
|
||||
? ` · ${measurement.calculatedValue.toFixed(1)} Milben/Tag`
|
||||
: ""}
|
||||
</p>
|
||||
{measurement.assessment ? <p>Einschätzung: {measurement.assessment}</p> : null}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { ColonyEditCard } from "./colony-edit-card";
|
||||
import { ColonyActions } from "./colony-actions";
|
||||
import { ColonyHistoryCard } from "./colony-history";
|
||||
import { ColonyInspectionsCard } from "./colony-inspections";
|
||||
import { ColonyVarroaCard } from "./colony-varroa";
|
||||
|
||||
export default function ColonyDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
@@ -31,6 +32,7 @@ export default function ColonyDetailPage() {
|
||||
<ColonyEditCard key={colony.id} colony={colony} />
|
||||
<ColonyActions colony={colony} />
|
||||
<ColonyInspectionsCard colonyId={colony.id} />
|
||||
<ColonyVarroaCard colonyId={colony.id} />
|
||||
<ColonyHistoryCard colonyId={colony.id} />
|
||||
</main>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user