diff --git a/backend/app/routers/views.py b/backend/app/routers/views.py index ba93e97..2f1b35e 100644 --- a/backend/app/routers/views.py +++ b/backend/app/routers/views.py @@ -32,6 +32,7 @@ def shopping_list( product_id=product.id, name=product.name, base_unit=product.base_unit, + package_size=product.package_size, stock=stock, min_stock=product.min_stock, deficit=product.min_stock - stock, diff --git a/backend/app/schemas.py b/backend/app/schemas.py index af6af1c..2182211 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -182,6 +182,7 @@ class ShoppingItem(BaseModel): product_id: int name: str base_unit: BaseUnit + package_size: float | None = None stock: float min_stock: float deficit: float diff --git a/web/src/pages/Dashboard.jsx b/web/src/pages/Dashboard.jsx index eb4c8ad..f6848d8 100644 --- a/web/src/pages/Dashboard.jsx +++ b/web/src/pages/Dashboard.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { api } from "../api"; import Icon from "../components/Icon"; -import { fmt, unitShort } from "../units"; +import { amountText, fmt, unitShort } from "../units"; export default function Dashboard() { const [expiring, setExpiring] = useState([]); @@ -115,8 +115,8 @@ export default function Dashboard() { {shopping.map((it) => ( {it.name} - {fmt(it.stock)} {unitShort(it.base_unit)} - {fmt(it.deficit)} {unitShort(it.base_unit)} + {amountText(it.stock, it.package_size, it.base_unit)} + {amountText(it.deficit, it.package_size, it.base_unit)} ))} diff --git a/web/src/pages/ProductForm.jsx b/web/src/pages/ProductForm.jsx index 3631832..86ed8f3 100644 --- a/web/src/pages/ProductForm.jsx +++ b/web/src/pages/ProductForm.jsx @@ -25,11 +25,23 @@ export default function ProductForm() { const [error, setError] = useState(null); const [info, setInfo] = useState(null); const [busy, setBusy] = useState(false); + // Einheit, in der der Mindestbestand eingegeben wird: "base" oder "package". + const [minUnit, setMinUnit] = useState("base"); function set(k, v) { setForm((f) => ({ ...f, [k]: v })); } + // Wechselt die Mindestbestand-Einheit und rechnet den angezeigten Wert um. + function changeMinUnit(newUnit) { + const ps = Number(form.package_size); + if (form.min_stock !== "" && ps > 0 && newUnit !== minUnit) { + const v = Number(form.min_stock); + set("min_stock", String(newUnit === "package" ? v / ps : v * ps)); + } + setMinUnit(newUnit); + } + function applySuggestion(s, groupsList) { const groupGuess = guessGroup(groupsList, s); setForm((f) => ({ @@ -42,6 +54,7 @@ export default function ProductForm() { package_size: s.package_size != null ? String(s.package_size) : f.package_size, group_id: f.group_id || groupGuess, })); + if (s.package_size != null) setMinUnit("package"); setInfo( "Daten von Open Food Facts übernommen." + (s.package_size != null ? "" : " (Füllmenge nicht hinterlegt – bitte Packungsgröße prüfen.)") + @@ -76,10 +89,19 @@ export default function ProductForm() { if (!isNew) { const p = await api.getProduct(id); setProduct(p); + // Mindestbestand ist in Basiseinheiten gespeichert; bei Packungsprodukten + // zeigen wir ihn zur besseren Verständlichkeit in Packungen an. + let minDisplay = p.min_stock ?? ""; + if (p.min_stock != null && p.package_size && p.package_size > 0) { + minDisplay = p.min_stock / p.package_size; + setMinUnit("package"); + } else { + setMinUnit("base"); + } setForm({ barcode: p.barcode || "", name: p.name, brand: p.brand || "", image_url: p.image_url || "", base_unit: p.base_unit, - package_size: p.package_size ?? "", min_stock: p.min_stock ?? "", + package_size: p.package_size ?? "", min_stock: minDisplay, group_id: p.group_id ?? "", }); setLots(await api.listLots(id)); @@ -99,6 +121,12 @@ export default function ProductForm() { }, [id]); function buildPayload() { + const ps = Number(form.package_size); + let minBase = null; + if (form.min_stock !== "") { + const v = Number(form.min_stock); + minBase = minUnit === "package" && ps > 0 ? v * ps : v; + } return { barcode: form.barcode || null, name: form.name, @@ -106,7 +134,7 @@ export default function ProductForm() { image_url: form.image_url || null, base_unit: form.base_unit, package_size: form.package_size === "" ? null : Number(form.package_size), - min_stock: form.min_stock === "" ? null : Number(form.min_stock), + min_stock: minBase, group_id: form.group_id === "" ? null : Number(form.group_id), }; } @@ -195,8 +223,16 @@ export default function ProductForm() {
- fehlt {fmt(it.deficit)} {unitShort(it.base_unit)}{" "} - (Bestand {fmt(it.stock)} / min {fmt(it.min_stock)}) + fehlt {amountText(it.deficit, it.package_size, it.base_unit)}{" "} + (Bestand {amountText(it.stock, it.package_size, it.base_unit)}) ); diff --git a/web/src/units.js b/web/src/units.js index a803826..ca21c8a 100644 --- a/web/src/units.js +++ b/web/src/units.js @@ -36,3 +36,11 @@ export function fmt(n) { if (n == null) return "–"; return Number(n).toLocaleString("de-DE", { maximumFractionDigits: 2 }); } + +// Menge lesbar darstellen: bei Packungsprodukten in Packungen (+ Basiseinheit in Klammern). +export function amountText(qty, packageSize, baseUnit) { + if (packageSize && packageSize > 0) { + return `${fmt(qty / packageSize)} Pkg (${fmt(qty)} ${unitShort(baseUnit)})`; + } + return `${fmt(qty)} ${unitShort(baseUnit)}`; +}