feat(ui): stock card with withdraw and inventory count, production batches
- Bottling detail shows Abgefüllt / Webshop verkauft / Entnommen / Rest (negative rest flagged "Prüfen") and the full movement list; buttons "Entnehmen" (reason, note) and "Inventur korrigieren" (counted quantity, difference booked). - Rest column in the bottling list. - New pages for production batches (list, create, detail with the same stock card), sidebar entry with its own permission. - Products get "Art" (honey vs. manufactured goods) and the webshop category; bottlings only offer honey articles. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
04f93e954a
commit
f57db97578
21 files changed
+1151
-5
No files matched your search
@@ -12,6 +12,7 @@ import { BottlingEditDialog } from "./bottling-edit-dialog";
|
||||
import { BottlingDeleteButton } from "./bottling-delete-button";
|
||||
import { PrintLabelsCard } from "./print-labels-card";
|
||||
import { DetailHeader, DetailQueryFallback } from "@/components/detail-header";
|
||||
import { StockCard } from "@/components/stock/stock-card";
|
||||
|
||||
const BACK = { backHref: "/bottling", backLabel: "Alle Abfüllungen" };
|
||||
|
||||
@@ -78,6 +79,13 @@ export default function BottlingBatchDetailPage() {
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<StockCard
|
||||
path="bottling-batches"
|
||||
id={bottlingBatch.id}
|
||||
initialLabel="Abgefüllt"
|
||||
unit="Gläser"
|
||||
canBook={hasPermission("bottling:withdraw")}
|
||||
/>
|
||||
<PrintLabelsCard bottlingBatchId={bottlingBatch.id} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -309,7 +309,9 @@ export default function NewBottlingBatchPage() {
|
||||
<SelectValue placeholder="Produkt wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{products?.map((product) => (
|
||||
{products
|
||||
?.filter((product) => product.kind !== "MANUFACTURED")
|
||||
.map((product) => (
|
||||
<SelectItem key={product.id} value={product.id}>
|
||||
{product.name}
|
||||
</SelectItem>
|
||||
|
||||
@@ -63,6 +63,13 @@ export default function BottlingBatchesPage() {
|
||||
label: "Menge",
|
||||
value: `${bottlingBatch.jarCount} × ${bottlingBatch.jarSize} g`,
|
||||
},
|
||||
{
|
||||
label: "Rest",
|
||||
value:
|
||||
bottlingBatch.remainingCount < 0
|
||||
? `${bottlingBatch.remainingCount} (prüfen)`
|
||||
: `${bottlingBatch.remainingCount} Gläser`,
|
||||
},
|
||||
{ label: "Datum", value: formatDate(bottlingBatch.date) },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { Factory } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { deleteProductionBatch, fetchProductionBatch } from "@/lib/production-batches/api";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ConfirmDialog } from "@/components/confirm-dialog";
|
||||
import { DetailHeader, DetailQueryFallback } from "@/components/detail-header";
|
||||
import { StockCard } from "@/components/stock/stock-card";
|
||||
import { useAuth } from "@/lib/auth/auth-context";
|
||||
|
||||
const BACK = { backHref: "/production-batches", backLabel: "Alle Herstellungschargen" };
|
||||
|
||||
export default function ProductionBatchDetailPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { hasPermission } = useAuth();
|
||||
const batchQuery = useQuery({
|
||||
queryKey: ["production-batches", params.id],
|
||||
queryFn: () => fetchProductionBatch(params.id),
|
||||
});
|
||||
const batch = batchQuery.data;
|
||||
|
||||
const deletion = useMutation({
|
||||
mutationFn: () => deleteProductionBatch(params.id),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["production-batches"] });
|
||||
toast.success("Charge gelöscht");
|
||||
router.push("/production-batches");
|
||||
},
|
||||
onError: (error: Error) => toast.error(error.message || "Charge konnte nicht gelöscht werden"),
|
||||
});
|
||||
|
||||
if (batchQuery.isLoading || batchQuery.isError || !batch) {
|
||||
return <DetailQueryFallback query={batchQuery} what="Daten der Charge" {...BACK} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||
<DetailHeader
|
||||
icon={Factory}
|
||||
title={`Charge ${batch.lotNumber}`}
|
||||
subtitle={`${batch.product.name} · ${batch.quantity} Stück hergestellt`}
|
||||
{...BACK}
|
||||
/>
|
||||
<div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2.5">
|
||||
<CardTitle>Angaben</CardTitle>
|
||||
{hasPermission("production-batch:delete") ? (
|
||||
<ConfirmDialog
|
||||
triggerLabel="Löschen"
|
||||
triggerVariant="outline"
|
||||
triggerSize="sm"
|
||||
title="Charge löschen"
|
||||
description={`Charge ${batch.lotNumber} wird entfernt. Das geht nur, solange noch nichts verkauft oder entnommen wurde - danach bleibt die Charge als Historie erhalten.`}
|
||||
confirmLabel="Endgültig löschen"
|
||||
onConfirm={() => deletion.mutate()}
|
||||
isPending={deletion.isPending}
|
||||
/>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2 text-sm">
|
||||
<p>Artikel: {batch.product.name}</p>
|
||||
<p>Herstelldatum: {new Date(batch.date).toLocaleDateString("de-DE")}</p>
|
||||
{batch.bestBeforeDate ? (
|
||||
<p>Mindesthaltbar bis: {new Date(batch.bestBeforeDate).toLocaleDateString("de-DE")}</p>
|
||||
) : null}
|
||||
{batch.notes ? <p className="whitespace-pre-line">Notiz: {batch.notes}</p> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<StockCard
|
||||
path="production-batches"
|
||||
id={batch.id}
|
||||
initialLabel="Hergestellt"
|
||||
unit="Stück"
|
||||
canBook={hasPermission("production-batch:withdraw")}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Factory } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { createProductionBatch } from "@/lib/production-batches/api";
|
||||
import { fetchProducts } from "@/lib/products/api";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
function today(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export default function NewProductionBatchPage() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [productId, setProductId] = useState("");
|
||||
const [lotNumber, setLotNumber] = useState("");
|
||||
const [date, setDate] = useState(today());
|
||||
const [quantity, setQuantity] = useState("");
|
||||
const [bestBeforeDate, setBestBeforeDate] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { data: products } = useQuery({
|
||||
queryKey: ["products", { status: "ACTIVE" }],
|
||||
queryFn: () => fetchProducts({ status: "ACTIVE" }),
|
||||
});
|
||||
const manufactured = (products ?? []).filter((product) => product.kind === "MANUFACTURED");
|
||||
const selected = manufactured.find((product) => product.id === productId);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createProductionBatch({
|
||||
productId,
|
||||
lotNumber: lotNumber.trim(),
|
||||
date: new Date(date).toISOString(),
|
||||
quantity: Number(quantity),
|
||||
bestBeforeDate: bestBeforeDate ? new Date(bestBeforeDate).toISOString() : undefined,
|
||||
notes: notes.trim() || undefined,
|
||||
}),
|
||||
onSuccess: (batch) => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["production-batches"] });
|
||||
toast.success(`Charge ${batch.lotNumber} angelegt`);
|
||||
router.push(`/production-batches/${batch.id}`);
|
||||
},
|
||||
onError: (mutationError: Error) => {
|
||||
const message = mutationError.message || "Charge konnte nicht angelegt werden";
|
||||
setError(message);
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
if (!productId) {
|
||||
setError("Bitte einen Artikel wählen.");
|
||||
return;
|
||||
}
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||
<PageHeader
|
||||
icon={Factory}
|
||||
title="Neue Herstellungscharge"
|
||||
description="Was hast du hergestellt, und wie viele Stück?"
|
||||
/>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<form className="flex max-w-xl flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="pb-product">Artikel</Label>
|
||||
<Select value={productId} onValueChange={(value) => setProductId(value ?? "")}>
|
||||
<SelectTrigger id="pb-product" className="w-full">
|
||||
<SelectValue placeholder="Artikel wählen">{selected?.name}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{manufactured.map((product) => (
|
||||
<SelectItem key={product.id} value={product.id}>
|
||||
{product.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{products && manufactured.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Noch kein Artikel der Art „Hergestellte Ware“. Lege ihn unter Produkte an.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="pb-lot">Chargennummer</Label>
|
||||
<Input
|
||||
id="pb-lot"
|
||||
required
|
||||
value={lotNumber}
|
||||
onChange={(event) => setLotNumber(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="pb-date">Herstelldatum</Label>
|
||||
<Input
|
||||
id="pb-date"
|
||||
type="date"
|
||||
required
|
||||
value={date}
|
||||
onChange={(event) => setDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="pb-quantity">Hergestellte Stückzahl</Label>
|
||||
<Input
|
||||
id="pb-quantity"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={1}
|
||||
step={1}
|
||||
required
|
||||
value={quantity}
|
||||
onChange={(event) => setQuantity(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="pb-best-before">Mindesthaltbar bis (optional)</Label>
|
||||
<Input
|
||||
id="pb-best-before"
|
||||
type="date"
|
||||
value={bestBeforeDate}
|
||||
onChange={(event) => setBestBeforeDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="pb-notes">Notiz (optional, intern)</Label>
|
||||
<Textarea id="pb-notes" value={notes} onChange={(event) => setNotes(event.target.value)} />
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<Button type="submit" size="touch" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? "Speichert…" : "Charge anlegen"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Factory } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchProductionBatches } from "@/lib/production-batches/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { EntityCard } from "@/components/entity-card";
|
||||
import { QueryState } from "@/components/query-state";
|
||||
import { useAuth } from "@/lib/auth/auth-context";
|
||||
|
||||
export default function ProductionBatchesPage() {
|
||||
const { hasPermission } = useAuth();
|
||||
const canCreate = hasPermission("production-batch:create");
|
||||
const batchesQuery = useQuery({
|
||||
queryKey: ["production-batches"],
|
||||
queryFn: () => fetchProductionBatches(),
|
||||
});
|
||||
|
||||
const newButton = canCreate ? (
|
||||
<Button size="touch" nativeButton={false} render={<Link href="/production-batches/new" />}>
|
||||
+ Neue Herstellungscharge
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||
<PageHeader
|
||||
icon={Factory}
|
||||
title="Herstellungschargen"
|
||||
description="Kerzen, Propolis-Produkte und andere hergestellte Waren - je Charge mit hergestellter Menge und Rest."
|
||||
actions={newButton}
|
||||
/>
|
||||
|
||||
<QueryState
|
||||
query={batchesQuery}
|
||||
what="Herstellungschargen"
|
||||
empty={
|
||||
<EmptyState
|
||||
icon={Factory}
|
||||
title="Noch keine Herstellungschargen"
|
||||
description="Lege eine Charge an, sobald du Kerzen, Tinkturen oder Salben hergestellt hast. Der Artikel muss die Art „Hergestellte Ware“ haben."
|
||||
action={newButton ?? undefined}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(batches) => (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{batches.map((batch) => (
|
||||
<EntityCard
|
||||
key={batch.id}
|
||||
href={`/production-batches/${batch.id}`}
|
||||
icon={Factory}
|
||||
title={<span className="font-mono">{batch.lotNumber}</span>}
|
||||
subtitle={batch.product.name}
|
||||
fields={[
|
||||
{ label: "Hergestellt", value: String(batch.quantity) },
|
||||
{
|
||||
label: "Rest",
|
||||
value: batch.remainingCount < 0 ? `${batch.remainingCount} (prüfen)` : String(batch.remainingCount),
|
||||
},
|
||||
{ label: "Datum", value: new Date(batch.date).toLocaleDateString("de-DE") },
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</QueryState>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,12 @@ import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { updateProduct } from "@/lib/products/api";
|
||||
import { Product, ProductStatus } from "@/lib/products/types";
|
||||
import { PRODUCT_STATUS_LABELS } from "@/lib/products/labels";
|
||||
import { Product, ProductStatus, ShopCategory } from "@/lib/products/types";
|
||||
import {
|
||||
PRODUCT_KIND_LABELS,
|
||||
PRODUCT_STATUS_LABELS,
|
||||
SHOP_CATEGORY_LABELS,
|
||||
} from "@/lib/products/labels";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -37,6 +41,7 @@ export function EditProductDialog({ product }: { product: Product }) {
|
||||
const [weight, setWeight] = useState(product.weight?.toString() ?? "");
|
||||
const [salesUnit, setSalesUnit] = useState(product.salesUnit ?? "");
|
||||
const [status, setStatus] = useState<ProductStatus>(product.status);
|
||||
const [shopCategory, setShopCategory] = useState<ShopCategory>(product.shopCategory);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
@@ -48,6 +53,7 @@ export function EditProductDialog({ product }: { product: Product }) {
|
||||
weight: weight ? Number(weight) : undefined,
|
||||
salesUnit: salesUnit || undefined,
|
||||
status,
|
||||
shopCategory,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["products"] });
|
||||
@@ -145,6 +151,29 @@ export function EditProductDialog({ product }: { product: Product }) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="product-edit-shop-category">Kategorie im Webshop</Label>
|
||||
<Select
|
||||
value={shopCategory}
|
||||
onValueChange={(value) =>
|
||||
setShopCategory((value ?? shopCategory) as ShopCategory)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="product-edit-shop-category" className="w-full">
|
||||
<SelectValue>{SHOP_CATEGORY_LABELS[shopCategory]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(SHOP_CATEGORY_LABELS) as ShopCategory[]).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{SHOP_CATEGORY_LABELS[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Art: {PRODUCT_KIND_LABELS[product.kind]} - nicht änderbar, sobald Chargen existieren.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="product-edit-sales-unit">Verkaufseinheit</Label>
|
||||
<Input
|
||||
|
||||
@@ -59,6 +59,8 @@ describe("NewProductPage", () => {
|
||||
packagingSize: 500,
|
||||
weight: undefined,
|
||||
salesUnit: undefined,
|
||||
kind: "BOTTLED_HONEY",
|
||||
shopCategory: "HONIG",
|
||||
});
|
||||
expect(push).toHaveBeenCalledWith("/products");
|
||||
});
|
||||
|
||||
@@ -17,6 +17,15 @@ import { Field } from "@/components/form/field";
|
||||
import { FormSection } from "@/components/form/form-section";
|
||||
import { StickySubmit } from "@/components/form/sticky-submit";
|
||||
import { DraftBanner } from "@/components/form/draft-banner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PRODUCT_KIND_LABELS, SHOP_CATEGORY_LABELS } from "@/lib/products/labels";
|
||||
import type { ProductKind, ShopCategory } from "@/lib/products/types";
|
||||
import {
|
||||
ProductFormErrors,
|
||||
ProductFormValues,
|
||||
@@ -121,6 +130,51 @@ export default function NewProductPage() {
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
label="Art"
|
||||
hint="Honig wird abgefüllt; Kerzen, Tinkturen und Salben bekommen Herstellungschargen"
|
||||
>
|
||||
{({ id, describedBy }) => (
|
||||
<Select
|
||||
value={values.kind}
|
||||
onValueChange={(value) =>
|
||||
set("kind", (value ?? values.kind) as ProductKind)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id={id} aria-describedby={describedBy} className="w-full">
|
||||
<SelectValue>{PRODUCT_KIND_LABELS[values.kind]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(PRODUCT_KIND_LABELS) as ProductKind[]).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{PRODUCT_KIND_LABELS[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="Kategorie im Webshop">
|
||||
{({ id, describedBy }) => (
|
||||
<Select
|
||||
value={values.shopCategory}
|
||||
onValueChange={(value) =>
|
||||
set("shopCategory", (value ?? values.shopCategory) as ShopCategory)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id={id} aria-describedby={describedBy} className="w-full">
|
||||
<SelectValue>{SHOP_CATEGORY_LABELS[values.shopCategory]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(SHOP_CATEGORY_LABELS) as ShopCategory[]).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{SHOP_CATEGORY_LABELS[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="Verkaufseinheit" hint="z. B. Glas, Flasche">
|
||||
{({ id, describedBy }) => (
|
||||
<Input
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { CreateProductInput } from "@/lib/products/types";
|
||||
import type {
|
||||
CreateProductInput,
|
||||
ProductKind,
|
||||
ShopCategory,
|
||||
} from "@/lib/products/types";
|
||||
|
||||
/** Everything the form holds, as plain values that survive a draft save. */
|
||||
export interface ProductFormValues {
|
||||
@@ -8,6 +12,8 @@ export interface ProductFormValues {
|
||||
weight: string;
|
||||
sku: string;
|
||||
description: string;
|
||||
kind: ProductKind;
|
||||
shopCategory: ShopCategory;
|
||||
}
|
||||
|
||||
export type ProductFormErrors = Partial<Record<keyof ProductFormValues, string>>;
|
||||
@@ -20,6 +26,8 @@ export function initialProductValues(): ProductFormValues {
|
||||
weight: "",
|
||||
sku: "",
|
||||
description: "",
|
||||
kind: "BOTTLED_HONEY",
|
||||
shopCategory: "HONIG",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,5 +51,7 @@ export function toProductInput(values: ProductFormValues): CreateProductInput {
|
||||
packagingSize: numberOrUndefined(values.packagingSize),
|
||||
weight: numberOrUndefined(values.weight),
|
||||
salesUnit: textOrUndefined(values.salesUnit),
|
||||
kind: values.kind,
|
||||
shopCategory: values.shopCategory,
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Crown,
|
||||
Database,
|
||||
Droplet,
|
||||
Factory,
|
||||
FileClock,
|
||||
FlaskConical,
|
||||
Hexagon,
|
||||
@@ -81,6 +82,12 @@ const NAV_GROUPS: NavGroup[] = [
|
||||
{ label: "Schleuderungen", href: "/extractions", icon: FlaskConical },
|
||||
{ label: "Honigchargen", href: "/honey-batches", icon: Droplet },
|
||||
{ label: "Abfüllungen", href: "/bottling", icon: Package },
|
||||
{
|
||||
label: "Herstellungschargen",
|
||||
href: "/production-batches",
|
||||
icon: Factory,
|
||||
permission: "production-batch:read",
|
||||
},
|
||||
{ label: "Produkte", href: "/products", icon: ShoppingBag },
|
||||
{ label: "Lager", href: "/storage", icon: Warehouse },
|
||||
{ label: "Inventar", href: "/equipment", icon: Wrench },
|
||||
|
||||
Reference in new issue
Block a user