diff --git a/ApiCore-Frontend/src/app/(app)/bottling/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/bottling/[id]/page.tsx new file mode 100644 index 0000000..070ca61 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/bottling/[id]/page.tsx @@ -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

Lädt…

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

+ Abfüllung konnte nicht geladen werden. +

+ ); + } + + return ( +
+ + + Abfüllung {bottlingBatch.lotNumber} + + +

Honigcharge: {bottlingBatch.honeyBatch.batchNumber}

+ {bottlingBatch.product ?

Produkt: {bottlingBatch.product.name}

: null} +

Datum: {new Date(bottlingBatch.date).toLocaleDateString("de-DE")}

+

+ {bottlingBatch.jarCount} × {bottlingBatch.jarSize} g ={" "} + {bottlingBatch.totalAmount} kg +

+

+ Mindesthaltbar bis:{" "} + {new Date(bottlingBatch.bestBeforeDate).toLocaleDateString("de-DE")} +

+
+
+
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/bottling/new/page.tsx b/ApiCore-Frontend/src/app/(app)/bottling/new/page.tsx new file mode 100644 index 0000000..b4a7c6d --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/bottling/new/page.tsx @@ -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(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) { + 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 ( +
+ + + Neue Abfüllung + + +
+
+ + +
+
+ + +
+
+ + setDate(event.target.value)} + /> +
+
+ + setJarSize(event.target.value)} + /> +
+
+ + setJarCount(event.target.value)} + /> +
+
+ + setBestBeforeDate(event.target.value)} + /> +
+
+ + setLotNumber(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 index 852492c..5e0b4ba 100644 --- a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { useParams } from "next/navigation"; import { useMutation, useQuery } from "@tanstack/react-query"; import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api"; @@ -49,6 +50,15 @@ export default function HoneyBatchDetailPage() { {honeyBatch.honeyType ?

Sorte: {honeyBatch.honeyType}

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

Menge: {honeyBatch.amount} kg

: null} +
diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx index 74c0c97..4a5f4a3 100644 --- a/ApiCore-Frontend/src/app/(app)/layout.tsx +++ b/ApiCore-Frontend/src/app/(app)/layout.tsx @@ -29,6 +29,11 @@ const PRIMARY_LINKS = [ { label: "Suche", href: "/search" }, ]; +const MORE_LINKS = [ + { label: "Produkte", href: "/products" }, + { label: "Abfüllungen", href: "/bottling/new" }, +]; + const ADMIN_LINKS = [ { label: "Benutzer", href: "/settings/users" }, { label: "Audit-Log", href: "/settings/audit-log" }, @@ -73,23 +78,28 @@ export default function AppLayout({ children }: { children: ReactNode }) { {PRIMARY_LINKS.map((item) => ( ))} - {user.role === "ADMIN" ? ( - - } - > - - Mehr - - - {ADMIN_LINKS.map((item) => ( - }> - {item.label} - - ))} - - - ) : null} + + } + > + + Mehr + + + {MORE_LINKS.map((item) => ( + }> + {item.label} + + ))} + {user.role === "ADMIN" + ? ADMIN_LINKS.map((item) => ( + }> + {item.label} + + )) + : null} + +
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 + + +
+
+ + setName(event.target.value)} + /> +
+
+ +