298 lines
14 KiB
HTML
298 lines
14 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Planung – Finanzberatung{% endblock %}
|
||
{% block content %}
|
||
<h1>Planung</h1>
|
||
|
||
<section class="planning-section">
|
||
<h2>Wiederkehrende Posten</h2>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Name</th><th>Betrag</th><th>Rhythmus</th><th>Fälligkeitstag</th><th>Start</th><th>Ende</th><th>Kategorie</th><th></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% 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>{{ r.rhythm|de_label }}</td>
|
||
<td>{{ r.due_day }}</td>
|
||
<td>{{ r.start_date.strftime('%d.%m.%Y') if r.start_date else '–' }}</td>
|
||
<td>{{ r.end_date.strftime('%d.%m.%Y') if r.end_date else '–' }}</td>
|
||
<td>{{ category_names.get(r.category_id, '–') }}</td>
|
||
<td>
|
||
<button type="button" onclick="toggleEdit('rec', {{ r.id }}, true)">Bearbeiten</button>
|
||
<form class="inline-form" hx-delete="/api/recurring/{{ r.id }}" hx-swap="none"
|
||
hx-confirm="„{{ r.name }}“ wirklich löschen?"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<button type="submit">Löschen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<tr id="rec-edit-{{ r.id }}" hidden>
|
||
<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()}">
|
||
<label>Name <input type="text" name="name" value="{{ r.name }}" required></label>
|
||
<label>Betrag <input type="text" name="amount" data-type="amount" value="{{ r.amount|eur }}" required></label>
|
||
<label>Rhythmus
|
||
<select name="rhythm" data-type="str">
|
||
{% for rh in rhythms %}<option value="{{ rh }}" {% if rh == r.rhythm %}selected{% endif %}>{{ rh|de_label }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Fälligkeitstag <input type="number" name="due_day" data-type="int" min="1" max="31" value="{{ r.due_day }}" required></label>
|
||
<label>Start <input type="date" name="start_date" value="{{ r.start_date.isoformat() if r.start_date else '' }}"></label>
|
||
<label>Ende <input type="date" name="end_date" value="{{ r.end_date.isoformat() if r.end_date else '' }}"></label>
|
||
<label>Kategorie
|
||
<select name="category_id" data-type="int">
|
||
<option value="">–</option>
|
||
{% for c in categories %}<option value="{{ c.id }}" {% if c.id == r.category_id %}selected{% endif %}>{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" onclick="toggleEdit('rec', {{ r.id }}, false)">Abbrechen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="8">Noch keine wiederkehrenden Posten.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
<fieldset>
|
||
<legend>Neuen wiederkehrenden Posten anlegen</legend>
|
||
<form hx-ext="json-form" hx-post="/api/recurring" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" required></label>
|
||
<label>Betrag <input type="text" name="amount" data-type="amount" placeholder="-49,99" required></label>
|
||
<label>Rhythmus
|
||
<select name="rhythm" data-type="str">
|
||
{% for rh in rhythms %}<option value="{{ rh }}">{{ rh|de_label }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Fälligkeitstag <input type="number" name="due_day" data-type="int" min="1" max="31" required></label>
|
||
<label>Start (optional) <input type="date" name="start_date"></label>
|
||
<label>Ende (optional) <input type="date" name="end_date"></label>
|
||
<label>Kategorie
|
||
<select name="category_id" data-type="int">
|
||
<option value="">–</option>
|
||
{% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit">Anlegen</button>
|
||
</form>
|
||
</fieldset>
|
||
|
||
<fieldset>
|
||
<legend>Vorschläge aus Buchungen</legend>
|
||
{% if suggestions %}
|
||
<table>
|
||
<thead>
|
||
<tr><th>Name</th><th>Betrag</th><th>Rhythmus</th><th>Fälligkeitstag</th><th></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for s in suggestions %}
|
||
<tr>
|
||
<td>{{ s.name }}</td>
|
||
<td class="{{ 'neg' if s.amount < 0 else '' }}">{{ s.amount|eur }} €</td>
|
||
<td>{{ s.rhythm|de_label }}</td>
|
||
<td>{{ s.due_day }}</td>
|
||
<td>
|
||
<form class="inline-form" hx-ext="json-form" hx-post="/api/recurring" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<input type="hidden" name="name" value="{{ s.name }}">
|
||
<input type="hidden" name="amount" value="{{ s.amount }}">
|
||
<input type="hidden" name="rhythm" value="{{ s.rhythm }}">
|
||
<input type="hidden" name="due_day" data-type="int" value="{{ s.due_day }}">
|
||
<input type="hidden" name="category_id" data-type="int" value="{{ s.category_id if s.category_id is not none else '' }}">
|
||
<button type="submit">Vorschlag übernehmen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p class="muted">Keine Vorschläge — erkannt werden Serien aus mindestens 3 Monaten gleichartiger Buchungen.</p>
|
||
{% endif %}
|
||
</fieldset>
|
||
</section>
|
||
|
||
<section class="planning-section">
|
||
<h2>Einmalposten</h2>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Name</th><th>Betrag</th><th>Fällig am</th><th>Kategorie</th><th></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% 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>{{ p.due.strftime('%d.%m.%Y') }}</td>
|
||
<td>{{ category_names.get(p.category_id, '–') }}</td>
|
||
<td>
|
||
<button type="button" onclick="toggleEdit('pln', {{ p.id }}, true)">Bearbeiten</button>
|
||
<form class="inline-form" hx-delete="/api/planned/{{ p.id }}" hx-swap="none"
|
||
hx-confirm="„{{ p.name }}“ wirklich löschen?"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<button type="submit">Löschen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<tr id="pln-edit-{{ p.id }}" hidden>
|
||
<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()}">
|
||
<label>Name <input type="text" name="name" value="{{ p.name }}" required></label>
|
||
<label>Betrag <input type="text" name="amount" data-type="amount" value="{{ p.amount|eur }}" required></label>
|
||
<label>Fällig am <input type="date" name="due" value="{{ p.due.isoformat() }}" required></label>
|
||
<label>Kategorie
|
||
<select name="category_id" data-type="int">
|
||
<option value="">–</option>
|
||
{% for c in categories %}<option value="{{ c.id }}" {% if c.id == p.category_id %}selected{% endif %}>{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" onclick="toggleEdit('pln', {{ p.id }}, false)">Abbrechen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="5">Noch keine Einmalposten.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
<fieldset>
|
||
<legend>Neuen Einmalposten anlegen</legend>
|
||
<form hx-ext="json-form" hx-post="/api/planned" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" required></label>
|
||
<label>Betrag <input type="text" name="amount" data-type="amount" placeholder="-2000,00" required></label>
|
||
<label>Fällig am <input type="date" name="due" required></label>
|
||
<label>Kategorie
|
||
<select name="category_id" data-type="int">
|
||
<option value="">–</option>
|
||
{% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit">Anlegen</button>
|
||
</form>
|
||
</fieldset>
|
||
</section>
|
||
|
||
<section class="planning-section">
|
||
<h2>Kredite</h2>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Name</th><th>Summe</th><th>Zins p.a.</th><th>Laufzeit</th><th>Auszahlung</th><th>Tilgungsart</th><th></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for l in loans %}
|
||
<tr id="loan-row-{{ l.id }}">
|
||
<td>{{ l.name }}</td>
|
||
<td>{{ l.principal|eur }} €</td>
|
||
<td>{{ l.annual_rate_pct|eur }} %</td>
|
||
<td>{{ l.term_months }} Monate</td>
|
||
<td>{{ l.payout_date.strftime('%d.%m.%Y') }}</td>
|
||
<td>{{ l.repayment_type|de_label }}</td>
|
||
<td>
|
||
<button type="button" onclick="toggleEdit('loan', {{ l.id }}, true)">Bearbeiten</button>
|
||
<form class="inline-form" hx-delete="/api/loans/{{ l.id }}" hx-swap="none"
|
||
hx-confirm="„{{ l.name }}“ wirklich löschen?"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<button type="submit">Löschen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<!-- Hinweis: eine Kredit-Aenderung veraendert den Tilgungsplan; danach
|
||
muessen betroffene Szenarien neu durchgerechnet werden. -->
|
||
<tr id="loan-edit-{{ l.id }}" hidden>
|
||
<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()}">
|
||
<label>Name <input type="text" name="name" value="{{ l.name }}" required></label>
|
||
<label>Darlehenssumme <input type="text" name="principal" data-type="amount" value="{{ l.principal|eur }}" required></label>
|
||
<label>Zins p.a. (%) <input type="text" name="annual_rate_pct" data-type="amount" value="{{ l.annual_rate_pct|eur }}" required></label>
|
||
<label>Laufzeit (Monate) <input type="number" name="term_months" data-type="int" min="1" value="{{ l.term_months }}" required></label>
|
||
<label>Auszahlungsdatum <input type="date" name="payout_date" value="{{ l.payout_date.isoformat() }}" required></label>
|
||
<label>Tilgungsart
|
||
<select name="repayment_type" data-type="str">
|
||
{% for rt in repayment_types %}<option value="{{ rt }}" {% if rt == l.repayment_type %}selected{% endif %}>{{ rt|de_label }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" onclick="toggleEdit('loan', {{ l.id }}, false)">Abbrechen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="7">
|
||
<details ontoggle="if(this.open){loadLoanSchedule({{ l.id }})}">
|
||
<summary>Tilgungsplan</summary>
|
||
<div id="schedule-{{ l.id }}">Wird beim Öffnen geladen…</div>
|
||
</details>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="7">Noch keine Kredite.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
<fieldset>
|
||
<legend>Neuen Kredit anlegen</legend>
|
||
<form hx-ext="json-form" hx-post="/api/loans" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" required></label>
|
||
<label>Darlehenssumme <input type="text" name="principal" data-type="amount" placeholder="10000,00" required></label>
|
||
<label>Zins p.a. (%) <input type="text" name="annual_rate_pct" data-type="amount" placeholder="4,5" required></label>
|
||
<label>Laufzeit (Monate) <input type="number" name="term_months" data-type="int" min="1" required></label>
|
||
<label>Auszahlungsdatum <input type="date" name="payout_date" required></label>
|
||
<label>Tilgungsart
|
||
<select name="repayment_type" data-type="str">
|
||
{% for rt in repayment_types %}<option value="{{ rt }}">{{ rt|de_label }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<button type="submit">Anlegen</button>
|
||
</form>
|
||
</fieldset>
|
||
</section>
|
||
|
||
<script>
|
||
// json-form-Extension ist zentral in base.html registriert (eine
|
||
// Definition fuer alle Templates, inkl. data-type="bool"-Handling).
|
||
// toggleEdit ist ebenfalls zentral in base.html definiert (wird auf
|
||
// mehreren Seiten fuer Inline-Edit-Formulare gebraucht).
|
||
|
||
var loadedSchedules = {};
|
||
function formatIsoDate(iso) {
|
||
var parts = iso.split('-');
|
||
return parts[2] + '.' + parts[1] + '.' + parts[0];
|
||
}
|
||
function fmtEur(n) {
|
||
return Number(n).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
||
}
|
||
function loadLoanSchedule(loanId) {
|
||
if (loadedSchedules[loanId]) return;
|
||
loadedSchedules[loanId] = true;
|
||
fetch('/api/loans/' + loanId + '/schedule')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (rows) {
|
||
var html = '<table><thead><tr><th>Fällig</th><th>Rate</th><th>Zins</th>'
|
||
+ '<th>Tilgung</th><th>Restschuld</th></tr></thead><tbody>';
|
||
rows.forEach(function (r) {
|
||
html += '<tr><td>' + formatIsoDate(r.due) + '</td>'
|
||
+ '<td>' + fmtEur(r.payment) + ' €</td>'
|
||
+ '<td>' + fmtEur(r.interest) + ' €</td>'
|
||
+ '<td>' + fmtEur(r.principal) + ' €</td>'
|
||
+ '<td>' + fmtEur(r.remaining) + ' €</td></tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
document.getElementById('schedule-' + loanId).innerHTML = html;
|
||
});
|
||
}
|
||
</script>
|
||
{% endblock %}
|