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>
|
||||
<tbody>
|
||||
{% for r in recurring %}
|
||||
<tr>
|
||||
<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>
|
||||
@@ -20,6 +20,7 @@
|
||||
<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()}">
|
||||
@@ -27,6 +28,31 @@
|
||||
</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 %}
|
||||
@@ -100,12 +126,13 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in planned %}
|
||||
<tr>
|
||||
<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()}">
|
||||
@@ -113,6 +140,24 @@
|
||||
</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 %}
|
||||
@@ -145,7 +190,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in loans %}
|
||||
<tr>
|
||||
<tr id="loan-row-{{ l.id }}">
|
||||
<td>{{ l.name }}</td>
|
||||
<td>{{ l.principal|eur }} €</td>
|
||||
<td>{{ l.annual_rate_pct|eur }} %</td>
|
||||
@@ -153,6 +198,7 @@
|
||||
<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()}">
|
||||
@@ -160,6 +206,27 @@
|
||||
</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 }})}">
|
||||
@@ -199,11 +266,23 @@
|
||||
{% set sc = row.scenario %}
|
||||
<fieldset>
|
||||
<legend>{{ sc.name }}</legend>
|
||||
<p>
|
||||
<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>
|
||||
</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>
|
||||
@@ -315,6 +394,12 @@
|
||||
// json-form-Extension ist zentral in base.html registriert (eine
|
||||
// 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) {
|
||||
var form = select.closest('form');
|
||||
var catSpan = form.querySelector('.mod-target-category');
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
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"}
|
||||
|
||||
@@ -309,6 +309,22 @@ def test_salden_stichtag_garbage_falls_back_to_today(client):
|
||||
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):
|
||||
# UX-Regel: Konten ohne Anker sind immer sichtbar als relative Werte
|
||||
# gekennzeichnet, nicht stillschweigend wie geankerte Konten dargestellt.
|
||||
|
||||
Reference in New Issue
Block a user