From d71785d531e87360c48bcfbab17e10c2fcb93f94 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sat, 22 Aug 2026 00:20:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20F=C3=BCtterungen-Modul=20(Mod?= =?UTF-8?q?ul=209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mobile-optimiertes Erfassungsformular unter /feedings/new sowie "Fütterung"-Ansicht mit Mengensumme auf der Volk-Detailseite. --- .../(app)/colonies/[id]/colony-feedings.tsx | 58 +++++ .../src/app/(app)/colonies/[id]/page.tsx | 2 + .../src/app/(app)/feedings/new/page.tsx | 198 ++++++++++++++++++ ApiCore-Frontend/src/lib/feedings/api.ts | 17 ++ ApiCore-Frontend/src/lib/feedings/types.ts | 31 +++ TASKS.md | 2 +- 6 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-feedings.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/feedings/new/page.tsx create mode 100644 ApiCore-Frontend/src/lib/feedings/api.ts create mode 100644 ApiCore-Frontend/src/lib/feedings/types.ts diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-feedings.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-feedings.tsx new file mode 100644 index 0000000..2e64f5b --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-feedings.tsx @@ -0,0 +1,58 @@ +"use client"; + +import Link from "next/link"; +import { useQuery } from "@tanstack/react-query"; +import { fetchColonyFeedings, fetchColonyFeedingsSummary } from "@/lib/feedings/api"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +export function ColonyFeedingsCard({ colonyId }: { colonyId: string }) { + const { data: feedings, isLoading } = useQuery({ + queryKey: ["colonies", colonyId, "feedings"], + queryFn: () => fetchColonyFeedings(colonyId), + }); + const { data: summary } = useQuery({ + queryKey: ["colonies", colonyId, "feedings", "summary"], + queryFn: () => fetchColonyFeedingsSummary(colonyId), + }); + + return ( + + + Fütterung + + + + {summary && summary.length > 0 ? ( +

+ Gesamt:{" "} + {summary + .map((entry) => `${entry.totalAmount.toFixed(1)} ${entry.unit}`) + .join(" · ")} +

+ ) : null} + {isLoading ?

Lädt…

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

Noch keine Fütterung dokumentiert.

+ ) : null} + {feedings?.map((feeding) => ( +
+

+ {new Date(feeding.date).toLocaleDateString("de-DE")} · {feeding.feedType} +

+

+ {feeding.amount} {feeding.unit} + {feeding.product ? ` · ${feeding.product}` : ""} +

+
+ ))} +
+
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx index d3a22d7..79fd3f2 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/page.tsx @@ -9,6 +9,7 @@ import { ColonyHistoryCard } from "./colony-history"; import { ColonyInspectionsCard } from "./colony-inspections"; import { ColonyVarroaCard } from "./colony-varroa"; import { ColonyTreatmentsCard } from "./colony-treatments"; +import { ColonyFeedingsCard } from "./colony-feedings"; export default function ColonyDetailPage() { const params = useParams<{ id: string }>(); @@ -35,6 +36,7 @@ export default function ColonyDetailPage() { + ); diff --git a/ApiCore-Frontend/src/app/(app)/feedings/new/page.tsx b/ApiCore-Frontend/src/app/(app)/feedings/new/page.tsx new file mode 100644 index 0000000..f030dc3 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/feedings/new/page.tsx @@ -0,0 +1,198 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { fetchColonies } from "@/lib/colonies/api"; +import { createFeeding } from "@/lib/feedings/api"; +import { FeedType } from "@/lib/feedings/types"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const FEED_TYPE_OPTIONS: FeedType[] = [ + "SUGAR_WATER", + "FEED_SYRUP", + "FEED_PASTE", + "OWN_HONEY", + "OTHER", +]; + +function todayIsoDate(): string { + return new Date().toISOString().slice(0, 10); +} + +export default function NewFeedingPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const queryClient = useQueryClient(); + + const { data: colonies } = useQuery({ + queryKey: ["colonies"], + queryFn: fetchColonies, + }); + const activeColonies = colonies?.filter((colony) => colony.status === "ACTIVE") ?? []; + + const [colonyId, setColonyId] = useState(searchParams.get("colonyId") ?? ""); + const [date, setDate] = useState(todayIsoDate()); + const [feedType, setFeedType] = useState("SUGAR_WATER"); + const [amount, setAmount] = useState(""); + const [unit, setUnit] = useState("l"); + const [product, setProduct] = useState(""); + const [notes, setNotes] = useState(""); + const [error, setError] = useState(null); + + const mutation = useMutation({ + mutationFn: createFeeding, + onSuccess: (feeding) => { + void queryClient.invalidateQueries({ + queryKey: ["colonies", feeding.colonyId, "feedings"], + }); + void queryClient.invalidateQueries({ + queryKey: ["colonies", feeding.colonyId, "feedings", "summary"], + }); + router.push(`/colonies/${feeding.colonyId}`); + }, + onError: () => { + setError("Fütterung konnte nicht gespeichert werden."); + }, + }); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + setError(null); + if (!colonyId) { + setError("Bitte ein Volk auswählen."); + return; + } + if (!amount) { + setError("Bitte eine Menge angeben."); + return; + } + mutation.mutate({ + colonyId, + date: new Date(date).toISOString(), + feedType, + amount: Number(amount), + unit, + product: product || undefined, + notes: notes || undefined, + }); + } + + return ( +
+ + + Neue Fütterung + + +
+
+ + +
+
+ + setDate(event.target.value)} + /> +
+
+ + +
+
+
+ + setAmount(event.target.value)} + /> +
+
+ + setUnit(event.target.value)} + /> +
+
+
+ + setProduct(event.target.value)} + /> +
+
+ +