From f3d99524ff3c498b3e9ac929976e711ca1152539 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sat, 22 Aug 2026 04:58:47 +0200 Subject: [PATCH] feat(frontend): Honigchargen-Ansichten (Modul 11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chargenliste mit Anlage-Dialog sowie Detailseite mit Honigmessungen und Rückverfolgbarkeits-Ansicht bis zu den Ursprungsvölkern. --- .../[id]/add-measurement-dialog.tsx | 110 ++++++++++++++ .../src/app/(app)/honey-batches/[id]/page.tsx | 97 ++++++++++++ .../create-honey-batch-dialog.tsx | 138 ++++++++++++++++++ .../src/app/(app)/honey-batches/page.tsx | 69 +++++++++ ApiCore-Frontend/src/app/(app)/layout.tsx | 6 + ApiCore-Frontend/src/lib/honey-batches/api.ts | 36 +++++ .../src/lib/honey-batches/types.ts | 68 +++++++++ TASKS.md | 2 +- 8 files changed, 525 insertions(+), 1 deletion(-) create mode 100644 ApiCore-Frontend/src/app/(app)/honey-batches/[id]/add-measurement-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/honey-batches/create-honey-batch-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx create mode 100644 ApiCore-Frontend/src/lib/honey-batches/api.ts create mode 100644 ApiCore-Frontend/src/lib/honey-batches/types.ts diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/add-measurement-dialog.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/add-measurement-dialog.tsx new file mode 100644 index 0000000..10d6d3c --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/add-measurement-dialog.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { addHoneyMeasurement } from "@/lib/honey-batches/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +function todayIsoDate(): string { + return new Date().toISOString().slice(0, 10); +} + +export function AddMeasurementDialog({ honeyBatchId }: { honeyBatchId: string }) { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [waterContent, setWaterContent] = useState(""); + const [measuredAt, setMeasuredAt] = useState(todayIsoDate()); + const [device, setDevice] = useState(""); + const [error, setError] = useState(null); + + const mutation = useMutation({ + mutationFn: (input: { waterContent: number; measuredAt: string; device?: string }) => + addHoneyMeasurement(honeyBatchId, input), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ["honey-batches", honeyBatchId] }); + setOpen(false); + setWaterContent(""); + setDevice(""); + setError(null); + }, + onError: () => { + setError("Messung konnte nicht gespeichert werden."); + }, + }); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + setError(null); + if (!waterContent) { + setError("Bitte einen Wassergehalt angeben."); + return; + } + mutation.mutate({ + waterContent: Number(waterContent), + measuredAt: new Date(measuredAt).toISOString(), + device: device || undefined, + }); + } + + return ( + + Neue Messung} /> + + + Honigmessung + +
+
+ + setWaterContent(event.target.value)} + /> +
+
+ + setMeasuredAt(event.target.value)} + /> +
+
+ + setDevice(event.target.value)} + /> +
+ {error ? ( +

+ {error} +

+ ) : null} + + + +
+
+
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx new file mode 100644 index 0000000..134935b --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { AddMeasurementDialog } from "./add-measurement-dialog"; + +export default function HoneyBatchDetailPage() { + const params = useParams<{ id: string }>(); + const { data: honeyBatch, isLoading, isError } = useQuery({ + queryKey: ["honey-batches", params.id], + queryFn: () => fetchHoneyBatch(params.id), + }); + const { data: traceability } = useQuery({ + queryKey: ["honey-batches", params.id, "traceability"], + queryFn: () => fetchHoneyBatchTraceability(params.id), + }); + + if (isLoading) { + return

Lädt…

; + } + if (isError || !honeyBatch) { + return ( +

+ Honigcharge konnte nicht geladen werden. +

+ ); + } + + return ( +
+ + + {honeyBatch.batchNumber} + {honeyBatch.status} + + + {honeyBatch.honeyType ?

Sorte: {honeyBatch.honeyType}

: null} + {honeyBatch.amount !== null ?

Menge: {honeyBatch.amount} kg

: null} +
+
+ + + + Honigmessungen + + + + {honeyBatch.measurements.length === 0 ? ( +

Noch keine Messung dokumentiert.

+ ) : null} + {honeyBatch.measurements.map((measurement) => ( +
+

+ {new Date(measurement.measuredAt).toLocaleDateString("de-DE")} ·{" "} + {measurement.waterContent}% Wassergehalt +

+ {measurement.device ?

{measurement.device}

: null} +
+ ))} +
+
+ + + + Rückverfolgbarkeit + + + {!traceability ?

Lädt…

: null} + {traceability ? ( + <> +

+ Schleuderung am{" "} + {new Date(traceability.extraction.date).toLocaleDateString("de-DE")} + {traceability.extraction.room ? ` · ${traceability.extraction.room}` : ""} +

+
+ {traceability.harvests.map((harvest) => ( +
+

+ Ernte am {new Date(harvest.date).toLocaleDateString("de-DE")} +

+

+ Völker: {harvest.colonies.map((colony) => colony.code).join(", ")} +

+
+ ))} +
+ + ) : null} +
+
+
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/create-honey-batch-dialog.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/create-honey-batch-dialog.tsx new file mode 100644 index 0000000..454d0d9 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/create-honey-batch-dialog.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { fetchExtractions } from "@/lib/extractions/api"; +import { createHoneyBatch } from "@/lib/honey-batches/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +export function CreateHoneyBatchDialog() { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [batchNumber, setBatchNumber] = useState(""); + const [extractionId, setExtractionId] = useState(""); + const [honeyType, setHoneyType] = useState(""); + const [amount, setAmount] = useState(""); + const [error, setError] = useState(null); + + const { data: extractions } = useQuery({ + queryKey: ["extractions"], + queryFn: fetchExtractions, + }); + + const mutation = useMutation({ + mutationFn: createHoneyBatch, + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ["honey-batches"] }); + setOpen(false); + setBatchNumber(""); + setExtractionId(""); + setHoneyType(""); + setAmount(""); + setError(null); + }, + onError: () => { + setError("Honigcharge konnte nicht angelegt werden."); + }, + }); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + setError(null); + if (!extractionId) { + setError("Bitte eine Schleuderung auswählen."); + return; + } + mutation.mutate({ + batchNumber, + extractionId, + honeyType: honeyType || undefined, + amount: amount ? Number(amount) : undefined, + }); + } + + return ( + + + Neue Charge} /> + + + Neue Honigcharge + +
+
+ + setBatchNumber(event.target.value)} + /> +
+
+ + +
+
+ + setHoneyType(event.target.value)} + /> +
+
+ + setAmount(event.target.value)} + /> +
+ {error ? ( +

+ {error} +

+ ) : null} + + + +
+
+
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx new file mode 100644 index 0000000..63d445b --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/page.tsx @@ -0,0 +1,69 @@ +"use client"; + +import Link from "next/link"; +import { useQuery } from "@tanstack/react-query"; +import { fetchHoneyBatches } from "@/lib/honey-batches/api"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { CreateHoneyBatchDialog } from "./create-honey-batch-dialog"; + +export default function HoneyBatchesPage() { + const { data: honeyBatches, isLoading, isError } = useQuery({ + queryKey: ["honey-batches"], + queryFn: fetchHoneyBatches, + }); + + return ( +
+
+

Honigchargen

+ +
+ + {isLoading ?

Lädt…

: null} + {isError ? ( +

+ Honigchargen konnten nicht geladen werden. +

+ ) : null} + + {honeyBatches ? ( + + + + Chargennummer + Sorte + Menge + Status + + + + {honeyBatches.map((honeyBatch) => ( + + + + {honeyBatch.batchNumber} + + + {honeyBatch.honeyType ?? "—"} + + {honeyBatch.amount !== null ? `${honeyBatch.amount} kg` : "—"} + + + {honeyBatch.status} + + + ))} + +
+ ) : null} +
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx index b765a3a..be666f0 100644 --- a/ApiCore-Frontend/src/app/(app)/layout.tsx +++ b/ApiCore-Frontend/src/app/(app)/layout.tsx @@ -37,6 +37,12 @@ export default function AppLayout({ children }: { children: ReactNode }) { Aufgaben + + Honigchargen + {user.role === "ADMIN" ? ( { + return apiFetch("/honey-batches"); +} + +export function fetchHoneyBatch(id: string): Promise { + return apiFetch(`/honey-batches/${id}`); +} + +export function fetchHoneyBatchTraceability(id: string): Promise { + return apiFetch(`/honey-batches/${id}/traceability`); +} + +export function createHoneyBatch(input: CreateHoneyBatchInput): Promise { + return apiFetch("/honey-batches", { + method: "POST", + body: JSON.stringify(input), + }); +} + +export function addHoneyMeasurement( + id: string, + input: CreateHoneyMeasurementInput, +): Promise { + return apiFetch(`/honey-batches/${id}/measurements`, { + method: "POST", + body: JSON.stringify(input), + }); +} diff --git a/ApiCore-Frontend/src/lib/honey-batches/types.ts b/ApiCore-Frontend/src/lib/honey-batches/types.ts new file mode 100644 index 0000000..7cf6010 --- /dev/null +++ b/ApiCore-Frontend/src/lib/honey-batches/types.ts @@ -0,0 +1,68 @@ +export type HoneyBatchStatus = + | "RAW" + | "EXTRACTED" + | "CLARIFIED" + | "CREAMED" + | "STORED" + | "BOTTLED" + | "SOLD" + | "CONSUMED"; + +export interface HoneyMeasurement { + id: string; + waterContent: number; + measuredAt: string; + device: string | null; + temperature: number | null; + notes: string | null; +} + +export interface HoneyBatch { + id: string; + batchNumber: string; + extractionId: string; + honeyType: string | null; + amount: number | null; + waterContent: number | null; + storageContainerId: string | null; + status: HoneyBatchStatus; + createdAt: string; + measurements: HoneyMeasurement[]; +} + +export interface Traceability { + honeyBatch: { + id: string; + batchNumber: string; + honeyType: string | null; + amount: number | null; + status: HoneyBatchStatus; + }; + extraction: { + id: string; + date: string; + room: string | null; + }; + harvests: { + id: string; + date: string; + apiaryId: string; + colonies: { id: string; code: string }[]; + }[]; +} + +export interface CreateHoneyBatchInput { + batchNumber: string; + extractionId: string; + honeyType?: string; + amount?: number; + waterContent?: number; +} + +export interface CreateHoneyMeasurementInput { + waterContent: number; + measuredAt: string; + device?: string; + temperature?: number; + notes?: string; +} diff --git a/TASKS.md b/TASKS.md index 2a794b7..4054618 100644 --- a/TASKS.md +++ b/TASKS.md @@ -99,7 +99,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo - Endpunkte: `GET/POST /api/v1/honey-batches`, `GET /api/v1/honey-batches/:id`, `GET /api/v1/honey-batches/:id/traceability`, `POST /api/v1/honey-batches/:id/measurements` - Frontend: `/honey-batches`, `/honey-batches/[id]` (inkl. Rückverfolgbarkeits-Ansicht) - [x] Backend-Modul fertig, getestet, grün, committet -- [ ] Frontend-Views fertig, getestet, grün, committet +- [x] Frontend-Views fertig, getestet, grün, committet ### 12. Grundlegende Lagerverwaltung