feat: Web-GUI Basis, Uebersicht, Buchungsliste

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 18:45:54 +02:00
parent e8ea948b38
commit a6009c47e3
11 changed files with 488 additions and 9 deletions

View File

@@ -0,0 +1,146 @@
{% 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>
<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>{{ tx.account_id }}</td>
<td>{{ tx.purpose }}</td>
<td>{{ tx.counterparty }}</td>
<td class="{{ 'neg' if tx.amount < 0 else '' }}">{{ '%.2f'|format(tx.amount) }} €</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>
<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>
<script>
// Minimale JSON-Kodierung für htmx-Formulare/Selects, die gegen die JSON-APIs
// (POST/PATCH /api/...) senden. Felder mit data-type="int" werden als Zahl
// kodiert, leere Werte als null, alles andere bleibt String.
htmx.defineExtension('json-form', {
onEvent: function (name, evt) {
if (name === 'htmx:configRequest') {
evt.detail.headers['Content-Type'] = 'application/json';
}
},
encodeParameters: function (xhr, parameters, elt) {
var out = {};
for (var k in parameters) {
var v = parameters[k];
var field = elt.querySelector('[name="' + k + '"]');
var kind = field && field.dataset ? field.dataset.type : null;
if (v === '') {
out[k] = null;
} else if (kind === 'int') {
out[k] = parseInt(v, 10);
} else {
out[k] = v;
}
}
xhr.overrideMimeType('text/json');
return JSON.stringify(out);
}
});
</script>
{% endblock %}