Dashboard zaehlt Einzelstuecke mit; Baum-Dropdown scrollbar

Auswertungen (by_category, expiry_split, stats) iterierten nur Lots. Gegenstaende
mit Menge-je-Lagerort liegen als Lots und tauchten auf – Einzelstuecke liegen
aber als Items und fehlten dadurch komplett (Dashboard zeigte z.B. nur die
Powerbank, nicht Kamera/Kabel). Neuer Helfer _bestand_beitraege deckt beide
Speicherformen ab: jedes Item zaehlt als ein Stueck ohne MHD. Regressionstest
ergaenzt; volle Suite gruen (120).

CategorySelect: Scrollen IM aufklappbaren Menue schloss es, weil jeder Scroll als
Seiten-Scroll gewertet wurde. Jetzt schliesst nur ein Scroll ausserhalb des
Menues; die lange Liste laesst sich scrollen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-26 12:09:25 +02:00
parent f35126cb7b
commit a70a99b387
3 changed files with 75 additions and 19 deletions

View File

@@ -24,6 +24,7 @@ from ..models import (
Category,
DashboardLayout,
Group,
Item,
Lot,
Movement,
MovementType,
@@ -300,6 +301,35 @@ def _lots_mit_produkt(db: Session) -> list[tuple[Lot, Product]]:
)
def _bestand_beitraege(
db: Session, heute: date, warnfrist: int
) -> list[tuple[Product, float, str]]:
"""Alle Bestands-Beiträge als (Produkt, Artikeleinheiten, Ablaufzustand).
Deckt beide Speicherformen ab, damit die Auswertungen vollständig sind:
- Chargen (Lebensmittel und Mengen-Gegenstände) je Lot mit ihrem MHD-Zustand.
- Einzelstücke: jedes Item zählt als ein Stück ohne MHD ("no_date"). Diese
liegen als Items und nicht als Lots vor ohne sie fehlten Einzelstück-
Produkte (z.B. eine Kamera) in jeder Kategorie-/Ablauf-Auswertung.
"""
beitraege: list[tuple[Product, float, str]] = []
for lot, product in _lots_mit_produkt(db):
# Einzelstücke werden über ihre Items gezählt (siehe unten), nie über Lots.
if product.individual:
continue
faktor, _label = article_unit(product)
menge = lot.quantity / (faktor or 1.0)
beitraege.append((product, menge, _zustand(lot.best_before, heute, warnfrist)))
for product in db.query(Product).filter(Product.individual.is_(True)).all():
anzahl = db.query(Item).filter(Item.product_id == product.id).count()
if anzahl <= 0:
continue
faktor, _label = article_unit(product)
beitraege.append((product, anzahl / (faktor or 1.0), "no_date"))
return beitraege
@router.get("/stats", response_model=DashboardStats)
def stats(
db: Session = Depends(get_db), _: User = Depends(get_current_user)
@@ -310,11 +340,9 @@ def stats(
einheiten = 0.0
bestand_produkte: set[int] = set()
bald = abgelaufen = 0
for lot, product in _lots_mit_produkt(db):
faktor, _label = article_unit(product)
einheiten += lot.quantity / faktor if faktor else 0.0
for product, menge, zustand in _bestand_beitraege(db, heute, warnfrist):
einheiten += menge
bestand_produkte.add(product.id)
zustand = _zustand(lot.best_before, heute, warnfrist)
if zustand == "expired":
abgelaufen += 1
elif zustand == "soon":
@@ -351,9 +379,8 @@ def expiry_split(
heute = date.today()
warnfrist = get_expiry_warning_days(db)
summen = {"ok": 0.0, "soon": 0.0, "expired": 0.0, "no_date": 0.0}
for lot, product in _lots_mit_produkt(db):
faktor, _label = article_unit(product)
summen[_zustand(lot.best_before, heute, warnfrist)] += lot.quantity / (faktor or 1.0)
for _product, menge, zustand in _bestand_beitraege(db, heute, warnfrist):
summen[zustand] += menge
return ExpirySplit(**{k: round(v, 3) for k, v in summen.items()})
@@ -368,12 +395,10 @@ def by_category(
leer = lambda: {"article_units": 0.0, "ok": 0.0, "soon": 0.0, "expired": 0.0, "no_date": 0.0}
eimer: dict[int | None, dict] = defaultdict(leer)
for lot, product in _lots_mit_produkt(db):
faktor, _label = article_unit(product)
menge = lot.quantity / (faktor or 1.0)
for product, menge, zustand in _bestand_beitraege(db, heute, warnfrist):
topf = eimer[product.category_id]
topf["article_units"] += menge
topf[_zustand(lot.best_before, heute, warnfrist)] += menge
topf[zustand] += menge
ergebnis = [
CategoryShare(