feat(frontend): Statistik-Seite mit animierten Auswertungen (Modul 27)
Tab-basierte /statistics-Seite (Voelker, Koeniginnen, Bienenstaende, Varroa, Honig, Futter) mit animierten Balkendiagrammen, Sidebar-Eintrag und Live-Verifikation ueber alle Tabs sowie Mobile-Breakpoint.
This commit is contained in:
1 parent
e8177432b0
commit
8856d7d0f5
6 files changed
+449
-2
No files matched your search
@@ -3,6 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
|
BarChart3,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
Crown,
|
Crown,
|
||||||
Droplet,
|
Droplet,
|
||||||
@@ -68,6 +69,10 @@ const NAV_GROUPS: NavGroup[] = [
|
|||||||
{ label: "Inventar", href: "/equipment", icon: Wrench },
|
{ label: "Inventar", href: "/equipment", icon: Wrench },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Auswertung",
|
||||||
|
items: [{ label: "Statistik", href: "/statistics", icon: BarChart3 }],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ADMIN_GROUP: NavGroup = {
|
const ADMIN_GROUP: NavGroup = {
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchApiaries } from "@/lib/apiaries/api";
|
||||||
|
import { fetchColonies } from "@/lib/colonies/api";
|
||||||
|
import {
|
||||||
|
fetchApiaryStats,
|
||||||
|
fetchColonyStats,
|
||||||
|
fetchFeedingStats,
|
||||||
|
fetchHoneyStats,
|
||||||
|
fetchQueenStats,
|
||||||
|
fetchVarroaStats,
|
||||||
|
} from "@/lib/statistics/api";
|
||||||
|
import { StatBarList } from "@/components/stat-bar-list";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
|
const COLONY_STATUS_LABELS: Record<string, string> = {
|
||||||
|
ACTIVE: "Aktiv",
|
||||||
|
WEAK: "Schwach",
|
||||||
|
CONSPICUOUS: "Auffällig",
|
||||||
|
QUEENLESS: "Weisellos",
|
||||||
|
DISSOLVED: "Aufgelöst",
|
||||||
|
SOLD: "Verkauft",
|
||||||
|
DECEASED: "Verstorben",
|
||||||
|
MERGED: "Vereinigt",
|
||||||
|
};
|
||||||
|
|
||||||
|
function kg(value: number): string {
|
||||||
|
return value.toLocaleString("de-DE", { maximumFractionDigits: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortedRecordEntries(record: Record<string, number>): [string, number][] {
|
||||||
|
return Object.entries(record).sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
function KpiTile({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<Card size="sm">
|
||||||
|
<CardContent className="flex flex-col gap-1">
|
||||||
|
<p className="text-2xl font-semibold tabular-nums">{value}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColonyStatsPanel() {
|
||||||
|
const { data, isLoading } = useQuery({ queryKey: ["statistics", "colonies"], queryFn: fetchColonyStats });
|
||||||
|
|
||||||
|
if (isLoading || !data) return <p className="text-sm text-muted-foreground">Lädt…</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Völker nach Status</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={data.byStatus.map((row) => ({
|
||||||
|
label: COLONY_STATUS_LABELS[row.status] ?? row.status,
|
||||||
|
value: row.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Völkerverluste pro Jahr</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={sortedRecordEntries(data.lossesByYear).map(([year, count]) => ({
|
||||||
|
label: year,
|
||||||
|
value: count,
|
||||||
|
}))}
|
||||||
|
emptyText="Keine Verluste erfasst."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueenStatsPanel() {
|
||||||
|
const { data, isLoading } = useQuery({ queryKey: ["statistics", "queens"], queryFn: fetchQueenStats });
|
||||||
|
|
||||||
|
if (isLoading || !data) return <p className="text-sm text-muted-foreground">Lädt…</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<KpiTile label="Aktive Königinnen" value={String(data.totalActive)} />
|
||||||
|
<KpiTile
|
||||||
|
label="Ø Lebensdauer (Tage)"
|
||||||
|
value={data.averageLifespanDays !== null ? Math.round(data.averageLifespanDays).toString() : "–"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Honigertrag pro Königin (Ø-Bewertung)</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={data.evaluationAverages
|
||||||
|
.slice(0, 10)
|
||||||
|
.map((row) => ({ label: row.code, value: row.honeyYield }))}
|
||||||
|
emptyText="Noch keine Königinnenbewertungen erfasst."
|
||||||
|
formatValue={(value) => value.toFixed(1)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApiaryStatsPanel() {
|
||||||
|
const { data, isLoading } = useQuery({ queryKey: ["statistics", "apiaries"], queryFn: fetchApiaryStats });
|
||||||
|
|
||||||
|
if (isLoading || !data) return <p className="text-sm text-muted-foreground">Lädt…</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Honigertrag pro Bienenstand</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={data
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.totalHoneyKg - a.totalHoneyKg)
|
||||||
|
.map((row) => ({ label: row.name, value: row.totalHoneyKg, unit: "kg" }))}
|
||||||
|
formatValue={kg}
|
||||||
|
emptyText="Noch keine Ernten erfasst."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VarroaStatsPanel({ colonyLabels }: { colonyLabels: Map<string, string> }) {
|
||||||
|
const { data, isLoading } = useQuery({ queryKey: ["statistics", "varroa"], queryFn: fetchVarroaStats });
|
||||||
|
|
||||||
|
if (isLoading || !data) return <p className="text-sm text-muted-foreground">Lädt…</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Behandlungen pro Volk</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={data.treatmentCountByColony
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.treatmentCount - a.treatmentCount)
|
||||||
|
.slice(0, 10)
|
||||||
|
.map((row) => ({
|
||||||
|
label: colonyLabels.get(row.colonyId) ?? row.colonyId,
|
||||||
|
value: row.treatmentCount,
|
||||||
|
}))}
|
||||||
|
emptyText="Noch keine Behandlungen erfasst."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Letzte Milbenmessungen</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={data.measurements
|
||||||
|
.slice(-10)
|
||||||
|
.reverse()
|
||||||
|
.map((row) => ({
|
||||||
|
label: colonyLabels.get(row.colonyId) ?? row.colonyId,
|
||||||
|
value: row.miteCount,
|
||||||
|
unit: "Milben",
|
||||||
|
}))}
|
||||||
|
emptyText="Noch keine Varroamessungen erfasst."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HoneyStatsPanel({ apiaryLabels }: { apiaryLabels: Map<string, string> }) {
|
||||||
|
const { data, isLoading } = useQuery({ queryKey: ["statistics", "honey"], queryFn: fetchHoneyStats });
|
||||||
|
|
||||||
|
if (isLoading || !data) return <p className="text-sm text-muted-foreground">Lädt…</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<KpiTile label="Erntemenge gesamt" value={`${kg(data.totalHarvestKg)} kg`} />
|
||||||
|
<KpiTile label="Honigchargen gesamt" value={`${kg(data.totalHoneyBatchKg)} kg`} />
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Ertrag pro Jahr</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={sortedRecordEntries(data.yieldPerYear).map(([year, value]) => ({
|
||||||
|
label: year,
|
||||||
|
value,
|
||||||
|
unit: "kg",
|
||||||
|
}))}
|
||||||
|
formatValue={kg}
|
||||||
|
emptyText="Noch keine Ernten erfasst."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Ertrag pro Bienenstand</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={Object.entries(data.yieldPerApiary)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.map(([apiaryId, value]) => ({
|
||||||
|
label: apiaryLabels.get(apiaryId) ?? apiaryId,
|
||||||
|
value,
|
||||||
|
unit: "kg",
|
||||||
|
}))}
|
||||||
|
formatValue={kg}
|
||||||
|
emptyText="Noch keine Ernten erfasst."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeedingStatsPanel({ colonyLabels }: { colonyLabels: Map<string, string> }) {
|
||||||
|
const { data, isLoading } = useQuery({ queryKey: ["statistics", "feeding"], queryFn: fetchFeedingStats });
|
||||||
|
|
||||||
|
if (isLoading || !data) return <p className="text-sm text-muted-foreground">Lädt…</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Futterverbrauch pro Volk</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={Object.entries(data.byColony)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([colonyId, value]) => ({
|
||||||
|
label: colonyLabels.get(colonyId) ?? colonyId,
|
||||||
|
value,
|
||||||
|
}))}
|
||||||
|
emptyText="Noch keine Fütterungen erfasst."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Futterverbrauch pro Saison</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<StatBarList
|
||||||
|
items={sortedRecordEntries(data.bySeason).map(([year, value]) => ({
|
||||||
|
label: year,
|
||||||
|
value,
|
||||||
|
}))}
|
||||||
|
emptyText="Noch keine Fütterungen erfasst."
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatisticsPage() {
|
||||||
|
const { data: colonies } = useQuery({ queryKey: ["colonies"], queryFn: () => fetchColonies() });
|
||||||
|
const { data: apiaries } = useQuery({ queryKey: ["apiaries"], queryFn: () => fetchApiaries() });
|
||||||
|
|
||||||
|
const colonyLabels = new Map((colonies ?? []).map((colony) => [colony.id, colony.code]));
|
||||||
|
const apiaryLabels = new Map((apiaries ?? []).map((apiary) => [apiary.id, apiary.name]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col gap-6 p-4 sm:p-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Statistik</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">Auswertungen über deine Imkerei</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="colonies">
|
||||||
|
<TabsList className="w-full sm:w-fit">
|
||||||
|
<TabsTrigger value="colonies">Völker</TabsTrigger>
|
||||||
|
<TabsTrigger value="queens">Königinnen</TabsTrigger>
|
||||||
|
<TabsTrigger value="apiaries">Bienenstände</TabsTrigger>
|
||||||
|
<TabsTrigger value="varroa">Varroa</TabsTrigger>
|
||||||
|
<TabsTrigger value="honey">Honig</TabsTrigger>
|
||||||
|
<TabsTrigger value="feeding">Futter</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="colonies">
|
||||||
|
<ColonyStatsPanel />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="queens">
|
||||||
|
<QueenStatsPanel />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="apiaries">
|
||||||
|
<ApiaryStatsPanel />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="varroa">
|
||||||
|
<VarroaStatsPanel colonyLabels={colonyLabels} />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="honey">
|
||||||
|
<HoneyStatsPanel apiaryLabels={apiaryLabels} />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="feeding">
|
||||||
|
<FeedingStatsPanel colonyLabels={colonyLabels} />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
interface StatBarItem {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatBarListProps {
|
||||||
|
items: StatBarItem[];
|
||||||
|
emptyText?: string;
|
||||||
|
formatValue?: (value: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatBarList({ items, emptyText = "Keine Daten vorhanden.", formatValue }: StatBarListProps) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return <p className="text-sm text-muted-foreground">{emptyText}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const max = Math.max(...items.map((item) => item.value), 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.label} className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between gap-3 text-sm">
|
||||||
|
<span className="truncate font-medium">{item.label}</span>
|
||||||
|
<span className="shrink-0 tabular-nums text-muted-foreground">
|
||||||
|
{formatValue ? formatValue(item.value) : item.value}
|
||||||
|
{item.unit ? ` ${item.unit}` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-primary motion-safe:transition-[width] motion-safe:duration-700 motion-safe:ease-out"
|
||||||
|
style={{ width: `${item.value > 0 ? Math.max((item.value / max) * 100, 4) : 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
|
import { ApiaryStats, ColonyStats, FeedingStats, HoneyStats, QueenStats, VarroaStats } from "./types";
|
||||||
|
|
||||||
|
export function fetchColonyStats(): Promise<ColonyStats> {
|
||||||
|
return apiFetch<ColonyStats>("/statistics/colonies");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchQueenStats(): Promise<QueenStats> {
|
||||||
|
return apiFetch<QueenStats>("/statistics/queens");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchApiaryStats(): Promise<ApiaryStats> {
|
||||||
|
return apiFetch<ApiaryStats>("/statistics/apiaries");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchVarroaStats(): Promise<VarroaStats> {
|
||||||
|
return apiFetch<VarroaStats>("/statistics/varroa");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchHoneyStats(): Promise<HoneyStats> {
|
||||||
|
return apiFetch<HoneyStats>("/statistics/honey");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchFeedingStats(): Promise<FeedingStats> {
|
||||||
|
return apiFetch<FeedingStats>("/statistics/feeding");
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
export interface ColonyStats {
|
||||||
|
byStatus: { status: string; count: number }[];
|
||||||
|
strengthOverTime: { colonyId: string; date: string; colonyStrength: number }[];
|
||||||
|
lossesByYear: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueenEvaluationAverage {
|
||||||
|
queenId: string;
|
||||||
|
code: string;
|
||||||
|
evaluationCount: number;
|
||||||
|
colonyStrength: number;
|
||||||
|
gentleness: number;
|
||||||
|
combBuild: number;
|
||||||
|
swarmingTendency: number;
|
||||||
|
broodPattern: number;
|
||||||
|
honeyYield: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueenStats {
|
||||||
|
totalActive: number;
|
||||||
|
averageLifespanDays: number | null;
|
||||||
|
evaluationAverages: QueenEvaluationAverage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiaryStatsEntry {
|
||||||
|
apiaryId: string;
|
||||||
|
name: string;
|
||||||
|
activeColonies: number;
|
||||||
|
lostColonies: number;
|
||||||
|
totalHoneyKg: number;
|
||||||
|
averageHoneyKgPerColony: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiaryStats = ApiaryStatsEntry[];
|
||||||
|
|
||||||
|
export interface VarroaStats {
|
||||||
|
measurements: { colonyId: string; date: string; miteCount: number; method: string }[];
|
||||||
|
treatmentCountByColony: { colonyId: string; treatmentCount: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HoneyStats {
|
||||||
|
totalHarvestKg: number;
|
||||||
|
totalHoneyBatchKg: number;
|
||||||
|
yieldPerYear: Record<string, number>;
|
||||||
|
yieldPerApiary: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeedingStats {
|
||||||
|
byColony: Record<string, number>;
|
||||||
|
bySeason: Record<string, number>;
|
||||||
|
}
|
||||||
@@ -219,8 +219,8 @@ Reihenfolge = Bau-Reihenfolge. Gleiche Haken-Regel wie MVP: erst setzen, wenn Li
|
|||||||
- Backend: `StatisticsModule` – Auswertungen gemäß Abschnitt 4.26 (Volksstärke-Entwicklung, Völkerverluste, Königinnen-Ertrag/Lebensdauer, Standort-Ertrag, Varroa-Verlauf, Honigertrag pro Volk/Stand/Jahr, Futterverbrauch)
|
- Backend: `StatisticsModule` – Auswertungen gemäß Abschnitt 4.26 (Volksstärke-Entwicklung, Völkerverluste, Königinnen-Ertrag/Lebensdauer, Standort-Ertrag, Varroa-Verlauf, Honigertrag pro Volk/Stand/Jahr, Futterverbrauch)
|
||||||
- Endpunkte: `GET /api/v1/statistics/{colonies|queens|apiaries|varroa|honey|feeding}`
|
- Endpunkte: `GET /api/v1/statistics/{colonies|queens|apiaries|varroa|honey|feeding}`
|
||||||
- Frontend: `/statistics` mit Diagrammen
|
- Frontend: `/statistics` mit Diagrammen
|
||||||
- [ ] 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
|
||||||
|
|
||||||
### 28. Webhooks
|
### 28. Webhooks
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user