feat: Web-GUI Basis, Uebersicht, Buchungsliste
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
25
finance/app/templates/base.html
Normal file
25
finance/app/templates/base.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Finanzberatung{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/">Übersicht</a>
|
||||
<a href="/import">Import</a>
|
||||
<a href="/buchungen">Buchungen</a>
|
||||
<a href="/planung">Planung</a>
|
||||
<a href="http://{{ request.url.hostname or '127.0.0.1' }}:8097" target="_blank" rel="noopener">Grafana</a>
|
||||
<form method="post" action="/logout">
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
</nav>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
6
finance/app/templates/import.html
Normal file
6
finance/app/templates/import.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Import – Finanzberatung{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Import</h1>
|
||||
<p>Der Kontoauszug-Import (Upload, Vorschau, Bestätigen) folgt in einem späteren Ausbauschritt.</p>
|
||||
{% endblock %}
|
||||
54
finance/app/templates/index.html
Normal file
54
finance/app/templates/index.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Übersicht – Finanzberatung{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Übersicht</h1>
|
||||
|
||||
{% if projection and projection.below_threshold_date %}
|
||||
<div class="warning">
|
||||
Achtung: Prognostizierter Saldo unterschreitet den Schwellenwert am
|
||||
<strong>{{ projection.below_threshold_date.strftime('%d.%m.%Y') }}</strong>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="total-balance {{ 'neg' if total < 0 else '' }}">
|
||||
Gesamtsaldo: {{ '%.2f'|format(total) }} €
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Konto</th><th>Bank</th><th>Saldo</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for account, balance in balances %}
|
||||
<tr>
|
||||
<td>{{ account.name }}</td>
|
||||
<td>{{ account.bank }}</td>
|
||||
<td class="{{ 'neg' if balance < 0 else '' }}">{{ '%.2f'|format(balance) }} €</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3">Keine Konten angelegt.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Nächste anstehende Posten (30 Tage)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Datum</th><th>Bezeichnung</th><th>Betrag</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in upcoming %}
|
||||
<tr>
|
||||
<td>{{ item.date.strftime('%d.%m.%Y') }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td class="{{ 'neg' if item.amount < 0 else '' }}">{{ '%.2f'|format(item.amount) }} €</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3">Keine anstehenden Posten.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Grafana-Dashboard</h2>
|
||||
<iframe class="grafana" src="http://{{ request.url.hostname or '127.0.0.1' }}:8097"></iframe>
|
||||
{% endblock %}
|
||||
21
finance/app/templates/login.html
Normal file
21
finance/app/templates/login.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Anmelden – Finanzberatung</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Finanzberatung</h1>
|
||||
<form method="post" action="/login">
|
||||
<label>Benutzer
|
||||
<input name="username" placeholder="Benutzer">
|
||||
</label>
|
||||
<label>Passwort
|
||||
<input name="password" type="password" placeholder="Passwort">
|
||||
</label>
|
||||
<button type="submit">Anmelden</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
6
finance/app/templates/planung.html
Normal file
6
finance/app/templates/planung.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Planung – Finanzberatung{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Planung</h1>
|
||||
<p>Die Verwaltung von Fixposten, Einmalposten und Szenarien folgt in einem späteren Ausbauschritt.</p>
|
||||
{% endblock %}
|
||||
146
finance/app/templates/transactions.html
Normal file
146
finance/app/templates/transactions.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user