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>
135 lines
4.8 KiB
HTML
135 lines
4.8 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Buchungen – Finanzberatung{% endblock %}
|
||
{% block content %}
|
||
<h1>Buchungen</h1>
|
||
|
||
<form class="filter-form" method="get" action="/buchungen">
|
||
<label>Konto
|
||
<select name="account_id">
|
||
<option value="">alle</option>
|
||
{% for a in accounts %}
|
||
<option value="{{ a.id }}" {% if filters.account_id == a.id %}selected{% endif %}>{{ a.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Von
|
||
<input type="date" name="date_from" value="{{ filters.date_from.isoformat() if filters.date_from else '' }}">
|
||
</label>
|
||
<label>Bis
|
||
<input type="date" name="date_to" value="{{ filters.date_to.isoformat() if filters.date_to else '' }}">
|
||
</label>
|
||
<label>Kategorie
|
||
<select name="category_id">
|
||
<option value="">alle</option>
|
||
{% for c in categories %}
|
||
<option value="{{ c.id }}" {% if filters.category_id == c.id %}selected{% endif %}>{{ c.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Suche
|
||
<input type="text" name="q" value="{{ filters.q }}" placeholder="Verwendungszweck / Empfänger">
|
||
</label>
|
||
<label>Status
|
||
<select name="status">
|
||
{% for s in ("confirmed", "draft") %}
|
||
<option value="{{ s }}" {% if filters.status == s %}selected{% endif %}>{{ s }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit">Filtern</button>
|
||
</form>
|
||
|
||
<table data-sort-mode="server">
|
||
<thead>
|
||
<tr>
|
||
<th>Datum</th><th>Konto</th><th>Verwendungszweck</th><th>Empfänger/Zahler</th>
|
||
<th>Betrag (€)</th><th>Kategorie</th><th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for tx in transactions %}
|
||
<tr>
|
||
<td>{{ tx.booking_date.strftime('%d.%m.%Y') }}</td>
|
||
<td class="account">{{ account_names.get(tx.account_id, tx.account_id) }}</td>
|
||
<td>{{ tx.purpose }}</td>
|
||
<td>{{ tx.counterparty }}</td>
|
||
<td class="amount {{ 'neg' if tx.amount < 0 else '' }}">{{ tx.amount|eur }}</td>
|
||
<td>
|
||
<select name="category_id" data-type="int" hx-ext="json-form"
|
||
hx-patch="/api/transactions/{{ tx.id }}" hx-trigger="change" hx-swap="none">
|
||
<option value="">–</option>
|
||
{% for c in categories %}
|
||
<option value="{{ c.id }}" {% if tx.category_id == c.id %}selected{% endif %}>{{ c.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td>
|
||
<details>
|
||
<summary>Regel erzeugen</summary>
|
||
<form hx-ext="json-form" hx-post="/api/category-rules" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){this.closest('details').open=false}">
|
||
<input type="hidden" name="pattern" value="{{ tx.counterparty }}">
|
||
<label>Kategorie
|
||
<select name="category_id" data-type="int" required>
|
||
{% for c in categories %}
|
||
<option value="{{ c.id }}" {% if tx.category_id == c.id %}selected{% endif %}>{{ c.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit">Regel speichern</button>
|
||
</form>
|
||
</details>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="7">Keine Buchungen gefunden.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
<p class="pager">
|
||
{% set pager_params = {
|
||
'account_id': filters.account_id,
|
||
'date_from': filters.date_from.isoformat() if filters.date_from else None,
|
||
'date_to': filters.date_to.isoformat() if filters.date_to else None,
|
||
'category_id': filters.category_id,
|
||
'q': filters.q,
|
||
'status': filters.status,
|
||
} %}
|
||
{% if page > 1 %}
|
||
<a href="/buchungen?{% for k, v in pager_params.items() %}{% if v %}{{ k }}={{ v|urlencode }}&{% endif %}{% endfor %}page={{ page - 1 }}">Zurück</a>
|
||
{% endif %}
|
||
Seite {{ page }} von {{ total_pages }}
|
||
{% if page < total_pages %}
|
||
<a href="/buchungen?{% for k, v in pager_params.items() %}{% if v %}{{ k }}={{ v|urlencode }}&{% endif %}{% endfor %}page={{ page + 1 }}">Weiter</a>
|
||
{% endif %}
|
||
</p>
|
||
|
||
<fieldset>
|
||
<legend>Buchung manuell erfassen</legend>
|
||
<form hx-ext="json-form" hx-post="/api/transactions" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){this.reset(); window.location.reload()}">
|
||
<label>Konto
|
||
<select name="account_id" data-type="int" required>
|
||
{% for a in accounts %}
|
||
<option value="{{ a.id }}">{{ a.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Buchungsdatum
|
||
<input type="date" name="booking_date" required>
|
||
</label>
|
||
<label>Betrag
|
||
<input type="text" name="amount" placeholder="-12.50" required>
|
||
</label>
|
||
<label>Verwendungszweck
|
||
<input type="text" name="purpose">
|
||
</label>
|
||
<label>Empfänger/Zahler
|
||
<input type="text" name="counterparty">
|
||
</label>
|
||
<button type="submit">Buchung speichern</button>
|
||
</form>
|
||
</fieldset>
|
||
{% endblock %}
|