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:
1 parent
e82cae257b
commit
f4df719df7
11 files changed
+554
-4
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api";
|
import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api";
|
||||||
@@ -49,6 +50,15 @@ export default function HoneyBatchDetailPage() {
|
|||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
{honeyBatch.honeyType ? <p>Sorte: {honeyBatch.honeyType}</p> : null}
|
{honeyBatch.honeyType ? <p>Sorte: {honeyBatch.honeyType}</p> : null}
|
||||||
{honeyBatch.amount !== null ? <p>Menge: {honeyBatch.amount} kg</p> : null}
|
{honeyBatch.amount !== null ? <p>Menge: {honeyBatch.amount} kg</p> : null}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-fit"
|
||||||
|
nativeButton={false}
|
||||||
|
render={<Link href={`/bottling/new?honeyBatchId=${honeyBatch.id}`} />}
|
||||||
|
>
|
||||||
|
Neue Abfüllung
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ const PRIMARY_LINKS = [
|
|||||||
{ label: "Suche", href: "/search" },
|
{ label: "Suche", href: "/search" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MORE_LINKS = [
|
||||||
|
{ label: "Produkte", href: "/products" },
|
||||||
|
{ label: "Abfüllungen", href: "/bottling/new" },
|
||||||
|
];
|
||||||
|
|
||||||
const ADMIN_LINKS = [
|
const ADMIN_LINKS = [
|
||||||
{ label: "Benutzer", href: "/settings/users" },
|
{ label: "Benutzer", href: "/settings/users" },
|
||||||
{ label: "Audit-Log", href: "/settings/audit-log" },
|
{ label: "Audit-Log", href: "/settings/audit-log" },
|
||||||
@@ -73,7 +78,6 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
|||||||
{PRIMARY_LINKS.map((item) => (
|
{PRIMARY_LINKS.map((item) => (
|
||||||
<NavLink key={item.href} {...item} pathname={pathname} />
|
<NavLink key={item.href} {...item} pathname={pathname} />
|
||||||
))}
|
))}
|
||||||
{user.role === "ADMIN" ? (
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
render={<Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" />}
|
render={<Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" />}
|
||||||
@@ -82,14 +86,20 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
|||||||
Mehr
|
Mehr
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
{ADMIN_LINKS.map((item) => (
|
{MORE_LINKS.map((item) => (
|
||||||
<DropdownMenuItem key={item.href} render={<Link href={item.href} />}>
|
<DropdownMenuItem key={item.href} render={<Link href={item.href} />}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
|
{user.role === "ADMIN"
|
||||||
|
? ADMIN_LINKS.map((item) => (
|
||||||
|
<DropdownMenuItem key={item.href} render={<Link href={item.href} />}>
|
||||||
|
{item.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="flex shrink-0 items-center gap-3 text-sm">
|
<div className="flex shrink-0 items-center gap-3 text-sm">
|
||||||
|
|||||||
@@ -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<string | null>(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<HTMLFormElement>) {
|
||||||
|
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 (
|
||||||
|
<main className="p-4 sm:p-6">
|
||||||
|
<Card className="max-w-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Neues Produkt</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="product-name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="product-name"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="product-description">Beschreibung</Label>
|
||||||
|
<Textarea
|
||||||
|
id="product-description"
|
||||||
|
value={description}
|
||||||
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="product-sku">SKU</Label>
|
||||||
|
<Input
|
||||||
|
id="product-sku"
|
||||||
|
value={sku}
|
||||||
|
onChange={(event) => setSku(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="product-packaging-size">Gebindegröße</Label>
|
||||||
|
<Input
|
||||||
|
id="product-packaging-size"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={packagingSize}
|
||||||
|
onChange={(event) => setPackagingSize(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="product-weight">Gewicht</Label>
|
||||||
|
<Input
|
||||||
|
id="product-weight"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={weight}
|
||||||
|
onChange={(event) => setWeight(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="product-sales-unit">Verkaufseinheit</Label>
|
||||||
|
<Input
|
||||||
|
id="product-sales-unit"
|
||||||
|
placeholder="z. B. Glas, Flasche"
|
||||||
|
value={salesUnit}
|
||||||
|
onChange={(event) => setSalesUnit(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error ? (
|
||||||
|
<p role="alert" className="text-sm text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending ? "Wird angelegt…" : "Anlegen"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Produkte</h1>
|
||||||
|
<Button nativeButton={false} render={<Link href="/products/new" />}>
|
||||||
|
+ Neues Produkt
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||||
|
{isError ? (
|
||||||
|
<p role="alert" className="text-sm text-destructive">
|
||||||
|
Produkte konnten nicht geladen werden.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{products ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>SKU</TableHead>
|
||||||
|
<TableHead>Verkaufseinheit</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{products.map((product) => (
|
||||||
|
<TableRow key={product.id}>
|
||||||
|
<TableCell className="font-medium">{product.name}</TableCell>
|
||||||
|
<TableCell className="font-mono">{product.sku ?? "—"}</TableCell>
|
||||||
|
<TableCell>{product.salesUnit ?? "—"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
|
||||||
|
{product.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
|
import { BottlingBatch, BottlingBatchFilter, CreateBottlingBatchInput } from "./types";
|
||||||
|
|
||||||
|
export function fetchBottlingBatches(
|
||||||
|
filter: BottlingBatchFilter = {},
|
||||||
|
): Promise<BottlingBatch[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter.honeyBatchId) params.set("honeyBatchId", filter.honeyBatchId);
|
||||||
|
const query = params.toString();
|
||||||
|
return apiFetch<BottlingBatch[]>(`/bottling-batches${query ? `?${query}` : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBottlingBatch(id: string): Promise<BottlingBatch> {
|
||||||
|
return apiFetch<BottlingBatch>(`/bottling-batches/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBottlingBatch(
|
||||||
|
input: CreateBottlingBatchInput,
|
||||||
|
): Promise<BottlingBatch> {
|
||||||
|
return apiFetch<BottlingBatch>("/bottling-batches", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
|
import {
|
||||||
|
CreateProductInput,
|
||||||
|
Product,
|
||||||
|
ProductFilter,
|
||||||
|
UpdateProductInput,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export function fetchProducts(filter: ProductFilter = {}): Promise<Product[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter.status) params.set("status", filter.status);
|
||||||
|
const query = params.toString();
|
||||||
|
return apiFetch<Product[]>(`/products${query ? `?${query}` : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchProduct(id: string): Promise<Product> {
|
||||||
|
return apiFetch<Product>(`/products/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProduct(input: CreateProductInput): Promise<Product> {
|
||||||
|
return apiFetch<Product>("/products", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProduct(id: string, input: UpdateProductInput): Promise<Product> {
|
||||||
|
return apiFetch<Product>(`/products/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<CreateProductInput> {
|
||||||
|
status?: ProductStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductFilter {
|
||||||
|
status?: ProductStatus;
|
||||||
|
}
|
||||||
@@ -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`
|
- 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`
|
- Frontend: `/bottling/new`, `/bottling/:id`, `/products`, `/products/new`
|
||||||
- [x] Backend-Modul fertig, getestet, grün, committet
|
- [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
|
### 24. Inventar
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user