From 5e70644f73be648e5b05270ebedf58106ef26491 Mon Sep 17 00:00:00 2001 From: Guido Date: Sun, 30 Aug 2026 14:05:28 +0200 Subject: [PATCH] feat: Ausgaben-Anteile faerben nach Budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Karte zeigte nur, wie sich die Ausgaben verteilen – nicht, ob sie im Rahmen liegen. Der Betrag ist jetzt gruen, solange er unter dem Geplanten bleibt, und rot darueber. Dieselben Farben wie ueberall sonst fuer Einnahme und Ausgabe. Ohne Plan bleibt der Betrag gedeckt. "Ueber Budget" waere sonst die Aussage ueber jede Kategorie, fuer die nie eine Vorgabe gemacht wurde – die Karte stuende dann fast durchgehend auf Rot und sagte nichts mehr. Das Geplante kommt aus derselben Quelle wie im Budget-Fortschritt, also einschliesslich eines Monats-Werts, der die Vorlage uebersteuert. Co-Authored-By: Claude Opus 5 --- backend/app/routers/dashboard.py | 9 ++++++- backend/tests/test_budget.py | 42 ++++++++++++++++++++++++++++++++ web/src/dashboard/cards.jsx | 8 +++++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index 5c46878..db44ebd 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -158,13 +158,20 @@ def expense_share( label = _label(db, user) start, end = budget.period_bounds(label, _start_day(db, user)) actual = budget.actual_expense_by_category(db, user, start, end, base) + # Das Geplante kommt mit, damit die Karte zeigen kann, ob ein Anteil noch + # im Rahmen liegt. Ohne Plan wird nichts behauptet: „ueber Budget" waere + # sonst die Aussage ueber jede Kategorie, fuer die nie eine gemacht wurde. + planned = budget.effective_planned(db, user, label) by_category = group_by != "group" rows = [] for name, color, ids in _units(_expense_groups(db, user), by_category): val = sum(actual.get(i, 0) for i in ids) if val > 0: - rows.append({"name": name, "color": color, "amount": val}) + geplant = sum(planned.get(i, 0) for i in ids) + rows.append({"name": name, "color": color, "amount": val, + "planned": geplant, + "over": geplant > 0 and val > geplant}) rows.sort(key=lambda r: r["amount"], reverse=True) return { "rows": rows, diff --git a/backend/tests/test_budget.py b/backend/tests/test_budget.py index 37f5132..89ca057 100644 --- a/backend/tests/test_budget.py +++ b/backend/tests/test_budget.py @@ -167,3 +167,45 @@ def test_budget_monate_stossen_lueckenlos_aneinander(): f"Starttag {start_day}: {label} endet {bis}, " f"{naechstes} beginnt {von_naechstes}") label = naechstes + + +def test_ausgabenanteile_nennen_das_geplante(admin): + """Die Karte soll zeigen, ob ein Anteil noch im Rahmen liegt. Ohne Plan + wird nichts behauptet – 'ueber Budget' waere sonst die Aussage ueber jede + Kategorie, fuer die nie eine Vorgabe gemacht wurde.""" + import datetime + + leb = _lebensmittel(admin) + name = next(c["name"] for g in admin.get("/api/categories").json() + for c in g["categories"] if c["id"] == leb) + konto = admin.post("/api/accounts", json={ + "name": "Anteile", "type": "girokonto", "currency": "EUR", + "opening_balance_cents": 0}).json() + admin.post("/api/transactions", json={ + "type": "expense", "date": datetime.date.today().isoformat(), + "account_id": konto["id"], "amount_cents": 60000, "category_id": leb}) + + # Ueber den MONATS-Wert, nicht die Vorlage: Ein vorhandener Monats-Wert + # aus einem anderen Test uebersteuert die Vorlage sonst. + label = admin.get("/api/budget/current").json()["label"] + + def plane(cents): + r = admin.put(f"/api/budget/{label}/category/{leb}", + params={"planned_cents": cents}) + assert r.status_code == 200, r.text + + def zeile(): + rows = admin.get("/api/dashboard/expense-share").json()["rows"] + return next(r for r in rows if r["name"] == name) + + ist = zeile()["amount"] + + plane(0) + assert zeile()["planned"] == 0 + assert zeile()["over"] is False, "ohne Plan darf nichts als ueberschritten gelten" + + plane(ist + 10000) + assert zeile()["over"] is False, "im Rahmen" + + plane(max(1, ist - 10000)) + assert zeile()["over"] is True, "ueberschritten" diff --git a/web/src/dashboard/cards.jsx b/web/src/dashboard/cards.jsx index 54d0e37..55fb723 100644 --- a/web/src/dashboard/cards.jsx +++ b/web/src/dashboard/cards.jsx @@ -178,7 +178,13 @@ function ExpenseShare({ props }) { ● {r.name} {pct.toFixed(1).replace(".", ",")} % - {formatMoney(r.amount, data.currency)} + {/* Grün im geplanten Rahmen, rot darüber – dieselben Farben wie + überall sonst für Einnahme und Ausgabe. Ohne Plan bleibt der + Betrag gedeckt: Da gibt es nichts zu überschreiten. */} + 0 ? (r.over ? "neg" : "pos") : "muted"}`}> + {formatMoney(r.amount, data.currency)} +