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
+9 -9
View File
@@ -13,7 +13,7 @@
{% for r in recurring %}
<tr id="rec-row-{{ r.id }}">
<td>{{ r.name }}</td>
<td class="{{ 'neg' if r.amount < 0 else '' }}">{{ r.amount|eur }} €</td>
<td class="{{ 'neg' if r.amount < 0 else '' }}" data-sort="{{ r.amount }}">{{ r.amount|eur }} €</td>
<td>{{ r.rhythm|de_label }}</td>
<td>{{ r.due_day }}</td>
<td>{{ r.start_date.strftime('%d.%m.%Y') if r.start_date else '–' }}</td>
@@ -28,7 +28,7 @@
</form>
</td>
</tr>
<tr id="rec-edit-{{ r.id }}" hidden>
<tr id="rec-edit-{{ r.id }}" hidden data-sort-follow>
<td colspan="8">
<form hx-ext="json-form" hx-patch="/api/recurring/{{ r.id }}" hx-swap="none"
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
@@ -94,7 +94,7 @@
{% for s in suggestions %}
<tr>
<td>{{ s.name }}</td>
<td class="{{ 'neg' if s.amount < 0 else '' }}">
<td class="{{ 'neg' if s.amount < 0 else '' }}" data-sort="{{ s.amount }}">
{{ s.amount|eur }} €
{% if s.hinweis %}<span class="muted">{{ s.hinweis }}</span>{% endif %}
</td>
@@ -134,7 +134,7 @@
{% for p in planned %}
<tr id="pln-row-{{ p.id }}">
<td>{{ p.name }}</td>
<td class="{{ 'neg' if p.amount < 0 else '' }}">{{ p.amount|eur }} €</td>
<td class="{{ 'neg' if p.amount < 0 else '' }}" data-sort="{{ p.amount }}">{{ p.amount|eur }} €</td>
<td>{{ p.due.strftime('%d.%m.%Y') }}</td>
<td>{{ category_names.get(p.category_id, '–') }}</td>
<td>
@@ -146,7 +146,7 @@
</form>
</td>
</tr>
<tr id="pln-edit-{{ p.id }}" hidden>
<tr id="pln-edit-{{ p.id }}" hidden data-sort-follow>
<td colspan="5">
<form hx-ext="json-form" hx-patch="/api/planned/{{ p.id }}" hx-swap="none"
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
@@ -198,9 +198,9 @@
{% for l in loans %}
<tr id="loan-row-{{ l.id }}">
<td>{{ l.name }}</td>
<td>{{ l.principal|eur }} €</td>
<td data-sort="{{ l.principal }}">{{ l.principal|eur }} €</td>
<td>{{ l.annual_rate_pct|eur }} %</td>
<td>{{ l.term_months }} Monate</td>
<td data-sort="{{ l.term_months }}">{{ l.term_months }} Monate</td>
<td>{{ l.payout_date.strftime('%d.%m.%Y') }}</td>
<td>{{ l.repayment_type|de_label }}</td>
<td>
@@ -214,7 +214,7 @@
</tr>
<!-- Hinweis: eine Kredit-Aenderung veraendert den Tilgungsplan; danach
muessen betroffene Szenarien neu durchgerechnet werden. -->
<tr id="loan-edit-{{ l.id }}" hidden>
<tr id="loan-edit-{{ l.id }}" hidden data-sort-follow>
<td colspan="7">
<form hx-ext="json-form" hx-patch="/api/loans/{{ l.id }}" hx-swap="none"
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
@@ -233,7 +233,7 @@
</form>
</td>
</tr>
<tr>
<tr id="loan-plan-{{ l.id }}" data-sort-follow>
<td colspan="7">
<details ontoggle="if(this.open){loadLoanSchedule({{ l.id }})}">
<summary>Tilgungsplan</summary>