diff --git a/finance/app/routers/gui.py b/finance/app/routers/gui.py index 7c79b2d..b5b71ed 100644 --- a/finance/app/routers/gui.py +++ b/finance/app/routers/gui.py @@ -291,7 +291,6 @@ def planung_page(request: Request, session: Session = Depends(get_session)): recurring = _list_recurring(session=session) planned = _list_planned(session=session) loans = _list_loans(session=session) - scenarios = _list_scenarios(session=session) return templates.TemplateResponse(request, "planning.html", { "categories": categories, "category_names": {c.id: c.name for c in categories}, @@ -300,8 +299,23 @@ def planung_page(request: Request, session: Session = Depends(get_session)): "suggestions": _recurring_suggestions(session=session), "planned": planned, "loans": loans, - "scenario_rows": _scenario_rows(session, scenarios, loans), "rhythms": ["monthly", "quarterly", "yearly"], "repayment_types": ["annuity", "bullet"], + }) + + +@router.get("/szenarien", dependencies=[Depends(gui_session)]) +def szenarien_page(request: Request, session: Session = Depends(get_session)): + categories = session.execute(select(Category)).scalars().all() + recurring = _list_recurring(session=session) + loans = _list_loans(session=session) + scenarios = _list_scenarios(session=session) + return templates.TemplateResponse(request, "szenarien.html", { + "categories": categories, + "category_names": {c.id: c.name for c in categories}, + "recurring": recurring, + "recurring_names": {r.id: r.name for r in recurring}, + "loans": loans, + "scenario_rows": _scenario_rows(session, scenarios, loans), "modifier_kinds": ["percent", "absolute", "remove", "ende"], }) diff --git a/finance/app/templates/base.html b/finance/app/templates/base.html index 431ed95..c334915 100644 --- a/finance/app/templates/base.html +++ b/finance/app/templates/base.html @@ -14,6 +14,7 @@ Buchungen Salden Planung + Szenarien Admin Hilfe Grafana @@ -74,6 +75,12 @@ return JSON.stringify(out); } }); + + function toggleEdit(prefix, id, editing) { + // UX-Regel: Zeilen werden versteckt (hidden), nie aus dem DOM entfernt. + document.getElementById(prefix + '-row-' + id).hidden = editing; + document.getElementById(prefix + '-edit-' + id).hidden = !editing; + } diff --git a/finance/app/templates/planning.html b/finance/app/templates/planning.html index 2ac17e5..9db9ce9 100644 --- a/finance/app/templates/planning.html +++ b/finance/app/templates/planning.html @@ -260,278 +260,11 @@ -
-

Szenarien

- {% for row in scenario_rows %} - {% set sc = row.scenario %} -
- {{ sc.name }} -

- {{ sc.description }} - · Wiederkehrende Posten: {{ 'ja' if sc.include_recurring else 'nein' }} - · Einmalposten: {{ 'ja' if sc.include_planned else 'nein' }} - -

- - -
- Kredite zuordnen - {% for l in loans %} - - {% else %} -

Keine Kredite vorhanden.

- {% endfor %} -
- -

Einträge

- - - - - - {% for m in row.modifiers %} - - - - - - - - - - - {% endfor %} - {% for sp in row.planned_items %} - - - - - - - - - - - {% endfor %} - {% if not row.modifiers and not row.planned_items %} - - {% endif %} - -
WasArtWert/BetragDatum
- {% if m.target_type == "category" %}Kategorie: {{ category_names.get(m.target_id, m.target_id) }} - {% else %}Posten: {{ recurring_names.get(m.target_id, m.target_id) }}{% endif %} - {{ m.kind|de_label }}{% if m.kind in ('percent', 'absolute') %}{{ m.value|eur }}{% else %}–{% endif %}{% if m.kind == 'ende' %}{{ m.end_date.strftime('%d.%m.%Y') if m.end_date else '–' }}{% else %}–{% endif %} - -
- -
-
{{ sp.name }}Einmalzahlung{{ sp.amount|eur }} €{{ sp.due.strftime('%d.%m.%Y') }} - -
- -
-
Keine Einträge.
- -
- Neuer Eintrag: - - - - - - - - - - - - -
- -
- -
- - {% if row.result %} -

- Tiefpunkt: {{ row.result.low_point_balance|eur }} € am {{ row.result.low_point_date.strftime('%d.%m.%Y') }}
- {% if row.result.below_zero_date %}Unterschreitet 0 € ab {{ row.result.below_zero_date.strftime('%d.%m.%Y') }}
{% endif %} - {% if row.result.below_threshold_date %}Unterschreitet Warnschwelle ab {{ row.result.below_threshold_date.strftime('%d.%m.%Y') }}
{% endif %} - Kurven in Grafana ansehen. -

- {% else %} -

Noch nicht durchgerechnet.

- {% endif %} -
- {% else %} -

Noch keine Szenarien.

- {% endfor %} - -
- Neues Szenario anlegen -
- - - - - -
-
-
- +{% endblock %} diff --git a/finance/tests/test_gui.py b/finance/tests/test_gui.py index 76a2fcc..0173b13 100644 --- a/finance/tests/test_gui.py +++ b/finance/tests/test_gui.py @@ -9,7 +9,7 @@ H = {"Authorization": "Bearer test-key"} def test_pages_require_login(client): - for path in ("/", "/buchungen", "/import", "/salden", "/planung", "/hilfe"): + for path in ("/", "/buchungen", "/import", "/salden", "/planung", "/szenarien", "/hilfe"): r = client.get(path, follow_redirects=False) assert r.status_code in (302, 303), path assert r.headers["location"] == "/login" @@ -17,7 +17,7 @@ def test_pages_require_login(client): def test_pages_render_after_login(client): client.post("/login", data={"username": "admin", "password": "geheim"}) - for path in ("/", "/buchungen", "/import", "/salden", "/planung", "/hilfe"): + for path in ("/", "/buchungen", "/import", "/salden", "/planung", "/szenarien", "/hilfe"): r = client.get(path) assert r.status_code == 200 assert "Finanzberatung" in r.text @@ -317,15 +317,23 @@ def test_planung_hat_bearbeiten_formulare(client, db): db.add(Loan(name="EditL", principal=Decimal("1000.00"), annual_rate_pct=Decimal("4.50"), term_months=12, payout_date=date(2026, 8, 1), repayment_type="annuity")) - db.add(Scenario(name="EditS", description="d")) db.commit() r = client.get("/planung").text - assert r.count(">Bearbeiten") >= 4 + assert r.count(">Bearbeiten") >= 3 for fragment in ('hx-patch="/api/recurring/', 'hx-patch="/api/planned/', - 'hx-patch="/api/loans/', 'hx-patch="/api/scenarios/'): + 'hx-patch="/api/loans/'): assert fragment in r, fragment +def test_szenarien_hat_bearbeiten_formular(client, db): + client.post("/login", data={"username": "admin", "password": "geheim"}) + db.add(Scenario(name="EditS", description="d")) + db.commit() + r = client.get("/szenarien").text + assert r.count(">Bearbeiten") >= 1 + assert 'hx-patch="/api/scenarios/' in r + + def test_salden_shows_ohne_anker_footnote(client, db): # UX-Regel: Konten ohne Anker sind immer sichtbar als relative Werte # gekennzeichnet, nicht stillschweigend wie geankerte Konten dargestellt. @@ -350,7 +358,7 @@ def test_szenario_gui_ende_und_einmalzahlungen(client, db): db.add(ScenarioPlannedItem(scenario_id=sc.id, name="Zufluss-GUI", amount=Decimal("5000.00"), due=date(2026, 7, 30))) db.commit() - r = client.get("/planung").text + r = client.get("/szenarien").text assert 'value="ende"' in r # neue Art im Dropdown assert 'name="end_date"' in r # Datumsfeld im Modifikator-Formular assert "31.08.2026" in r # ende-Modifikator zeigt Datum @@ -372,7 +380,7 @@ def test_szenario_eintraege_tabelle(client, db): db.add(ScenarioPlannedItem(scenario_id=sc.id, name="Zufluss-Eintrag", amount=Decimal("5000.00"), due=date(2026, 7, 30))) db.commit() - r = client.get("/planung").text + r = client.get("/szenarien").text assert "Einträge" in r # alte details-Abschnitte sind ersetzt assert "Modifikatoren" not in r @@ -387,3 +395,22 @@ def test_szenario_eintraege_tabelle(client, db): assert 'value="einmal"' in r assert f'data-planned-url="/api/scenarios/{sc.id}/planned"' in r assert "onEntryArtChange" in r and "onModKindChange" not in r + + +def test_szenarien_seite_und_nav(client, db): + client.post("/login", data={"username": "admin", "password": "geheim"}) + db.add(Scenario(name="Seiten-Test", description="d")) + db.commit() + r = client.get("/szenarien") + assert r.status_code == 200 + assert "Seiten-Test" in r.text and "Neues Szenario anlegen" in r.text + assert 'href="/szenarien"' in r.text # Nav-Punkt + # Planung enthaelt keine Szenarien-Sektion mehr: + p = client.get("/planung").text + assert "Seiten-Test" not in p and "Neues Szenario anlegen" not in p + assert "Wiederkehrende Posten" in p and "Kredite" in p + + +def test_szenarien_seite_braucht_login(client): + r = client.get("/szenarien", follow_redirects=False) + assert r.status_code in (302, 303) and r.headers["location"] == "/login"