feat: Inline-Bearbeiten fuer Fixposten, Einmalposten, Kredite und Szenario-Kopf
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for r in recurring %}
|
{% for r in recurring %}
|
||||||
<tr>
|
<tr id="rec-row-{{ r.id }}">
|
||||||
<td>{{ r.name }}</td>
|
<td>{{ r.name }}</td>
|
||||||
<td class="{{ 'neg' if r.amount < 0 else '' }}">{{ r.amount|eur }} €</td>
|
<td class="{{ 'neg' if r.amount < 0 else '' }}">{{ r.amount|eur }} €</td>
|
||||||
<td>{{ r.rhythm|de_label }}</td>
|
<td>{{ r.rhythm|de_label }}</td>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<td>{{ r.end_date.strftime('%d.%m.%Y') if r.end_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>{{ category_names.get(r.category_id, '–') }}</td>
|
||||||
<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"
|
<form class="inline-form" hx-delete="/api/recurring/{{ r.id }}" hx-swap="none"
|
||||||
hx-confirm="„{{ r.name }}“ wirklich löschen?"
|
hx-confirm="„{{ r.name }}“ wirklich löschen?"
|
||||||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||||||
@@ -27,6 +28,31 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 %}
|
{% else %}
|
||||||
<tr><td colspan="8">Noch keine wiederkehrenden Posten.</td></tr>
|
<tr><td colspan="8">Noch keine wiederkehrenden Posten.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -100,12 +126,13 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for p in planned %}
|
{% for p in planned %}
|
||||||
<tr>
|
<tr id="pln-row-{{ p.id }}">
|
||||||
<td>{{ p.name }}</td>
|
<td>{{ p.name }}</td>
|
||||||
<td class="{{ 'neg' if p.amount < 0 else '' }}">{{ p.amount|eur }} €</td>
|
<td class="{{ 'neg' if p.amount < 0 else '' }}">{{ p.amount|eur }} €</td>
|
||||||
<td>{{ p.due.strftime('%d.%m.%Y') }}</td>
|
<td>{{ p.due.strftime('%d.%m.%Y') }}</td>
|
||||||
<td>{{ category_names.get(p.category_id, '–') }}</td>
|
<td>{{ category_names.get(p.category_id, '–') }}</td>
|
||||||
<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"
|
<form class="inline-form" hx-delete="/api/planned/{{ p.id }}" hx-swap="none"
|
||||||
hx-confirm="„{{ p.name }}“ wirklich löschen?"
|
hx-confirm="„{{ p.name }}“ wirklich löschen?"
|
||||||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||||||
@@ -113,6 +140,24 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 %}
|
{% else %}
|
||||||
<tr><td colspan="5">Noch keine Einmalposten.</td></tr>
|
<tr><td colspan="5">Noch keine Einmalposten.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -145,7 +190,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for l in loans %}
|
{% for l in loans %}
|
||||||
<tr>
|
<tr id="loan-row-{{ l.id }}">
|
||||||
<td>{{ l.name }}</td>
|
<td>{{ l.name }}</td>
|
||||||
<td>{{ l.principal|eur }} €</td>
|
<td>{{ l.principal|eur }} €</td>
|
||||||
<td>{{ l.annual_rate_pct|eur }} %</td>
|
<td>{{ l.annual_rate_pct|eur }} %</td>
|
||||||
@@ -153,6 +198,7 @@
|
|||||||
<td>{{ l.payout_date.strftime('%d.%m.%Y') }}</td>
|
<td>{{ l.payout_date.strftime('%d.%m.%Y') }}</td>
|
||||||
<td>{{ l.repayment_type|de_label }}</td>
|
<td>{{ l.repayment_type|de_label }}</td>
|
||||||
<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"
|
<form class="inline-form" hx-delete="/api/loans/{{ l.id }}" hx-swap="none"
|
||||||
hx-confirm="„{{ l.name }}“ wirklich löschen?"
|
hx-confirm="„{{ l.name }}“ wirklich löschen?"
|
||||||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||||||
@@ -160,6 +206,27 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td colspan="7">
|
<td colspan="7">
|
||||||
<details ontoggle="if(this.open){loadLoanSchedule({{ l.id }})}">
|
<details ontoggle="if(this.open){loadLoanSchedule({{ l.id }})}">
|
||||||
@@ -199,11 +266,23 @@
|
|||||||
{% set sc = row.scenario %}
|
{% set sc = row.scenario %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{ sc.name }}</legend>
|
<legend>{{ sc.name }}</legend>
|
||||||
<p>
|
<p id="sc-row-{{ sc.id }}">
|
||||||
{{ sc.description }}
|
{{ sc.description }}
|
||||||
· Wiederkehrende Posten: {{ 'ja' if sc.include_recurring else 'nein' }}
|
· Wiederkehrende Posten: {{ 'ja' if sc.include_recurring else 'nein' }}
|
||||||
· Einmalposten: {{ 'ja' if sc.include_planned else 'nein' }}
|
· Einmalposten: {{ 'ja' if sc.include_planned else 'nein' }}
|
||||||
|
<button type="button" onclick="toggleEdit('sc', {{ sc.id }}, true)">Bearbeiten</button>
|
||||||
</p>
|
</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>
|
<details>
|
||||||
<summary>Kredite zuordnen</summary>
|
<summary>Kredite zuordnen</summary>
|
||||||
@@ -315,6 +394,12 @@
|
|||||||
// json-form-Extension ist zentral in base.html registriert (eine
|
// json-form-Extension ist zentral in base.html registriert (eine
|
||||||
// Definition fuer alle Templates, inkl. data-type="bool"-Handling).
|
// Definition fuer alle Templates, inkl. data-type="bool"-Handling).
|
||||||
|
|
||||||
|
function toggleEdit(prefix, id, editing) {
|
||||||
|
// UX-Regel: Zeilen werden versteckt (hidden), nie aus dem DOM entfernt.
|
||||||
|
document.getElementById(prefix + '-row-' + id).hidden = editing;
|
||||||
|
document.getElementById(prefix + '-edit-' + id).hidden = !editing;
|
||||||
|
}
|
||||||
|
|
||||||
function onModTargetTypeChange(select) {
|
function onModTargetTypeChange(select) {
|
||||||
var form = select.closest('form');
|
var form = select.closest('form');
|
||||||
var catSpan = form.querySelector('.mod-target-category');
|
var catSpan = form.querySelector('.mod-target-category');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import date, timedelta
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from app.formats import eur
|
from app.formats import eur
|
||||||
from app.models.tables import Account, RecurringItem
|
from app.models.tables import Account, Loan, PlannedItem, RecurringItem, Scenario
|
||||||
|
|
||||||
H = {"Authorization": "Bearer test-key"}
|
H = {"Authorization": "Bearer test-key"}
|
||||||
|
|
||||||
@@ -309,6 +309,22 @@ def test_salden_stichtag_garbage_falls_back_to_today(client):
|
|||||||
assert date.today().strftime('%d.%m.%Y') in r.text
|
assert date.today().strftime('%d.%m.%Y') in r.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_planung_hat_bearbeiten_formulare(client, db):
|
||||||
|
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||||||
|
db.add(RecurringItem(name="EditR", amount=Decimal("-5.00"), rhythm="monthly", due_day=1))
|
||||||
|
db.add(PlannedItem(name="EditP", amount=Decimal("-7.00"), due=date(2026, 8, 1)))
|
||||||
|
db.add(Loan(name="EditL", principal=Decimal("1000.00"),
|
||||||
|
annual_rate_pct=Decimal("4.50"), term_months=12,
|
||||||
|
payout_date=date(2026, 8, 1), repayment_type="annuity"))
|
||||||
|
db.add(Scenario(name="EditS", description="d"))
|
||||||
|
db.commit()
|
||||||
|
r = client.get("/planung").text
|
||||||
|
assert r.count(">Bearbeiten</button>") >= 4
|
||||||
|
for fragment in ('hx-patch="/api/recurring/', 'hx-patch="/api/planned/',
|
||||||
|
'hx-patch="/api/loans/', 'hx-patch="/api/scenarios/'):
|
||||||
|
assert fragment in r, fragment
|
||||||
|
|
||||||
|
|
||||||
def test_salden_shows_ohne_anker_footnote(client, db):
|
def test_salden_shows_ohne_anker_footnote(client, db):
|
||||||
# UX-Regel: Konten ohne Anker sind immer sichtbar als relative Werte
|
# UX-Regel: Konten ohne Anker sind immer sichtbar als relative Werte
|
||||||
# gekennzeichnet, nicht stillschweigend wie geankerte Konten dargestellt.
|
# gekennzeichnet, nicht stillschweigend wie geankerte Konten dargestellt.
|
||||||
|
|||||||
Reference in New Issue
Block a user