Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
318 lines
17 KiB
HTML
318 lines
17 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Szenarien – Finanzberatung{% endblock %}
|
||
{% block content %}
|
||
{% macro zeitraum(von, bis) -%}
|
||
{%- if von and bis -%}{{ von.strftime('%d.%m.%Y') }} – {{ bis.strftime('%d.%m.%Y') }}
|
||
{%- elif von -%}ab {{ von.strftime('%d.%m.%Y') }}
|
||
{%- elif bis -%}bis {{ bis.strftime('%d.%m.%Y') }}
|
||
{%- else -%}–{%- endif -%}
|
||
{%- endmacro %}
|
||
{% set TIP = 'Für diese Eintragsart nicht relevant' %}
|
||
|
||
<h1>Szenarien</h1>
|
||
|
||
<section class="planning-section">
|
||
{% for row in scenario_rows %}
|
||
{% set sc = row.scenario %}
|
||
<fieldset>
|
||
<legend>{{ sc.name }}</legend>
|
||
<p id="sc-row-{{ sc.id }}">
|
||
{{ sc.description }}
|
||
· Wiederkehrende Posten: {{ 'ja' if sc.include_recurring else 'nein' }}
|
||
· Einmalposten: {{ 'ja' if sc.include_planned else 'nein' }}
|
||
<button type="button" onclick="toggleEdit('sc', {{ sc.id }}, true)">Bearbeiten</button>
|
||
<button type="button" hx-delete="/api/scenarios/{{ sc.id }}" hx-swap="none"
|
||
hx-confirm="Szenario „{{ sc.name }}“ mit allen Einträgen wirklich löschen?"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">Löschen</button>
|
||
</p>
|
||
<div id="sc-edit-{{ sc.id }}" hidden>
|
||
<form hx-ext="json-form" hx-patch="/api/scenarios/{{ sc.id }}" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" value="{{ sc.name }}" required></label>
|
||
<label>Beschreibung <input type="text" name="description" value="{{ sc.description }}"></label>
|
||
<label><input type="checkbox" name="include_recurring" data-type="bool" {% if sc.include_recurring %}checked{% endif %}> Wiederkehrende Posten einschließen</label>
|
||
<label><input type="checkbox" name="include_planned" data-type="bool" {% if sc.include_planned %}checked{% endif %}> Einmalposten einschließen</label>
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" onclick="toggleEdit('sc', {{ sc.id }}, false)">Abbrechen</button>
|
||
</form>
|
||
</div>
|
||
|
||
<details>
|
||
<summary>Kredite zuordnen</summary>
|
||
{% for l in loans %}
|
||
<label class="inline-form">
|
||
<input type="checkbox" {% if l.id in row.assigned_loan_ids %}checked{% endif %}
|
||
onchange="toggleScenarioLoan({{ sc.id }}, {{ l.id }}, this.checked, this)">
|
||
{{ l.name }}
|
||
</label>
|
||
{% else %}
|
||
<p>Keine Kredite vorhanden.</p>
|
||
{% endfor %}
|
||
</details>
|
||
|
||
<h3>Einträge</h3>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Was</th><th>Art</th><th>Wert/Betrag</th><th>Zeitraum</th><th></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for m in row.modifiers %}
|
||
<tr id="mod-row-{{ m.id }}">
|
||
<td>
|
||
{% if m.target_type == "category" %}Kategorie: {{ category_names.get(m.target_id, m.target_id) }}
|
||
{% else %}Posten: {{ recurring_names.get(m.target_id, m.target_id) }}{% endif %}
|
||
</td>
|
||
<td>{{ m.kind|de_label }}</td>
|
||
<td>{% if m.kind in ('percent', 'absolute', 'set') %}{{ m.value|eur }}{% else %}–{% endif %}</td>
|
||
<td>{% if m.kind == 'ende' %}{{ m.end_date.strftime('%d.%m.%Y') if m.end_date else '–' }}{% else %}{{ zeitraum(m.start_date, m.end_date) }}{% endif %}</td>
|
||
<td>
|
||
<button type="button" onclick="toggleEdit('mod', {{ m.id }}, true)">Bearbeiten</button>
|
||
<form class="inline-form" hx-delete="/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<button type="submit">Löschen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<tr id="mod-edit-{{ m.id }}" hidden data-sort-follow>
|
||
<td colspan="5">
|
||
<form class="aenderung-form" hx-ext="json-form" hx-patch="/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Art
|
||
<select name="kind" data-type="str" onchange="FBSzenario.aenderungArt(this)">
|
||
{% for k in modifier_kinds %}<option value="{{ k }}" {% if k == m.kind %}selected{% endif %}>{{ k|de_label }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Ziel-Typ
|
||
<select name="target_type" data-type="str" onchange="FBSzenario.zielTyp(this)">
|
||
<option value="category" {% if m.target_type == 'category' %}selected{% endif %}>Kategorie</option>
|
||
<option value="recurring" {% if m.target_type == 'recurring' %}selected{% endif %}>Wiederkehrender Posten</option>
|
||
</select>
|
||
</label>
|
||
<span class="mod-target-category" {% if m.target_type != 'category' %}style="display:none"{% endif %}>
|
||
<label>Kategorie
|
||
<select name="target_id" data-type="int" {% if m.target_type != 'category' %}disabled{% endif %}>
|
||
{% for c in categories %}<option value="{{ c.id }}" {% if m.target_type == 'category' and c.id == m.target_id %}selected{% endif %}>{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
</span>
|
||
<span class="mod-target-recurring" {% if m.target_type != 'recurring' %}style="display:none"{% endif %}>
|
||
<label>Posten
|
||
<select name="target_id" data-type="int" {% if m.target_type != 'recurring' %}disabled{% endif %}>
|
||
{% for r in recurring %}<option value="{{ r.id }}" {% if m.target_type == 'recurring' and r.id == m.target_id %}selected{% endif %}>{{ r.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
</span>
|
||
<label><span class="value-label">Wert</span> <input type="text" name="value" data-type="amount" value="{{ m.value|eur }}" {% if m.kind not in ('percent', 'absolute', 'set') %}disabled title="{{ TIP }}"{% endif %}></label>
|
||
<label>Gültig ab <input type="date" name="start_date" value="{{ m.start_date.isoformat() if m.start_date else '' }}" {% if m.kind == 'ende' %}disabled title="{{ TIP }}"{% endif %}></label>
|
||
<label><span class="bis-label">{{ 'Endet am' if m.kind == 'ende' else 'Gültig bis' }}</span> <input type="date" name="end_date" value="{{ m.end_date.isoformat() if m.end_date else '' }}"></label>
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" onclick="toggleEdit('mod', {{ m.id }}, false)">Abbrechen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% for ri in row.recurring_items %}
|
||
<tr id="sri-row-{{ ri.id }}">
|
||
<td>{{ ri.name }}{% if ri.category_id %} <span class="muted">({{ category_names.get(ri.category_id, '') }})</span>{% endif %}</td>
|
||
<td>Wiederkehrend ({{ ri.rhythm|de_label }}, am {{ ri.due_day }}.)</td>
|
||
<td class="{{ 'neg' if ri.amount < 0 else '' }}">{{ ri.amount|eur }} €</td>
|
||
<td>{{ zeitraum(ri.start_date, ri.end_date) }}</td>
|
||
<td>
|
||
<button type="button" onclick="toggleEdit('sri', {{ ri.id }}, true)">Bearbeiten</button>
|
||
<form class="inline-form" hx-delete="/api/scenarios/{{ sc.id }}/recurring/{{ ri.id }}" hx-swap="none"
|
||
hx-confirm="„{{ ri.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="sri-edit-{{ ri.id }}" hidden data-sort-follow>
|
||
<td colspan="5">
|
||
<form hx-ext="json-form" hx-patch="/api/scenarios/{{ sc.id }}/recurring/{{ ri.id }}" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" value="{{ ri.name }}" required></label>
|
||
<label>Betrag <input type="text" name="amount" data-type="amount" value="{{ ri.amount|eur }}" required></label>
|
||
<label>Kategorie
|
||
<select name="category_id" data-type="int">
|
||
<option value="">(keine)</option>
|
||
{% for c in categories %}<option value="{{ c.id }}" {% if c.id == ri.category_id %}selected{% endif %}>{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Rhythmus
|
||
<select name="rhythm" data-type="str">
|
||
{% for rh in rhythms %}<option value="{{ rh }}" {% if rh == ri.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="{{ ri.due_day }}" required></label>
|
||
<label>Start <input type="date" name="start_date" value="{{ ri.start_date.isoformat() if ri.start_date else '' }}"></label>
|
||
<label>Ende <input type="date" name="end_date" value="{{ ri.end_date.isoformat() if ri.end_date else '' }}"></label>
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" onclick="toggleEdit('sri', {{ ri.id }}, false)">Abbrechen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% for sp in row.planned_items %}
|
||
<tr id="spi-row-{{ sp.id }}">
|
||
<td>{{ sp.name }}{% if sp.category_id %} <span class="muted">({{ category_names.get(sp.category_id, '') }})</span>{% endif %}</td>
|
||
<td>Einmalzahlung</td>
|
||
<td class="{{ 'neg' if sp.amount < 0 else '' }}">{{ sp.amount|eur }} €</td>
|
||
<td>{{ sp.due.strftime('%d.%m.%Y') }}</td>
|
||
<td>
|
||
<button type="button" onclick="toggleEdit('spi', {{ sp.id }}, true)">Bearbeiten</button>
|
||
<form class="inline-form" hx-delete="/api/scenarios/{{ sc.id }}/planned/{{ sp.id }}" hx-swap="none"
|
||
hx-confirm="„{{ sp.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="spi-edit-{{ sp.id }}" hidden data-sort-follow>
|
||
<td colspan="5">
|
||
<form hx-ext="json-form" hx-patch="/api/scenarios/{{ sc.id }}/planned/{{ sp.id }}" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" value="{{ sp.name }}" required></label>
|
||
<label>Betrag <input type="text" name="amount" data-type="amount" value="{{ sp.amount|eur }}" required></label>
|
||
<label>Kategorie
|
||
<select name="category_id" data-type="int">
|
||
<option value="">(keine)</option>
|
||
{% for c in categories %}<option value="{{ c.id }}" {% if c.id == sp.category_id %}selected{% endif %}>{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Fällig am <input type="date" name="due" value="{{ sp.due.isoformat() }}" required></label>
|
||
<button type="submit">Speichern</button>
|
||
<button type="button" onclick="toggleEdit('spi', {{ sp.id }}, false)">Abbrechen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% if not row.modifiers and not row.planned_items and not row.recurring_items %}
|
||
<tr><td colspan="5">Keine Einträge.</td></tr>
|
||
{% endif %}
|
||
</tbody>
|
||
</table>
|
||
|
||
<h4>Neue Änderung an Bestehendem</h4>
|
||
<form class="entry-form aenderung-form" hx-ext="json-form" hx-post="/api/scenarios/{{ sc.id }}/modifiers" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<div class="entry-art">
|
||
<label>Art
|
||
<select name="kind" data-type="str" onchange="FBSzenario.aenderungArt(this)">
|
||
{% for k in modifier_kinds %}<option value="{{ k }}">{{ k|de_label }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<span class="muted">— die zur Art passenden Felder werden aktiv, gesperrte Felder sind ausgegraut.</span>
|
||
</div>
|
||
<label>Ziel-Typ
|
||
<select name="target_type" data-type="str" onchange="FBSzenario.zielTyp(this)">
|
||
<option value="category">Kategorie</option>
|
||
<option value="recurring">Wiederkehrender Posten</option>
|
||
</select>
|
||
</label>
|
||
<span class="mod-target-category">
|
||
<label>Kategorie
|
||
<select name="target_id" data-type="int">
|
||
{% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
</span>
|
||
<span class="mod-target-recurring" style="display:none">
|
||
<label>Posten
|
||
<select name="target_id" data-type="int" disabled title="{{ TIP }}">
|
||
{% for r in recurring %}<option value="{{ r.id }}">{{ r.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
</span>
|
||
<label><span class="value-label">Prozentsatz (%)</span> <input type="text" name="value" data-type="amount" value="0"></label>
|
||
<label>Gültig ab <input type="date" name="start_date"></label>
|
||
<label><span class="bis-label">Gültig bis</span> <input type="date" name="end_date"></label>
|
||
<button type="submit">Hinzufügen</button>
|
||
</form>
|
||
|
||
<h4>Neue Zahlung nur in diesem Szenario</h4>
|
||
<form class="entry-form zahlung-form" hx-ext="json-form" hx-post="/api/scenarios/{{ sc.id }}/planned" hx-swap="none"
|
||
data-planned-url="/api/scenarios/{{ sc.id }}/planned"
|
||
data-recurring-url="/api/scenarios/{{ sc.id }}/recurring"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<div class="entry-art">
|
||
<label>Art
|
||
<select class="zahlung-art" onchange="FBSzenario.zahlungArt(this)">
|
||
<option value="einmal">Einmalzahlung</option>
|
||
<option value="wiederkehrend">Wiederkehrend</option>
|
||
</select>
|
||
</label>
|
||
<span class="muted">— zählt nur in diesem Szenario, auch ohne „Wiederkehrende Posten/Einmalposten einschließen“.</span>
|
||
</div>
|
||
<label>Name <input type="text" name="name" required></label>
|
||
<label>Betrag <input type="text" name="amount" data-type="amount" placeholder="-250,00" required></label>
|
||
<label>Kategorie
|
||
<select name="category_id" data-type="int">
|
||
<option value="">(keine)</option>
|
||
{% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
|
||
</select>
|
||
</label>
|
||
<label>Fällig am <input type="date" name="due" required></label>
|
||
<label>Rhythmus
|
||
<select name="rhythm" data-type="str" disabled title="{{ TIP }}">
|
||
{% 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 disabled title="{{ TIP }}"></label>
|
||
<label>Start <input type="date" name="start_date" disabled title="{{ TIP }}"></label>
|
||
<label>Ende <input type="date" name="end_date" disabled title="{{ TIP }}"></label>
|
||
<button type="submit">Hinzufügen</button>
|
||
</form>
|
||
|
||
<hr>
|
||
<div class="project-block">
|
||
<form hx-post="/api/scenarios/{{ sc.id }}/project" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<button type="submit">Durchrechnen</button>
|
||
</form>
|
||
</div>
|
||
|
||
{% if row.result %}
|
||
<p>
|
||
Tiefpunkt: {{ row.result.low_point_balance|eur }} € am {{ row.result.low_point_date.strftime('%d.%m.%Y') }}<br>
|
||
{% if row.result.below_zero_date %}Unterschreitet 0 € ab {{ row.result.below_zero_date.strftime('%d.%m.%Y') }}<br>{% endif %}
|
||
{% if row.result.below_threshold_date %}Unterschreitet Warnschwelle ab {{ row.result.below_threshold_date.strftime('%d.%m.%Y') }}<br>{% endif %}
|
||
Kurven in <a href="{{ grafana_public_base(request) }}" target="_blank" rel="noopener">Grafana</a> ansehen.
|
||
</p>
|
||
{% else %}
|
||
<p>Noch nicht durchgerechnet.</p>
|
||
{% endif %}
|
||
</fieldset>
|
||
{% else %}
|
||
<p>Noch keine Szenarien.</p>
|
||
{% endfor %}
|
||
|
||
<fieldset>
|
||
<legend>Neues Szenario anlegen</legend>
|
||
<form hx-ext="json-form" hx-post="/api/scenarios" hx-swap="none"
|
||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||
<label>Name <input type="text" name="name" required></label>
|
||
<label>Beschreibung <input type="text" name="description"></label>
|
||
<label><input type="checkbox" name="include_recurring" data-type="bool" checked> Wiederkehrende Posten einschließen</label>
|
||
<label><input type="checkbox" name="include_planned" data-type="bool" checked> Einmalposten einschließen</label>
|
||
<button type="submit">Szenario anlegen</button>
|
||
</form>
|
||
</fieldset>
|
||
</section>
|
||
|
||
<script>
|
||
function toggleScenarioLoan(scenarioId, loanId, checked, checkbox) {
|
||
fetch('/api/scenarios/' + scenarioId + '/loans/' + loanId, { method: checked ? 'POST' : 'DELETE' })
|
||
.then(function (resp) {
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
window.location.reload();
|
||
})
|
||
.catch(function (err) {
|
||
checkbox.checked = !checked;
|
||
alert('Zuordnung fehlgeschlagen: ' + err.message);
|
||
});
|
||
}
|
||
</script>
|
||
{% endblock %}
|