feat: Dashboard-Karte „Ausgaben-Anteile"

Zeigt für den laufenden Budget-Monat, wie sich die tatsächlichen Ausgaben
prozentual auf die Kategorien verteilen – absteigend sortiert, mit Balken in der
Kategoriefarbe, Prozentwert und Betrag.

Anders als der Budget-Donut hängt sie nicht am geplanten Budget: Sie zeigt reine
Ist-Ausgaben, auch wenn für den Monat nichts geplant wurde. Umschaltbar zwischen
Kategorien (Standard) und Gruppen.

Die Anteile rechnet die Oberfläche aus Betrag und Summe – gerundete Prozente vom
Server addierten sich sonst nicht auf 100 %. Wird die Karte schmal, gibt zuerst
der Betrag nach; ohne Kategorienamen sagt die Zeile nichts mehr aus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 09:30:14 +02:00
co-authored by Claude Opus 4.8
parent 8d767e728a
commit e10ccdf185
6 changed files with 95 additions and 2 deletions
+33
View File
@@ -142,6 +142,39 @@ def budget_donut(
"label": label, "group_by": "category" if by_category else "group"}
@router.get("/expense-share")
def expense_share(
group_by: str = Query(default="category"),
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> dict:
"""Tatsächliche Ausgaben des laufenden Budget-Monats je Kategorie/Gruppe.
Anders als der Budget-Donut zeigt das rein die Ist-Ausgaben – unabhängig
davon, ob für den Monat überhaupt geplant wurde. Die Anteile rechnet die
Oberfläche aus `amount` und `total`, damit sich gerundete Prozente nicht zu
99,9 % addieren."""
base = base_currency(db, user)
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)
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})
rows.sort(key=lambda r: r["amount"], reverse=True)
return {
"rows": rows,
"total": sum(r["amount"] for r in rows),
"currency": base,
"label": label,
"group_by": "category" if by_category else "group",
}
@router.get("/budget-progress")
def budget_progress(
group_by: str = Query(default="group"),