From 06392c0fecba453cce0f7ea7301814185b3ad254176093170d23545a531e99af Mon Sep 17 00:00:00 2001 From: wlfb Date: Sun, 19 Jul 2026 17:09:35 +0200 Subject: [PATCH] fix: tolerante GUI-Filter, sichtbare Bedienelemente, fixierte Navigation Co-Authored-By: Claude Fable 5 --- finance/app/routers/gui.py | 38 ++++++++++++++++++++--- finance/app/static/style.css | 9 ++++++ finance/app/templates/_import_list.html | 5 ++++ finance/app/templates/planning.html | 6 ++-- finance/tests/test_gui.py | 40 +++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/finance/app/routers/gui.py b/finance/app/routers/gui.py index ad10a97..80b8213 100644 --- a/finance/app/routers/gui.py +++ b/finance/app/routers/gui.py @@ -62,6 +62,28 @@ def _latest_projection(session: Session) -> ProjectionResult | None: ).scalars().first() +def _opt_int(value: str | None) -> int | None: + """Wandelt einen GUI-Query-Parameter tolerant in int|None um. HTML-Formulare + senden bei nicht ausgefüllten Feldern leere Strings statt gar keinen + Parameter - das darf nie zu einem 422 fuehren, sondern degradiert zu None.""" + if value is None or not value.strip(): + return None + try: + return int(value) + except ValueError: + return None + + +def _opt_date(value: str | None) -> date | None: + """Wie _opt_int, nur fuer date-Felder (ISO-Format aus ).""" + if value is None or not value.strip(): + return None + try: + return date.fromisoformat(value) + except ValueError: + return None + + @router.get("/", dependencies=[Depends(gui_session)]) def index(request: Request, session: Session = Depends(get_session)): accounts = session.execute(select(Account)).scalars().all() @@ -77,15 +99,23 @@ def index(request: Request, session: Session = Depends(get_session)): @router.get("/buchungen", dependencies=[Depends(gui_session)]) def buchungen( request: Request, - account_id: int | None = None, - date_from: date | None = None, - date_to: date | None = None, - category_id: int | None = None, + account_id: str | None = None, + date_from: str | None = None, + date_to: str | None = None, + category_id: str | None = None, q: str | None = None, status: str = "confirmed", page: int = 1, session: Session = Depends(get_session), ): + # HTML-Filterformular sendet leere Strings fuer nicht ausgefuellte Felder - + # daher hier bewusst str|None statt int|None/date|None und tolerante + # Konvertierung, statt FastAPI vor dem Handler mit 422 abbrechen zu lassen. + # Die API-Routen (/api/transactions) bleiben strikt typisiert. + account_id = _opt_int(account_id) + date_from = _opt_date(date_from) + date_to = _opt_date(date_to) + category_id = _opt_int(category_id) page = max(1, page) total = count_transactions( account_id=account_id, diff --git a/finance/app/static/style.css b/finance/app/static/style.css index b6fdc8b..9d8c603 100644 --- a/finance/app/static/style.css +++ b/finance/app/static/style.css @@ -16,6 +16,10 @@ nav { border-bottom: 1px solid #ccc; padding-bottom: 0.5rem; margin-bottom: 1rem; + position: sticky; + top: 0; + z-index: 10; + background: #fff; } nav a { @@ -102,6 +106,11 @@ button { cursor: pointer; } +button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + .badge { display: inline-block; padding: 0.15rem 0.6rem; diff --git a/finance/app/templates/_import_list.html b/finance/app/templates/_import_list.html index 1b1c525..6e302e9 100644 --- a/finance/app/templates/_import_list.html +++ b/finance/app/templates/_import_list.html @@ -39,19 +39,24 @@ hx-on::after-request="if(event.detail.successful){htmx.trigger(document.getElementById('imports'), 'refresh')}"> + {% elif s.status == "confirmed" %} Bestätigt — Buchungen übernommen + +
{% else %} +
+ {% endif %} diff --git a/finance/app/templates/planning.html b/finance/app/templates/planning.html index b80d47e..2877281 100644 --- a/finance/app/templates/planning.html +++ b/finance/app/templates/planning.html @@ -53,9 +53,9 @@ - {% if suggestions %}
Vorschläge aus Buchungen + {% if suggestions %} @@ -82,8 +82,10 @@ {% endfor %}
NameBetragRhythmusFälligkeitstag
+ {% else %} +

Keine Vorschläge — erkannt werden Serien aus mindestens 3 Monaten gleichartiger Buchungen.

+ {% endif %}
- {% endif %}
diff --git a/finance/tests/test_gui.py b/finance/tests/test_gui.py index 577946f..407af09 100644 --- a/finance/tests/test_gui.py +++ b/finance/tests/test_gui.py @@ -78,6 +78,46 @@ def test_buchungen_pagination(client, db): assert "MARKER51" in r2.text +def test_buchungen_empty_filter_fields_no_422(client): + # HTML-Formulare senden bei nicht ausgefüllten Feldern leere Strings, nicht + # "kein Parameter" — die GUI-Route muss das tolerant behandeln statt mit + # einem rohen 422 zu scheitern. + client.post("/login", data={"username": "admin", "password": "geheim"}) + r = client.get( + "/buchungen" + "?account_id=2&date_from=&date_to=&category_id=&q=&status=confirmed" + ) + assert r.status_code == 200 + + +def test_buchungen_filled_filters_scope_to_account(client, db): + from app.models.tables import Account, Transaction + + acc_a = Account(bank="dkb", iban="DE01", name="Konto A", type="giro") + acc_b = Account(bank="dkb", iban="DE02", name="Konto B", type="giro") + db.add_all([acc_a, acc_b]) + db.flush() + db.add(Transaction( + account_id=acc_a.id, booking_date=date(2026, 6, 15), + amount=Decimal("10.00"), purpose="MARKER-A", counterparty="X", + status="confirmed", dedup_hash="hash-a", + )) + db.add(Transaction( + account_id=acc_b.id, booking_date=date(2026, 6, 15), + amount=Decimal("20.00"), purpose="MARKER-B", counterparty="Y", + status="confirmed", dedup_hash="hash-b", + )) + db.commit() + + client.post("/login", data={"username": "admin", "password": "geheim"}) + r = client.get( + f"/buchungen?account_id={acc_a.id}&date_from=2026-06-01&date_to=2026-06-30" + ) + assert r.status_code == 200 + assert "MARKER-A" in r.text + assert "MARKER-B" not in r.text + + def test_planning_page_has_all_sections(client): client.post("/login", data={"username": "admin", "password": "geheim"}) r = client.get("/planung")