From cd7ba6aa6ae5224a94751694a38ae5895a6a24039f4e9dff0f1eb3d454469014 Mon Sep 17 00:00:00 2001 From: wlfb Date: Wed, 30 Sep 2026 13:08:16 +0200 Subject: [PATCH] feat: Szenarien-Seite mit zwei Formularen, Zeitraum-Spalte und wiederkehrenden Szenario-Zahlungen (A14-T4) Co-Authored-By: Claude Opus 5.5 (1M context) --- finance/app/formats.py | 3 +- finance/app/routers/gui.py | 11 +- finance/app/static/szenarien.js | 72 +++++++++ finance/app/templates/base.html | 2 + finance/app/templates/szenarien.html | 208 +++++++++++++------------ finance/tests/js/szenarien.dom.test.js | 116 ++++++++++++++ finance/tests/test_formats.py | 5 +- finance/tests/test_gui.py | 62 +++++++- 8 files changed, 373 insertions(+), 106 deletions(-) create mode 100644 finance/app/static/szenarien.js create mode 100644 finance/tests/js/szenarien.dom.test.js diff --git a/finance/app/formats.py b/finance/app/formats.py index d22d7dc..a56938d 100644 --- a/finance/app/formats.py +++ b/finance/app/formats.py @@ -13,7 +13,8 @@ DE_LABELS = { "annuity": "Annuität", "bullet": "endfällig", "percent": "Prozent", - "absolute": "Absolut", + "absolute": "Kürzung (€)", + "set": "Neuer Betrag", "remove": "Entfällt", "ende": "Ende", } diff --git a/finance/app/routers/gui.py b/finance/app/routers/gui.py index 29aa0d3..8acce66 100644 --- a/finance/app/routers/gui.py +++ b/finance/app/routers/gui.py @@ -21,7 +21,8 @@ from app.formats import de_label, eur, pct from app.models.tables import (Account, Category, PlannedItem, ProjectionResult, RecurringItem, Scenario, ScenarioLoan, ScenarioModifier, - ScenarioPlannedItem, Transaction) + ScenarioPlannedItem, ScenarioRecurringItem, + Transaction) from app.routers.imports import list_imports as _list_imports from app.routers.imports import preview as _preview_import from app.routers.planning import list_loans as _list_loans @@ -322,11 +323,16 @@ def _scenario_rows(session: Session, scenarios: list, loans: list) -> list[dict] select(ScenarioPlannedItem) .where(ScenarioPlannedItem.scenario_id == sc.id) .order_by(ScenarioPlannedItem.due)).scalars().all() + recurring_items = session.execute( + select(ScenarioRecurringItem) + .where(ScenarioRecurringItem.scenario_id == sc.id) + .order_by(ScenarioRecurringItem.name, ScenarioRecurringItem.id)).scalars().all() rows.append({ "scenario": sc, "assigned_loan_ids": assigned_loan_ids, "modifiers": modifiers, "planned_items": planned_items, + "recurring_items": recurring_items, "result": session.get(ProjectionResult, sc.id), }) return rows @@ -378,7 +384,8 @@ def szenarien_page(request: Request, session: Session = Depends(get_session)): "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"], + "modifier_kinds": ["percent", "absolute", "set", "remove", "ende"], + "rhythms": ["monthly", "quarterly", "yearly"], }) diff --git a/finance/app/static/szenarien.js b/finance/app/static/szenarien.js new file mode 100644 index 0000000..c5e0e67 --- /dev/null +++ b/finance/app/static/szenarien.js @@ -0,0 +1,72 @@ +/* Szenarien-Seite (Ausbaustufe 14): Umschaltlogik der Formulare + * „Neue Änderung an Bestehendem" (.aenderung-form, auch in den Edit-Zeilen + * der Einträge) und „Neue Zahlung nur in diesem Szenario" (.zahlung-form). + * UX-Regel: Felder werden nur per disabled gesperrt, nie entfernt; + * gesperrte Felder tragen den Tooltip TIP. Gesperrte Felder sendet htmx + * nicht mit. + */ +(function () { + 'use strict'; + + var TIP = 'Für diese Eintragsart nicht relevant'; + var WERT_LABEL = { percent: 'Prozentsatz (%)', absolute: 'Kürzung (€)', set: 'Neuer Betrag (€)' }; + + function sperren(el, gesperrt) { + if (!el) { return; } + el.disabled = gesperrt; + if (gesperrt) { el.title = TIP; } else { el.removeAttribute('title'); } + } + + function feld(form, name) { return form.querySelector('[name="' + name + '"]'); } + + function beschriften(form, cls, text) { + var el = form.querySelector('.' + cls); + if (el) { el.textContent = text; } + } + + // Ziel-Typ: passende Auswahl (Kategorie oder Posten) zeigen und freigeben. + function zielTyp(select) { + var form = select.closest('form'); + var istKategorie = select.value === 'category'; + var kat = form.querySelector('.mod-target-category'); + var rec = form.querySelector('.mod-target-recurring'); + kat.style.display = istKategorie ? '' : 'none'; + rec.style.display = istKategorie ? 'none' : ''; + sperren(kat.querySelector('select'), !istKategorie); + sperren(rec.querySelector('select'), istKategorie); + } + + function aenderungArt(select) { + var form = select.closest('form'); + var art = select.value; + var mitWert = Object.prototype.hasOwnProperty.call(WERT_LABEL, art); + sperren(feld(form, 'value'), !mitWert); + beschriften(form, 'value-label', mitWert ? WERT_LABEL[art] : 'Wert'); + sperren(feld(form, 'start_date'), art === 'ende'); + beschriften(form, 'bis-label', art === 'ende' ? 'Endet am' : 'Gültig bis'); + var typ = feld(form, 'target_type'); + var kategorieOption = typ.querySelector('option[value="category"]'); + // „Neuer Betrag" gibt es nur für Posten. + if (art === 'set') { typ.value = 'recurring'; } + kategorieOption.disabled = art === 'set'; + zielTyp(typ); + } + + function zahlungArt(select) { + var form = select.closest('form'); + var einmal = select.value === 'einmal'; + sperren(feld(form, 'due'), !einmal); + ['rhythm', 'due_day', 'start_date', 'end_date'].forEach(function (name) { + sperren(feld(form, name), einmal); + }); + form.setAttribute('hx-post', form.getAttribute(einmal ? 'data-planned-url' : 'data-recurring-url')); + if (window.htmx) { window.htmx.process(form); } + } + + function init(root) { + root.querySelectorAll('.aenderung-form select[name="kind"]').forEach(aenderungArt); + root.querySelectorAll('.zahlung-form select.zahlung-art').forEach(zahlungArt); + } + + window.FBSzenario = { zielTyp: zielTyp, aenderungArt: aenderungArt, zahlungArt: zahlungArt, init: init }; +})(); diff --git a/finance/app/templates/base.html b/finance/app/templates/base.html index 0f470cd..cfed846 100644 --- a/finance/app/templates/base.html +++ b/finance/app/templates/base.html @@ -8,6 +8,7 @@ +