diff --git a/ApiCore-Frontend/src/app/(app)/products/new/page.tsx b/ApiCore-Frontend/src/app/(app)/products/new/page.tsx
new file mode 100644
index 0000000..f79c039
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/products/new/page.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { FormEvent, useState } from "react";
+import { useRouter } from "next/navigation";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { createProduct } from "@/lib/products/api";
+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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+export default function NewProductPage() {
+ const router = useRouter();
+ const queryClient = useQueryClient();
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+ const [sku, setSku] = useState("");
+ const [packagingSize, setPackagingSize] = useState("");
+ const [weight, setWeight] = useState("");
+ const [salesUnit, setSalesUnit] = useState("");
+ const [error, setError] = useState
(null);
+
+ const mutation = useMutation({
+ mutationFn: createProduct,
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: ["products"] });
+ router.push("/products");
+ },
+ onError: () => {
+ setError("Produkt konnte nicht angelegt werden.");
+ },
+ });
+
+ function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ setError(null);
+ mutation.mutate({
+ name,
+ description: description || undefined,
+ sku: sku || undefined,
+ packagingSize: packagingSize ? Number(packagingSize) : undefined,
+ weight: weight ? Number(weight) : undefined,
+ salesUnit: salesUnit || undefined,
+ });
+ }
+
+ return (
+
+
+
+ Neues Produkt
+
+
+
+
+
+
+ );
+}
diff --git a/ApiCore-Frontend/src/app/(app)/products/page.tsx b/ApiCore-Frontend/src/app/(app)/products/page.tsx
new file mode 100644
index 0000000..9fe9f65
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/products/page.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import Link from "next/link";
+import { useQuery } from "@tanstack/react-query";
+import { fetchProducts } from "@/lib/products/api";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+export default function ProductsPage() {
+ const { data: products, isLoading, isError } = useQuery({
+ queryKey: ["products"],
+ queryFn: () => fetchProducts(),
+ });
+
+ return (
+
+
+
Produkte
+ }>
+ + Neues Produkt
+
+
+
+ {isLoading ? Lädt…
: null}
+ {isError ? (
+
+ Produkte konnten nicht geladen werden.
+
+ ) : null}
+
+ {products ? (
+
+
+
+ Name
+ SKU
+ Verkaufseinheit
+ Status
+
+
+
+ {products.map((product) => (
+
+ {product.name}
+ {product.sku ?? "—"}
+ {product.salesUnit ?? "—"}
+
+
+ {product.status}
+
+
+
+ ))}
+
+
+ ) : null}
+
+ );
+}
diff --git a/ApiCore-Frontend/src/lib/bottling/api.ts b/ApiCore-Frontend/src/lib/bottling/api.ts
new file mode 100644
index 0000000..b2c96db
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/bottling/api.ts
@@ -0,0 +1,24 @@
+import { apiFetch } from "@/lib/auth/api-client";
+import { BottlingBatch, BottlingBatchFilter, CreateBottlingBatchInput } from "./types";
+
+export function fetchBottlingBatches(
+ filter: BottlingBatchFilter = {},
+): Promise {
+ const params = new URLSearchParams();
+ if (filter.honeyBatchId) params.set("honeyBatchId", filter.honeyBatchId);
+ const query = params.toString();
+ return apiFetch(`/bottling-batches${query ? `?${query}` : ""}`);
+}
+
+export function fetchBottlingBatch(id: string): Promise {
+ return apiFetch(`/bottling-batches/${id}`);
+}
+
+export function createBottlingBatch(
+ input: CreateBottlingBatchInput,
+): Promise {
+ return apiFetch("/bottling-batches", {
+ method: "POST",
+ body: JSON.stringify(input),
+ });
+}
diff --git a/ApiCore-Frontend/src/lib/bottling/types.ts b/ApiCore-Frontend/src/lib/bottling/types.ts
new file mode 100644
index 0000000..31ce2b9
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/bottling/types.ts
@@ -0,0 +1,28 @@
+export interface BottlingBatch {
+ id: string;
+ honeyBatchId: string;
+ productId: string | null;
+ date: string;
+ jarSize: number;
+ jarCount: number;
+ totalAmount: number;
+ bestBeforeDate: string;
+ lotNumber: string;
+ createdAt: string;
+ honeyBatch: { id: string; batchNumber: string };
+ product: { id: string; name: string } | null;
+}
+
+export interface CreateBottlingBatchInput {
+ honeyBatchId: string;
+ productId?: string;
+ date: string;
+ jarSize: number;
+ jarCount: number;
+ bestBeforeDate: string;
+ lotNumber: string;
+}
+
+export interface BottlingBatchFilter {
+ honeyBatchId?: string;
+}
diff --git a/ApiCore-Frontend/src/lib/products/api.ts b/ApiCore-Frontend/src/lib/products/api.ts
new file mode 100644
index 0000000..55fe246
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/products/api.ts
@@ -0,0 +1,32 @@
+import { apiFetch } from "@/lib/auth/api-client";
+import {
+ CreateProductInput,
+ Product,
+ ProductFilter,
+ UpdateProductInput,
+} from "./types";
+
+export function fetchProducts(filter: ProductFilter = {}): Promise {
+ const params = new URLSearchParams();
+ if (filter.status) params.set("status", filter.status);
+ const query = params.toString();
+ return apiFetch(`/products${query ? `?${query}` : ""}`);
+}
+
+export function fetchProduct(id: string): Promise {
+ return apiFetch(`/products/${id}`);
+}
+
+export function createProduct(input: CreateProductInput): Promise {
+ return apiFetch("/products", {
+ method: "POST",
+ body: JSON.stringify(input),
+ });
+}
+
+export function updateProduct(id: string, input: UpdateProductInput): Promise {
+ return apiFetch(`/products/${id}`, {
+ method: "PATCH",
+ body: JSON.stringify(input),
+ });
+}
diff --git a/ApiCore-Frontend/src/lib/products/types.ts b/ApiCore-Frontend/src/lib/products/types.ts
new file mode 100644
index 0000000..a4503eb
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/products/types.ts
@@ -0,0 +1,31 @@
+export type ProductStatus = "ACTIVE" | "DISCONTINUED";
+
+export interface Product {
+ id: string;
+ name: string;
+ description: string | null;
+ sku: string | null;
+ packagingSize: number | null;
+ weight: number | null;
+ salesUnit: string | null;
+ status: ProductStatus;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface CreateProductInput {
+ name: string;
+ description?: string;
+ sku?: string;
+ packagingSize?: number;
+ weight?: number;
+ salesUnit?: string;
+}
+
+export interface UpdateProductInput extends Partial {
+ status?: ProductStatus;
+}
+
+export interface ProductFilter {
+ status?: ProductStatus;
+}
diff --git a/TASKS.md b/TASKS.md
index b8ad6a3..c2096ee 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -188,7 +188,7 @@ Reihenfolge = Bau-Reihenfolge. Gleiche Haken-Regel wie MVP: erst setzen, wenn Li
- Endpunkte: `GET/POST /api/v1/bottling-batches`, `GET /api/v1/bottling-batches/:id`, `GET/POST /api/v1/products`, `GET/PATCH /api/v1/products/:id`
- Frontend: `/bottling/new`, `/bottling/:id`, `/products`, `/products/new`
- [x] Backend-Modul fertig, getestet, grün, committet
-- [ ] Frontend-Views fertig, getestet, grün, committet
+- [x] Frontend-Views fertig, getestet, grün, committet
### 24. Inventar