From 9b0771ae0ef910e037932ff30fc41ac03fe17e29 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 30 Jul 2026 17:39:30 +0200 Subject: [PATCH] Kategorien-Anteile: einstellbare Kategorie-Tiefe (Unterkategorien zusammenfassen) /dashboard/by-category bekommt depth: rollt jede Artikel-Kategorie auf ihren Vorfahren der gewuenschten Stufe hoch (1 = oberste Ebene), ohne depth zaehlt jede zugeordnete Kategorie einzeln (feinste). Neue Karten-Konfig select Kategorie-Tiefe (Feinste / oberste / bis 2. / bis 3. Stufe). KartenConfig zeigt jetzt auch bei select den Hinweis. Co-Authored-By: Claude Opus 4.8 --- backend/app/routers/dashboard.py | 36 ++++++++++++++++++++++++++---- web/src/api.js | 2 +- web/src/dashboard/KartenConfig.jsx | 2 +- web/src/dashboard/cards.jsx | 16 +++++++++++-- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index 5819936..7f6bbd3 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -386,17 +386,45 @@ def expiry_split( @router.get("/by-category", response_model=list[CategoryShare]) def by_category( - db: Session = Depends(get_db), _: User = Depends(get_current_user) + depth: int | None = None, + db: Session = Depends(get_db), + _: User = Depends(get_current_user), ) -> list[CategoryShare]: - """Artikeleinheiten je Kategorie, zusätzlich nach Ablaufzustand aufgeteilt.""" + """Artikeleinheiten je Kategorie, zusätzlich nach Ablaufzustand aufgeteilt. + + ``depth`` fasst tiefe Unterkategorien zu ihrer Ober­kategorie auf der + gewünschten Stufe zusammen (1 = oberste Ebene). Ohne ``depth`` zählt jede + Kategorie so, wie sie dem Artikel zugeordnet ist (feinste Auflösung). + """ heute = date.today() warnfrist = get_expiry_warning_days(db) - namen = {c.id: c.name for c in db.query(Category).all()} + kategorien = db.query(Category).all() + namen = {c.id: c.name for c in kategorien} + eltern = {c.id: c.parent_id for c in kategorien} + + # Kategorie auf die gewünschte Tiefe hochrollen (Vorfahr auf Stufe ``depth``). + rollup_cache: dict[int | None, int | None] = {} + + def rollup(cid: int | None) -> int | None: + if depth is None or cid is None: + return cid + if cid in rollup_cache: + return rollup_cache[cid] + pfad: list[int] = [] + cur, gesehen = cid, set() + while cur is not None and cur not in gesehen: + pfad.append(cur) + gesehen.add(cur) + cur = eltern.get(cur) + pfad.reverse() # Wurzel zuerst + ziel = pfad[min(max(depth, 1) - 1, len(pfad) - 1)] + rollup_cache[cid] = ziel + return ziel 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 product, menge, zustand in _bestand_beitraege(db, heute, warnfrist): - topf = eimer[product.category_id] + topf = eimer[rollup(product.category_id)] topf["article_units"] += menge topf[zustand] += menge diff --git a/web/src/api.js b/web/src/api.js index 41e612d..63acf73 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -229,7 +229,7 @@ export const api = { request(`/dashboard/layout/enforced?value=${value ? "true" : "false"}`, { method: "PUT" }), dashboardStats: () => request("/dashboard/stats"), dashboardExpirySplit: () => request("/dashboard/expiry-split"), - dashboardByCategory: () => request("/dashboard/by-category"), + dashboardByCategory: (depth) => request(`/dashboard/by-category${depth ? `?depth=${depth}` : ""}`), dashboardTimeline: (days, productId) => request(`/dashboard/timeline?days=${days}${productId ? `&product_id=${productId}` : ""}`), dashboardActivity: (days) => request(`/dashboard/activity?days=${days}`), diff --git a/web/src/dashboard/KartenConfig.jsx b/web/src/dashboard/KartenConfig.jsx index 8088c78..c43c804 100644 --- a/web/src/dashboard/KartenConfig.jsx +++ b/web/src/dashboard/KartenConfig.jsx @@ -52,7 +52,7 @@ export default function KartenConfig({ if (f.type === "select") { return (