Einkaufsliste: Fehlmenge als Gebinde (Glaeser/Dosen), Basiseinheit klein

Statt 'fehlt 500 Gramm' jetzt die Produktgroesse als Leitangabe:
'fehlt 3 Glaeser (500 g)'. Auf ganze Gebinde aufgerundet, weil man nur
ganze Glaeser/Dosen kauft; Basiseinheit als kleiner Hinweis.

Backend: neues ShoppingNeed-Objekt an jeder Einkaufslisten-Zeile (Produkte
+ Gruppen, gesamt + je Ort) - damit die Angabe auch direkt ueber die API
kommt ('x Glaeser (y g)'), inkl. Pluralisierung aus der Gebinde-Tabelle.
Gruppen leiten das Gebinde ab, wenn alle passenden Produkte dasselbe haben,
sonst bleibt es bei der Basiseinheit. Bestehende Felder unveraendert.

Frontend: gemeinsame Komponente ShoppingNeedText fuer Karte und volle Seite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-07 18:03:37 +02:00
parent e214bb76a9
commit f2c657ff31
6 changed files with 294 additions and 29 deletions

View File

@@ -1,3 +1,4 @@
import math
from collections import defaultdict from collections import defaultdict
from datetime import date, timedelta from datetime import date, timedelta
from typing import Callable from typing import Callable
@@ -14,11 +15,13 @@ from ..models import (
Location, Location,
Lot, Lot,
Movement, Movement,
PackageType,
Product, Product,
ProductLocationMinStock, ProductLocationMinStock,
User, User,
) )
from ..schemas import ( from ..schemas import (
BaseUnit,
ExpiringItem, ExpiringItem,
GroupShoppingItem, GroupShoppingItem,
LocationContentEntry, LocationContentEntry,
@@ -29,6 +32,7 @@ from ..schemas import (
MovementOut, MovementOut,
ShoppingItem, ShoppingItem,
ShoppingListAll, ShoppingListAll,
ShoppingNeed,
) )
from ..services.conversion import BASE_OF_KIND, article_unit, display_unit_info from ..services.conversion import BASE_OF_KIND, article_unit, display_unit_info
from ..services.stock import ( from ..services.stock import (
@@ -41,11 +45,119 @@ from .settings import get_expiry_warning_days
router = APIRouter(tags=["views"]) router = APIRouter(tags=["views"])
# ---- Bedarf lesbar aufbereiten (Gebinde als Leitangabe) --------------------
_UNIT_SHORT: dict[BaseUnit, str] = {
BaseUnit.piece: "Stk",
BaseUnit.gram: "g",
BaseUnit.milliliter: "ml",
}
def _de_num(x: float) -> str:
"""Deutsche Kurzzahl ohne unnoetige Nullen: 500, 1, 1,4."""
r = round(float(x), 2)
if r == int(r):
return str(int(r))
return f"{r:.2f}".rstrip("0").rstrip(".").replace(".", ",")
def _package_plural(db: Session) -> dict[str, str]:
"""Einzahl -> Mehrzahl der Gebinde (Glas -> Gläser), aus der Gebinde-Tabelle."""
return {pt.singular: pt.plural for pt in db.query(PackageType).all()}
def _uniform_package(products: list[Product]) -> tuple[float, str] | None:
"""Gemeinsames Gebinde einer Produktmenge nur wenn *alle* dasselbe haben.
Für eine Gruppe lässt sich „x Gläser" nur dann eindeutig sagen, wenn jedes
passende Produkt dieselbe Packungsgröße und -bezeichnung trägt. Sonst bleibt
es bei der Basiseinheit."""
if not products:
return None
combos = {(p.package_size, p.package_label or "Packung") for p in products}
if len(combos) == 1:
size, label = next(iter(combos))
if size and size > 0:
return float(size), label
return None
def _build_need(
*,
deficit_base: float,
stock_base: float,
min_base: float,
factor: float,
singular: str,
is_package: bool,
base_unit: BaseUnit | None,
plural: dict[str, str],
) -> ShoppingNeed:
"""Rechnet Basiseinheiten in Gebinde/Artikeleinheiten um und baut die Texte.
``factor`` = Basiseinheiten je Gebinde (Packungsgröße bzw. Einheitenfaktor).
Bei zählbaren Packungen wird die Kaufmenge auf ganze Gebinde aufgerundet."""
f = factor or 1.0
count = math.ceil(deficit_base / f - 1e-9) if is_package else round(deficit_base / f, 3)
label = singular if abs(count) == 1 else plural.get(singular, singular)
text = f"{_de_num(count)} {label}".strip()
hint = ""
base_amount: float | None = None
if (is_package or f != 1.0) and base_unit is not None:
base_amount = round(deficit_base, 3)
hint = f"{_de_num(deficit_base)} {_UNIT_SHORT.get(base_unit, base_unit.value)}"
return ShoppingNeed(
text=text,
hint=hint,
count=count,
label=label,
singular=singular,
is_package=is_package,
base_amount=base_amount,
base_unit=base_unit if base_amount is not None else None,
stock=round(stock_base / f, 3),
min_stock=round(min_base / f, 3),
)
def _gruppen_bedarf(
*,
deficit_unit: float,
stock_unit: float,
min_unit: float,
divisor: float,
unit_name: str,
base_unit: BaseUnit | None,
products: list[Product],
plural: dict[str, str],
) -> ShoppingNeed:
"""Bedarf einer Gruppe. Mengen kommen in der Gruppen-Einheit (``unit_name``);
``divisor`` rechnet sie in Basiseinheiten zurück. Haben alle passenden
Produkte dasselbe Gebinde, wird als Leitangabe dieses Gebinde (z.B. Gläser)
genutzt, sonst die Gruppen-Einheit selbst."""
pkg = _uniform_package(products) if base_unit is not None else None
if pkg is not None:
factor, singular, is_package = pkg[0], pkg[1], True
else:
factor, singular, is_package = (divisor or 1.0), unit_name, False
return _build_need(
deficit_base=deficit_unit * divisor,
stock_base=stock_unit * divisor,
min_base=min_unit * divisor,
factor=factor,
singular=singular,
is_package=is_package,
base_unit=base_unit,
plural=plural,
)
@router.get("/shopping-list", response_model=list[ShoppingItem]) @router.get("/shopping-list", response_model=list[ShoppingItem])
def shopping_list( def shopping_list(
db: Session = Depends(get_db), _: User = Depends(get_current_user) db: Session = Depends(get_db), _: User = Depends(get_current_user)
) -> list[ShoppingItem]: ) -> list[ShoppingItem]:
"""Produkte, deren Bestand unter dem Mindestbestand liegt.""" """Produkte, deren Bestand unter dem Mindestbestand liegt."""
plural = _package_plural(db)
items: list[ShoppingItem] = [] items: list[ShoppingItem] = []
products = ( products = (
db.query(Product) db.query(Product)
@@ -55,6 +167,7 @@ def shopping_list(
for product in products: for product in products:
stock = current_stock(db, product.id) stock = current_stock(db, product.id)
if stock < product.min_stock: if stock < product.min_stock:
factor, singular = article_unit(product)
items.append( items.append(
ShoppingItem( ShoppingItem(
product_id=product.id, product_id=product.id,
@@ -64,6 +177,16 @@ def shopping_list(
stock=stock, stock=stock,
min_stock=product.min_stock, min_stock=product.min_stock,
deficit=product.min_stock - stock, deficit=product.min_stock - stock,
need=_build_need(
deficit_base=product.min_stock - stock,
stock_base=stock,
min_base=product.min_stock,
factor=factor,
singular=singular,
is_package=bool(product.package_size and product.package_size > 0),
base_unit=product.base_unit,
plural=plural,
),
) )
) )
items.sort(key=lambda i: i.deficit, reverse=True) items.sort(key=lambda i: i.deficit, reverse=True)
@@ -79,6 +202,7 @@ def group_shopping_list(
Gruppen-Bestand = Summe der Produktbestände in der Gruppe (in Basiseinheiten). Gruppen-Bestand = Summe der Produktbestände in der Gruppe (in Basiseinheiten).
Sinnvoll, wenn die Produkte einer Gruppe dieselbe Basiseinheit teilen. Sinnvoll, wenn die Produkte einer Gruppe dieselbe Basiseinheit teilen.
""" """
plural = _package_plural(db)
items: list[GroupShoppingItem] = [] items: list[GroupShoppingItem] = []
groups = ( groups = (
db.query(Group).filter(Group.min_stock.isnot(None), Group.min_stock > 0).all() db.query(Group).filter(Group.min_stock.isnot(None), Group.min_stock > 0).all()
@@ -88,22 +212,37 @@ def group_shopping_list(
if unit is not None: if unit is not None:
base = BASE_OF_KIND[unit.kind] base = BASE_OF_KIND[unit.kind]
products = [p for p in group.products if p.base_unit == base] products = [p for p in group.products if p.base_unit == base]
stock = float(sum(current_stock(db, p.id) for p in products)) / unit.factor divisor = unit.factor
stock = float(sum(current_stock(db, p.id) for p in products)) / divisor
unit_name = unit.name unit_name = unit.name
base_unit: BaseUnit | None = base
else: else:
products = list(group.products) products = list(group.products)
divisor = 1.0
stock = float(sum(current_stock(db, p.id) for p in products)) stock = float(sum(current_stock(db, p.id) for p in products))
unit_name = "" unit_name = ""
base_unit = None
if stock < group.min_stock: if stock < group.min_stock:
deficit = group.min_stock - stock
items.append( items.append(
GroupShoppingItem( GroupShoppingItem(
group_id=group.id, group_id=group.id,
name=group.name, name=group.name,
stock=stock, stock=stock,
min_stock=group.min_stock, min_stock=group.min_stock,
deficit=group.min_stock - stock, deficit=deficit,
unit_name=unit_name, unit_name=unit_name,
product_count=len(products), product_count=len(products),
need=_gruppen_bedarf(
deficit_unit=deficit,
stock_unit=stock,
min_unit=group.min_stock,
divisor=divisor,
unit_name=unit_name,
base_unit=base_unit,
products=products,
plural=plural,
),
) )
) )
items.sort(key=lambda i: i.deficit, reverse=True) items.sort(key=lambda i: i.deficit, reverse=True)
@@ -141,6 +280,7 @@ def shopping_list_by_location(
) -> list[LocationNeeds]: ) -> list[LocationNeeds]:
"""Bedarfe je Lagerort: Produkte und Gruppen, deren Bestand AN DIESEM ORT """Bedarfe je Lagerort: Produkte und Gruppen, deren Bestand AN DIESEM ORT
unter dem dort hinterlegten Mindestbestand liegt.""" unter dem dort hinterlegten Mindestbestand liegt."""
plural = _package_plural(db)
prod_needs: dict[str, list[LocationNeedProduct]] = defaultdict(list) prod_needs: dict[str, list[LocationNeedProduct]] = defaultdict(list)
group_needs: dict[str, list[LocationNeedGroup]] = defaultdict(list) group_needs: dict[str, list[LocationNeedGroup]] = defaultdict(list)
@@ -154,6 +294,7 @@ def shopping_list_by_location(
continue continue
faktor, label = article_unit(product) faktor, label = article_unit(product)
faktor = faktor or 1.0 faktor = faktor or 1.0
ist_gebinde = bool(product.package_size and product.package_size > 0)
locs_min = {e.location_id: e.min_stock for e in entries} locs_min = {e.location_id: e.min_stock for e in entries}
bestand = {loc: location_subtree_stock_base(db, product, loc) / faktor for loc in locs_min} bestand = {loc: location_subtree_stock_base(db, product, loc) / faktor for loc in locs_min}
for loc, need in _netted_topups(db, locs_min, bestand.__getitem__).items(): for loc, need in _netted_topups(db, locs_min, bestand.__getitem__).items():
@@ -162,6 +303,17 @@ def shopping_list_by_location(
product_id=product.id, name=product.name, unit_label=label, product_id=product.id, name=product.name, unit_label=label,
stock=round(bestand[loc], 3), min_stock=locs_min[loc], stock=round(bestand[loc], 3), min_stock=locs_min[loc],
deficit=round(need, 3), deficit=round(need, 3),
# Mengen liegen hier in Artikeleinheiten -> * faktor = Basiseinheiten.
need=_build_need(
deficit_base=need * faktor,
stock_base=bestand[loc] * faktor,
min_base=locs_min[loc] * faktor,
factor=faktor,
singular=label,
is_package=ist_gebinde,
base_unit=product.base_unit,
plural=plural,
),
)) ))
# Je Gruppe genauso Bestand je Ort ist die Summe der passenden Produkte im Subtree. # Je Gruppe genauso Bestand je Ort ist die Summe der passenden Produkte im Subtree.
@@ -177,8 +329,10 @@ def shopping_list_by_location(
base = BASE_OF_KIND[unit.kind] base = BASE_OF_KIND[unit.kind]
matching = [p for p in group.products if p.base_unit == base] matching = [p for p in group.products if p.base_unit == base]
divisor, unit_name = unit.factor, unit.name divisor, unit_name = unit.factor, unit.name
g_base_unit: BaseUnit | None = base
else: else:
matching, divisor, unit_name = list(group.products), 1.0, "" matching, divisor, unit_name = list(group.products), 1.0, ""
g_base_unit = None
locs_min = {e.location_id: e.min_stock for e in entries} locs_min = {e.location_id: e.min_stock for e in entries}
bestand = { bestand = {
loc: sum(location_subtree_stock_base(db, p, loc) for p in matching) / divisor loc: sum(location_subtree_stock_base(db, p, loc) for p in matching) / divisor
@@ -190,6 +344,16 @@ def shopping_list_by_location(
group_id=group.id, name=group.name, unit_name=unit_name, group_id=group.id, name=group.name, unit_name=unit_name,
stock=round(bestand[loc], 3), min_stock=locs_min[loc], stock=round(bestand[loc], 3), min_stock=locs_min[loc],
deficit=round(need, 3), deficit=round(need, 3),
need=_gruppen_bedarf(
deficit_unit=need,
stock_unit=bestand[loc],
min_unit=locs_min[loc],
divisor=divisor,
unit_name=unit_name,
base_unit=g_base_unit,
products=matching,
plural=plural,
),
)) ))
loc_ids = set(prod_needs) | set(group_needs) loc_ids = set(prod_needs) | set(group_needs)

View File

@@ -665,11 +665,29 @@ class ItemOut(BaseModel):
# ---- Views ---- # ---- Views ----
class ShoppingNeed(BaseModel):
"""Fehlmenge als Gebinde-Leitangabe bei zaehlbaren Packungen (Glas/Dose)
auf ganze aufgerundet, weil man nur ganze Gebinde kauft mit der
Basiseinheit als kleinem Hinweis. So bekommen auch API-Konsumenten direkt
„1 Glas (500 g)", ohne selbst Packungsgroessen umzurechnen."""
text: str # Leitangabe: "1 Glas", "4 Dosen", "500 g"
hint: str = "" # Basiseinheit klein: "500 g" / "2 l" leer wenn identisch
count: float # aufgerundete Gebindezahl (zum Weiterrechnen)
label: str # zur count passendes, pluralisiertes Label
singular: str # Einzahl-Label ("Glas", "Gramm", …)
is_package: bool = False # zaehlbares Gebinde (Glas/Dose) vs. Basis-/Anzeigeeinheit
base_amount: float | None = None
base_unit: BaseUnit | None = None
stock: float # Bestand in derselben Leitangabe
min_stock: float # Mindestbestand in derselben Leitangabe
class ShoppingItem(BaseModel): class ShoppingItem(BaseModel):
product_id: int product_id: int
name: str name: str
base_unit: BaseUnit base_unit: BaseUnit
package_size: float | None = None package_size: float | None = None
need: ShoppingNeed | None = None
stock: float stock: float
min_stock: float min_stock: float
deficit: float deficit: float
@@ -699,6 +717,7 @@ class GroupShoppingItem(BaseModel):
deficit: float deficit: float
unit_name: str = "" unit_name: str = ""
product_count: int product_count: int
need: ShoppingNeed | None = None
class LocationNeedProduct(BaseModel): class LocationNeedProduct(BaseModel):
@@ -709,6 +728,7 @@ class LocationNeedProduct(BaseModel):
stock: float stock: float
min_stock: float min_stock: float
deficit: float deficit: float
need: ShoppingNeed | None = None
class LocationNeedGroup(BaseModel): class LocationNeedGroup(BaseModel):
@@ -718,6 +738,7 @@ class LocationNeedGroup(BaseModel):
stock: float stock: float
min_stock: float min_stock: float
deficit: float deficit: float
need: ShoppingNeed | None = None
class LocationNeeds(BaseModel): class LocationNeeds(BaseModel):

View File

@@ -0,0 +1,66 @@
"""Einkaufsliste: Fehlmenge als Gebinde-Leitangabe (auf ganze Packungen
aufgerundet) mit der Basiseinheit als kleinem Hinweis „3 Gläser (500 g)".
"""
import pytest
from app.models import BaseUnit, Group, PackageType, Product, Role, Unit, User
from app.routers.views import group_shopping_list, shopping_list
@pytest.fixture()
def user(db):
person = User(username="tester", password_hash="x", role=Role.admin)
db.add(person)
db.commit()
db.refresh(person)
return person
def _gramm(db) -> Unit:
return db.query(Unit).filter(Unit.name == "Gramm").one()
def test_produkt_bedarf_in_ganzen_gebinden(db, user):
"""500 g fehlen bei 195-g-Gläsern -> 3 Gläser (aufgerundet), 500 g als Hinweis."""
db.add(PackageType(singular="Glas", plural="Gläser"))
db.add(Product(name="Pesto", base_unit=BaseUnit.gram, package_size=195,
package_label="Glas", min_stock=500))
db.commit() # kein Bestand -> es fehlt der volle Mindestbestand
(item,) = shopping_list(db=db, _=user)
assert item.need is not None
assert item.need.is_package is True
assert item.need.count == 3 # ceil(500 / 195)
assert item.need.text == "3 Gläser"
assert item.need.hint == "500 g"
def test_gruppe_mit_einheitlichem_gebinde(db, user):
"""Eine Gruppe, deren Produkte alle dasselbe Gebinde haben, zeigt es auch."""
db.add(PackageType(singular="Glas", plural="Gläser"))
gruppe = Group(name="Pesto alla Genovese", min_stock=500, min_stock_unit=_gramm(db))
db.add(gruppe)
db.add(Product(name="Pesto Barilla", base_unit=BaseUnit.gram, package_size=195,
package_label="Glas", group=gruppe))
db.commit()
(item,) = group_shopping_list(db=db, _=user)
assert item.need is not None
assert item.need.is_package is True
assert item.need.text == "3 Gläser"
assert item.need.hint == "500 g"
def test_gruppe_ohne_gebinde_bleibt_basiseinheit(db, user):
"""Ohne (einheitliches) Gebinde bleibt es bei der Basiseinheit kein Rateraten."""
gruppe = Group(name="Mehl", min_stock=500, min_stock_unit=_gramm(db))
db.add(gruppe)
db.add(Product(name="Mehl 405", base_unit=BaseUnit.gram, package_size=None, group=gruppe))
db.commit()
(item,) = group_shopping_list(db=db, _=user)
assert item.need is not None
assert item.need.is_package is False
assert item.need.text == "500 Gramm"
assert item.need.hint == ""

View File

@@ -0,0 +1,24 @@
import { fmt, gebinde } from "../units";
/**
* Einheitliche Bedarfszeile der Einkaufsliste: die Gebinde-Leitangabe groß
* ("3 Gläser"), die Basiseinheit klein daneben ("(500 g)"), dann Bestand/min in
* derselben Leitangabe. Die Rechnung (Aufrunden auf ganze Packungen,
* Pluralisierung des Leitworts) kommt fertig vom Server als `need`.
*/
export default function ShoppingNeedText({ need, stock, minStock, unitName }) {
// Fallback für den (Übergangs-)Fall ohne `need` vom Server: altes Format.
if (!need) {
return (
<>fehlt <strong>{fmt(stock == null ? 0 : minStock - stock)} {unitName}</strong></>
);
}
const einheit = need.singular ? ` ${gebinde(need.min_stock, need.singular)}` : "";
return (
<>
fehlt <strong>{need.text}</strong>
{need.hint ? <> <span className="muted">({need.hint})</span></> : null}{" "}
(Bestand {fmt(need.stock)} / min {fmt(need.min_stock)}{einheit})
</>
);
}

View File

@@ -3,9 +3,10 @@ import { Link } from "react-router-dom";
import { api } from "../api"; import { api } from "../api";
import Icon from "../components/Icon"; import Icon from "../components/Icon";
import { CardSkeleton } from "../components/Skeleton"; import { CardSkeleton } from "../components/Skeleton";
import ShoppingNeedText from "../components/ShoppingNeedText";
import { useSettings } from "../settings"; import { useSettings } from "../settings";
import { import {
amountText, articlePrimary, articleSecondary, fmt, relativeExpiry, articlePrimary, articleSecondary, fmt, relativeExpiry,
} from "../units"; } from "../units";
import Bars from "./charts/Bars"; import Bars from "./charts/Bars";
import Donut from "./charts/Donut"; import Donut from "./charts/Donut";
@@ -332,10 +333,10 @@ function KarteEinkaufsliste({ props: karteProps }) {
const abhaken = (key, val) => setErledigt((e) => ({ ...e, [key]: val })); const abhaken = (key, val) => setErledigt((e) => ({ ...e, [key]: val }));
// Ein Abschnitt (Gesamt oder ein Ort) als abhakbare Liste wie die volle Seite. // Ein Abschnitt (Gesamt oder ein Ort) als abhakbare Liste wie die volle Seite.
// `prefix` macht die Schlüssel über die Abschnitte hinweg eindeutig, `prodInfo` // `prefix` macht die Schlüssel über die Abschnitte hinweg eindeutig. Die
// formatiert die Produktzeile (Gesamt trägt Gebinde/Basiseinheit, je-Ort schon // Bedarfszeile kommt vom Server fertig aufbereitet (`need`) und ist für
// eine fertige Einheit). // Produkte wie Gruppen, gesamt wie je Ort identisch aufgebaut.
const abschnitt = (gruppen, produkte, prefix, prodInfo) => ( const abschnitt = (gruppen, produkte, prefix) => (
<ul className="checklist"> <ul className="checklist">
{gruppen.map((it) => { {gruppen.map((it) => {
const key = `${prefix}g${it.group_id}`; const key = `${prefix}g${it.group_id}`;
@@ -347,8 +348,7 @@ function KarteEinkaufsliste({ props: karteProps }) {
<span className="item-name">{it.name}</span> <span className="item-name">{it.name}</span>
</label> </label>
<span className="muted small"> <span className="muted small">
fehlt <strong>{fmt(it.deficit)} {it.unit_name}</strong>{" "} <ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_name} />
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)} {it.unit_name})
</span> </span>
</li> </li>
); );
@@ -361,27 +361,21 @@ function KarteEinkaufsliste({ props: karteProps }) {
<input type="checkbox" checked={!!erledigt[key]} onChange={(e) => abhaken(key, e.target.checked)} /> <input type="checkbox" checked={!!erledigt[key]} onChange={(e) => abhaken(key, e.target.checked)} />
<span className="item-name">{it.name}</span> <span className="item-name">{it.name}</span>
</label> </label>
<span className="muted small">{prodInfo(it)}</span> <span className="muted small">
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_label} />
</span>
</li> </li>
); );
})} })}
</ul> </ul>
); );
const gesamtInfo = (it) => (
<>fehlt <strong>{amountText(it.deficit, it.package_size, it.base_unit)}</strong>{" "}
(Bestand {amountText(it.stock, it.package_size, it.base_unit)})</>
);
const ortInfo = (it) => (
<>fehlt <strong>{fmt(it.deficit)} {it.unit_label}</strong>{" "}
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)})</>
);
return ( return (
<div className="card-stack"> <div className="card-stack">
{!gesamtLeer && ( {!gesamtLeer && (
<div> <div>
<div className="muted small" style={{ fontWeight: 600 }}>Gesamt</div> <div className="muted small" style={{ fontWeight: 600 }}>Gesamt</div>
{abschnitt(daten.gruppen, daten.produkte, "", gesamtInfo)} {abschnitt(daten.gruppen, daten.produkte, "")}
</div> </div>
)} )}
{orte.map((o) => ((o.products.length || o.groups.length) ? ( {orte.map((o) => ((o.products.length || o.groups.length) ? (
@@ -389,7 +383,7 @@ function KarteEinkaufsliste({ props: karteProps }) {
<div className="muted small" style={{ fontWeight: 600 }}> <div className="muted small" style={{ fontWeight: 600 }}>
<Icon name="location" size={13} /> {o.location_name} <Icon name="location" size={13} /> {o.location_name}
</div> </div>
{abschnitt(o.groups, o.products, `l${o.location_id}`, ortInfo)} {abschnitt(o.groups, o.products, `l${o.location_id}`)}
</div> </div>
) : null))} ) : null))}
</div> </div>

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { api } from "../api"; import { api } from "../api";
import Icon from "../components/Icon"; import Icon from "../components/Icon";
import { amountText, fmt, unitShort } from "../units"; import ShoppingNeedText from "../components/ShoppingNeedText";
export default function ShoppingList() { export default function ShoppingList() {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
@@ -51,8 +51,7 @@ export default function ShoppingList() {
<span className="item-name">{it.name}</span> <span className="item-name">{it.name}</span>
</label> </label>
<span className="muted small"> <span className="muted small">
fehlt <strong>{fmt(it.deficit)} {it.unit_name}</strong>{" "} <ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_name} />
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)} {it.unit_name})
</span> </span>
</li> </li>
); );
@@ -66,8 +65,7 @@ export default function ShoppingList() {
<span className="item-name">{it.name}</span> <span className="item-name">{it.name}</span>
</label> </label>
<span className="muted small"> <span className="muted small">
fehlt <strong>{amountText(it.deficit, it.package_size, it.base_unit)}</strong>{" "} <ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.base_unit} />
(Bestand {amountText(it.stock, it.package_size, it.base_unit)})
</span> </span>
</li> </li>
); );
@@ -93,8 +91,7 @@ export default function ShoppingList() {
<span className="item-name">{it.name}</span> <span className="item-name">{it.name}</span>
</label> </label>
<span className="muted small"> <span className="muted small">
fehlt <strong>{fmt(it.deficit)} {it.unit_name}</strong>{" "} <ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_name} />
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)})
</span> </span>
</li> </li>
); );
@@ -108,8 +105,7 @@ export default function ShoppingList() {
<span className="item-name">{it.name}</span> <span className="item-name">{it.name}</span>
</label> </label>
<span className="muted small"> <span className="muted small">
fehlt <strong>{fmt(it.deficit)} {it.unit_label}</strong>{" "} <ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_label} />
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)})
</span> </span>
</li> </li>
); );