feat(frontend): Abfuellungen und Produkte (Modul 23)

/products, /products/new, /bottling/new, /bottling/:id. Verbindung
von der Honigcharge-Detailseite zur Abfuellung, Nav-Dropdown "Mehr"
fuer alle Rollen mit Produkte/Abfuellungen erweitert.
This commit is contained in:
Nick Szaunig committed 2026-08-22 22:21:22 +02:00
1 parent e82cae257b
commit f4df719df7
11 files changed
+568 -18

No files matched your search

@@ -0,0 +1,48 @@
"use client";
import { useParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { fetchBottlingBatch } from "@/lib/bottling/api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function BottlingBatchDetailPage() {
const params = useParams<{ id: string }>();
const { data: bottlingBatch, isLoading, isError } = useQuery({
queryKey: ["bottling-batches", params.id],
queryFn: () => fetchBottlingBatch(params.id),
});
if (isLoading) {
return <p className="p-4 text-sm text-muted-foreground sm:p-6">Lädt…</p>;
}
if (isError || !bottlingBatch) {
return (
<p role="alert" className="p-4 text-sm text-destructive sm:p-6">
Abfüllung konnte nicht geladen werden.
</p>
);
}
return (
<main className="flex flex-col gap-4 p-4 sm:p-6">
<Card className="max-w-lg">
<CardHeader>
<CardTitle>Abfüllung {bottlingBatch.lotNumber}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-2 text-sm">
<p>Honigcharge: {bottlingBatch.honeyBatch.batchNumber}</p>
{bottlingBatch.product ? <p>Produkt: {bottlingBatch.product.name}</p> : null}
<p>Datum: {new Date(bottlingBatch.date).toLocaleDateString("de-DE")}</p>
<p>
{bottlingBatch.jarCount} × {bottlingBatch.jarSize} g ={" "}
{bottlingBatch.totalAmount} kg
</p>
<p>
Mindesthaltbar bis:{" "}
{new Date(bottlingBatch.bestBeforeDate).toLocaleDateString("de-DE")}
</p>
</CardContent>
</Card>
</main>
);
}
@@ -0,0 +1,177 @@
"use client";
import { FormEvent, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
import { fetchProducts } from "@/lib/products/api";
import { createBottlingBatch } from "@/lib/bottling/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function NewBottlingBatchPage() {
const router = useRouter();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const [honeyBatchId, setHoneyBatchId] = useState(searchParams.get("honeyBatchId") ?? "");
const [productId, setProductId] = useState("");
const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10));
const [jarSize, setJarSize] = useState("500");
const [jarCount, setJarCount] = useState("");
const [bestBeforeDate, setBestBeforeDate] = useState("");
const [lotNumber, setLotNumber] = useState("");
const [error, setError] = useState<string | null>(null);
const { data: honeyBatches } = useQuery({
queryKey: ["honey-batches"],
queryFn: () => fetchHoneyBatches(),
});
const { data: products } = useQuery({
queryKey: ["products"],
queryFn: () => fetchProducts({ status: "ACTIVE" }),
});
const mutation = useMutation({
mutationFn: createBottlingBatch,
onSuccess: (bottlingBatch) => {
void queryClient.invalidateQueries({ queryKey: ["bottling-batches"] });
void queryClient.invalidateQueries({ queryKey: ["honey-batches"] });
router.push(`/bottling/${bottlingBatch.id}`);
},
onError: (mutationError: Error) => {
setError(mutationError.message || "Abfüllung konnte nicht angelegt werden.");
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
mutation.mutate({
honeyBatchId,
productId: productId || undefined,
date: new Date(date).toISOString(),
jarSize: Number(jarSize),
jarCount: Number(jarCount),
bestBeforeDate: new Date(bestBeforeDate).toISOString(),
lotNumber,
});
}
return (
<main className="p-4 sm:p-6">
<Card className="max-w-lg">
<CardHeader>
<CardTitle>Neue Abfüllung</CardTitle>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-honey-batch">Honigcharge</Label>
<Select
value={honeyBatchId}
onValueChange={(value) => setHoneyBatchId(value ?? "")}
>
<SelectTrigger id="bottling-honey-batch">
<SelectValue placeholder="Charge wählen" />
</SelectTrigger>
<SelectContent>
{honeyBatches?.map((honeyBatch) => (
<SelectItem key={honeyBatch.id} value={honeyBatch.id}>
{honeyBatch.batchNumber}
{honeyBatch.amount !== null ? ` · ${honeyBatch.amount} kg` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-product">Produkt (optional)</Label>
<Select
value={productId}
onValueChange={(value) => setProductId(value ?? "")}
>
<SelectTrigger id="bottling-product">
<SelectValue placeholder="Produkt wählen" />
</SelectTrigger>
<SelectContent>
{products?.map((product) => (
<SelectItem key={product.id} value={product.id}>
{product.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-date">Datum</Label>
<Input
id="bottling-date"
type="date"
required
value={date}
onChange={(event) => setDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-jar-size">Glasgröße (g)</Label>
<Input
id="bottling-jar-size"
type="number"
required
value={jarSize}
onChange={(event) => setJarSize(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-jar-count">Anzahl Gläser</Label>
<Input
id="bottling-jar-count"
type="number"
required
value={jarCount}
onChange={(event) => setJarCount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-best-before">Mindesthaltbarkeitsdatum</Label>
<Input
id="bottling-best-before"
type="date"
required
value={bestBeforeDate}
onChange={(event) => setBestBeforeDate(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="bottling-lot-number">Los-/Chargennummer</Label>
<Input
id="bottling-lot-number"
required
value={lotNumber}
onChange={(event) => setLotNumber(event.target.value)}
/>
</div>
{error ? (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
) : null}
<Button type="submit" disabled={mutation.isPending || !honeyBatchId}>
{mutation.isPending ? "Wird angelegt…" : "Abfüllung anlegen"}
</Button>
</form>
</CardContent>
</Card>
</main>
);
}