feat: Tabellen per Spaltenkopf sortierbar (A11-T4)

Neues static/sort.js sortiert Tabellen clientseitig per Klick auf den
Spaltenkopf (⇅/▲/▼): Datum TT.MM.JJJJ, deutsche Zahlen/Beträge, sonst
Text (Intl.Collator 'de'). Sortierwert optional per td[data-sort]
(Punkt-Dezimal/ISO) überschreibbar, th[data-nosort] bekommt keinen
Button, table[data-sort-mode="server"] wird übersprungen (Buchungen
sortiert serverseitig, folgt in Task 5). Bearbeiten-/Tilgungsplan-
Zeilen (tr[data-sort-follow]) wandern mit ihrer Vorzeile mit.
Initialisierung über htmx.onLoad, greift auch bei nachgeladenen
Fragmenten.

Templates entsprechend annotiert (Planung, Szenarien, Admin, Index,
Salden, Import-Liste, Buchungen); Salden-Gesamtzeile dafür in ein
tfoot verschoben. test_gui.py::test_gui_zeigt_deutsche_betragsformate
angepasst, da das neue machinenlesbare data-sort-Attribut bewusst
Punkt-Dezimal führt (Interface-Konvention) - die Prüfung auf deutsches
Format im sichtbaren Text bleibt bestehen.

JS-Unit-/DOM-Tests unter tests/js (node --test, DOM-Teil optional via
jsdom), pytest-Wrapper skip ohne node (Fable-Gate führt sie per
Podman aus). Suite: 248 passed, 3 skipped (vorher 242/2).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 19:22:34 +02:00
co-authored by Claude Opus 5.5
parent 42b410446d
commit 4458300f43
15 changed files with 399 additions and 20 deletions
+72
View File
@@ -0,0 +1,72 @@
import re
from datetime import date
from decimal import Decimal
from app.models.tables import (Account, Category, Loan, PlannedItem,
RecurringItem, Scenario, ScenarioModifier,
ScenarioPlannedItem)
def _login(client):
client.post("/login", data={"username": "admin", "password": "geheim"})
def _follow_ids(html):
return re.findall(r'<tr id="([a-z]+-(?:edit|plan)-\d+)"[^>]*data-sort-follow', html)
def test_base_bindet_sort_js_ein(client):
_login(client)
assert '<script src="/static/sort.js"></script>' in client.get("/").text
def test_planung_folgezeilen_markiert(client, db):
_login(client)
c = Category(name="K")
db.add(c)
db.flush()
db.add_all([
RecurringItem(name="R", amount=Decimal("-1.00"), rhythm="monthly", due_day=1, category_id=c.id),
PlannedItem(name="P", amount=Decimal("-1.00"), due=date(2030, 1, 1)),
Loan(name="L", principal=Decimal("100.00"), annual_rate_pct=Decimal("1"),
term_months=2, payout_date=date(2030, 1, 1), repayment_type="annuity"),
])
db.commit()
kinds = sorted(i.split("-")[0] + "-" + i.split("-")[1] for i in _follow_ids(client.get("/planung").text))
assert kinds == ["loan-edit", "loan-plan", "pln-edit", "rec-edit"]
def test_szenarien_und_admin_folgezeilen_markiert(client, db):
_login(client)
c = Category(name="K")
sc = Scenario(name="S", description="")
db.add_all([c, sc])
db.flush()
db.add_all([
ScenarioModifier(scenario_id=sc.id, target_type="category", target_id=c.id,
kind="percent", value=Decimal("10")),
ScenarioPlannedItem(scenario_id=sc.id, name="E", amount=Decimal("-1.00"), due=date(2030, 1, 1)),
])
db.commit()
assert sorted(i.rsplit("-", 1)[0] for i in _follow_ids(client.get("/szenarien").text)) == ["mod-edit", "spi-edit"]
assert [i.rsplit("-", 1)[0] for i in _follow_ids(client.get("/admin").text)] == ["cat-edit"]
def test_salden_gesamtzeile_im_tfoot(client, db):
_login(client)
db.add(Account(bank="t", iban="DE-S-1", name="Konto", type="giro"))
db.commit()
html = client.get("/salden").text
assert re.search(r'<tfoot>\s*<tr class="total-row">', html)
def test_index_aktionsspalten_ohne_sortierung(client):
_login(client)
html = client.get("/").text
assert "<th data-nosort>Umbenennen</th>" in html
assert "<th data-nosort>Anker pflegen</th>" in html
def test_buchungen_tabelle_ist_serverseitig(client):
_login(client)
assert 'data-sort-mode="server"' in client.get("/buchungen").text