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
co-authored by Claude Opus 4.8
parent e214bb76a9
commit f2c657ff31
6 changed files with 294 additions and 29 deletions
+166 -2
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)
+21
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):
+66
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 == ""
+24
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})
</>
);
}
+12 -18
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>
+5 -9
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>
); );