docs: Implementierungsplan Ausbaustufe 6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
449
docs/superpowers/plans/2026-07-20-ausbaustufe-6.md
Normal file
449
docs/superpowers/plans/2026-07-20-ausbaustufe-6.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# Ausbaustufe 6 Implementation Plan — Szenario-Einträge-Tabelle (v0.7.0)
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Pro Szenario eine immer sichtbare, editierbare Tabelle „Einträge" (Modifikatoren + szenario-eigene Einmalzahlungen gemeinsam) mit Inline-Bearbeiten und einem gemeinsamen Anlege-Formular; Release v0.7.0.
|
||||
|
||||
**Architecture:** Datenmodell unverändert — zwei neue Full-Body-PATCH-Endpunkte in `routers/scenarios.py`; Template-Umbau in `planning.html` ersetzt die zwei `<details>`-Abschnitte durch eine Tabelle; Feld-Umschaltung per `disabled` (JS `onEntryArtChange` ersetzt `onModKindChange`), Endpunkt-Wechsel des Anlege-Formulars per `hx-post`-Attribut + `htmx.process`.
|
||||
|
||||
**Tech Stack:** FastAPI + Pydantic v2, Jinja2, htmx + json-form-Extension, pytest.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-07-20-szenario-eintraege-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Geldbeträge `Decimal`; JSON-API Punkt-Dezimal; HTML deutsch (`|eur`, `|de_label`, TT.MM.JJJJ).
|
||||
- API-/DB-Werte englisch; neuer Formular-Artwert `einmal` ist NUR clientseitig (landet nie in der DB — an `/planned` gesendet wird er von Pydantic ignoriert).
|
||||
- UX-Regel: Umschalten/Verstecken nur per `disabled`/`hidden`, nie DOM-Entfernen.
|
||||
- Fable-Testagent-Gate je Task VOR dem Commit; Ledger-Eintrag je Task in `.superpowers/sdd/progress.md`.
|
||||
- DATENSCHUTZ: keine echten Namen/Beträge in Commits/Tests/Doku; Live-Demo-Daten nur DB/Chat.
|
||||
- Testlauf: `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` — Basis 178 passed, muss grün bleiben.
|
||||
- Pfade unten relativ zu `/home/wlfb/bin/finance` (Ledger/Plan unter `/home/wlfb/bin`).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: PATCH-Endpunkte für Modifikatoren und Szenario-Einmalzahlungen
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/routers/scenarios.py`
|
||||
- Modify: `tests/test_crud_api.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ModifierIn`/`ModifierOut`, `ScenarioPlannedIn`/`ScenarioPlannedOut`, `_get_scenario` (alle vorhanden in `scenarios.py`).
|
||||
- Produces: `PATCH /api/scenarios/{scenario_id}/modifiers/{mod_id}` (Body `ModifierIn`, Antwort `ModifierOut`, 404 „Modifikator nicht gefunden") und `PATCH /api/scenarios/{scenario_id}/planned/{item_id}` (Body `ScenarioPlannedIn`, Antwort `ScenarioPlannedOut`, 404 „Einmalzahlung nicht gefunden"). Task 2 (GUI) sendet auf genau diese URLs.
|
||||
|
||||
- [ ] **Step 1: Failing Tests** — in `tests/test_crud_api.py` ergänzen:
|
||||
|
||||
```python
|
||||
def test_modifier_patch(client, db):
|
||||
sc = client.post("/api/scenarios", headers=H, json={"name": "ModPatch"}).json()
|
||||
m = client.post(f"/api/scenarios/{sc['id']}/modifiers", headers=H, json={
|
||||
"target_type": "recurring", "target_id": 1, "kind": "percent",
|
||||
"value": "10.00"}).json()
|
||||
# Wert aendern
|
||||
r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={
|
||||
"target_type": "recurring", "target_id": 1, "kind": "percent", "value": "25.00"})
|
||||
assert r.status_code == 200 and r.json()["value"] == "25.00"
|
||||
# Art auf ende wechseln (mit Datum); value wird vom Validator genullt
|
||||
r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={
|
||||
"target_type": "recurring", "target_id": 1, "kind": "ende",
|
||||
"end_date": "2026-08-31"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["kind"] == "ende" and r.json()["end_date"] == "2026-08-31"
|
||||
# ende ohne Datum -> 422
|
||||
r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={
|
||||
"target_type": "recurring", "target_id": 1, "kind": "ende"})
|
||||
assert r.status_code == 422
|
||||
# zurueck auf remove: end_date wird genullt
|
||||
r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={
|
||||
"target_type": "recurring", "target_id": 1, "kind": "remove",
|
||||
"end_date": "2026-08-31"})
|
||||
assert r.status_code == 200 and r.json()["end_date"] is None
|
||||
# fremdes Szenario -> 404
|
||||
other = client.post("/api/scenarios", headers=H, json={"name": "ModPatch2"}).json()
|
||||
r = client.patch(f"/api/scenarios/{other['id']}/modifiers/{m['id']}", headers=H, json={
|
||||
"target_type": "recurring", "target_id": 1, "kind": "remove"})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_scenario_planned_patch(client, db):
|
||||
sc = client.post("/api/scenarios", headers=H, json={"name": "SpiPatch"}).json()
|
||||
p = client.post(f"/api/scenarios/{sc['id']}/planned", headers=H, json={
|
||||
"name": "Zufluss", "amount": "5000.00", "due": "2026-07-30"}).json()
|
||||
r = client.patch(f"/api/scenarios/{sc['id']}/planned/{p['id']}", headers=H, json={
|
||||
"name": "Zufluss geaendert", "amount": "6000.00", "due": "2026-08-15"})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["name"] == "Zufluss geaendert"
|
||||
assert body["amount"] == "6000.00" and body["due"] == "2026-08-15"
|
||||
# fremdes Szenario -> 404
|
||||
other = client.post("/api/scenarios", headers=H, json={"name": "SpiPatch2"}).json()
|
||||
r = client.patch(f"/api/scenarios/{other['id']}/planned/{p['id']}", headers=H, json={
|
||||
"name": "x", "amount": "1.00", "due": "2026-08-15"})
|
||||
assert r.status_code == 404
|
||||
```
|
||||
|
||||
Run: `.venv/bin/python -m pytest tests/test_crud_api.py -q` → neue Tests FAIL (405 Method Not Allowed)
|
||||
|
||||
- [ ] **Step 2: Endpunkte implementieren** — `app/routers/scenarios.py`, direkt nach `delete_modifier` bzw. nach `delete_scenario_planned`:
|
||||
|
||||
```python
|
||||
@router.patch("/{scenario_id}/modifiers/{mod_id}", response_model=ModifierOut)
|
||||
def patch_modifier(scenario_id: int, mod_id: int, data: ModifierIn,
|
||||
session: Session = Depends(get_session)):
|
||||
# Full-Body-Update: das Edit-Formular sendet immer alle Felder des Typs.
|
||||
_get_scenario(session, scenario_id)
|
||||
modifier = session.get(ScenarioModifier, mod_id)
|
||||
if modifier is None or modifier.scenario_id != scenario_id:
|
||||
raise HTTPException(404, "Modifikator nicht gefunden")
|
||||
for key, value in data.model_dump().items():
|
||||
setattr(modifier, key, value)
|
||||
session.commit()
|
||||
session.refresh(modifier)
|
||||
return ModifierOut.model_validate(modifier)
|
||||
```
|
||||
|
||||
```python
|
||||
@router.patch("/{scenario_id}/planned/{item_id}", response_model=ScenarioPlannedOut)
|
||||
def patch_scenario_planned(scenario_id: int, item_id: int, data: ScenarioPlannedIn,
|
||||
session: Session = Depends(get_session)):
|
||||
# Full-Body-Update, Muster patch_modifier.
|
||||
_get_scenario(session, scenario_id)
|
||||
item = session.get(ScenarioPlannedItem, item_id)
|
||||
if item is None or item.scenario_id != scenario_id:
|
||||
raise HTTPException(404, "Einmalzahlung nicht gefunden")
|
||||
for key, value in data.model_dump().items():
|
||||
setattr(item, key, value)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
return ScenarioPlannedOut.model_validate(item)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Tests grün + Suite**
|
||||
|
||||
Run: `.venv/bin/python -m pytest tests/test_crud_api.py -q && .venv/bin/python -m pytest -q`
|
||||
Expected: alles PASS (178 + 2)
|
||||
|
||||
- [ ] **Step 4: Fable-Testagent-Abnahme** (Faktencheck: Validator-Wirkung beim PATCH [ende↔andere Arten], 404-Pfade, keine Nebenwirkungen auf Projektion). Erst nach VERIFIED weiter.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/wlfb/bin && git add finance/app/routers/scenarios.py finance/tests/test_crud_api.py
|
||||
git commit -m "feat: PATCH-Endpunkte fuer Szenario-Modifikatoren und -Einmalzahlungen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: GUI-Umbau — eine Einträge-Tabelle je Szenario
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/templates/planning.html` (Szenario-Abschnitt: die beiden `<details>` „Modifikatoren" [Zeilen ~300-362] und „Einmalzahlungen in diesem Szenario" [~364-395] ersetzen; JS-Block: `onModKindChange` ersetzen)
|
||||
- Modify: `app/routers/gui.py` (`_scenario_rows`: Modifikator-Query stabil sortieren)
|
||||
- Modify: `app/templates/hilfe.html` (Szenarien-Absatz an neue Tabelle anpassen)
|
||||
- Modify: `tests/test_gui.py` (neuer Test + Anpassung `test_szenario_gui_ende_und_einmalzahlungen`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: PATCH-Endpunkte aus Task 1; `toggleEdit`, `onModTargetTypeChange`, Filter `|eur`/`|de_label`, Kontext `row.modifiers`/`row.planned_items`/`modifier_kinds`/`recurring_names`/`category_names` (alle vorhanden).
|
||||
- Produces: Element-IDs `mod-row-{id}`/`mod-edit-{id}` und `spi-row-{id}`/`spi-edit-{id}`; JS `onEntryArtChange(select)` (ersetzt `onModKindChange` vollständig — auch in den Art-Selects); Anlege-Formular mit `data-modifiers-url`/`data-planned-url` und Eintragsart-Option `value="einmal"`.
|
||||
|
||||
- [ ] **Step 1: Failing GUI-Test** — in `tests/test_gui.py` ergänzen (Imports `Scenario, ScenarioModifier, ScenarioPlannedItem` existieren dort seit A5):
|
||||
|
||||
```python
|
||||
def test_szenario_eintraege_tabelle(client, db):
|
||||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||||
sc = Scenario(name="Eintraege-GUI", description="")
|
||||
db.add(sc)
|
||||
db.flush()
|
||||
db.add(ScenarioModifier(scenario_id=sc.id, target_type="recurring",
|
||||
target_id=1, kind="ende", value=Decimal("0"),
|
||||
end_date=date(2026, 8, 31)))
|
||||
db.add(ScenarioModifier(scenario_id=sc.id, target_type="category",
|
||||
target_id=1, kind="percent", value=Decimal("25.00")))
|
||||
db.add(ScenarioPlannedItem(scenario_id=sc.id, name="Zufluss-Eintrag",
|
||||
amount=Decimal("5000.00"), due=date(2026, 7, 30)))
|
||||
db.commit()
|
||||
r = client.get("/planung").text
|
||||
assert "Einträge" in r
|
||||
# alte details-Abschnitte sind ersetzt
|
||||
assert "<summary>Modifikatoren</summary>" not in r
|
||||
assert "Einmalzahlungen in diesem Szenario" not in r
|
||||
# gemischte Zeilen in EINER Tabelle
|
||||
assert "31.08.2026" in r and "25,00" in r and "Zufluss-Eintrag" in r
|
||||
assert ">Einmalzahlung<" in r
|
||||
# Inline-Edit je Typ
|
||||
assert f'hx-patch="/api/scenarios/{sc.id}/modifiers/' in r
|
||||
assert f'hx-patch="/api/scenarios/{sc.id}/planned/' in r
|
||||
# Anlege-Formular: Eintragsart inkl. Einmalzahlung + Endpunkt-Wechsel-Attribute
|
||||
assert 'value="einmal"' in r
|
||||
assert f'data-planned-url="/api/scenarios/{sc.id}/planned"' in r
|
||||
assert "onEntryArtChange" in r and "onModKindChange" not in r
|
||||
```
|
||||
|
||||
Run: → FAIL
|
||||
|
||||
- [ ] **Step 2: `_scenario_rows` sortieren** — `app/routers/gui.py`, Modifikator-Query um `.order_by(ScenarioModifier.id)` ergänzen:
|
||||
|
||||
```python
|
||||
modifiers = session.execute(
|
||||
select(ScenarioModifier).where(ScenarioModifier.scenario_id == sc.id)
|
||||
.order_by(ScenarioModifier.id)
|
||||
).scalars().all()
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Einträge-Tabelle** — in `planning.html` die BEIDEN `<details>`-Blöcke „Modifikatoren" und „Einmalzahlungen in diesem Szenario" komplett ersetzen durch:
|
||||
|
||||
```html
|
||||
<h3>Einträge</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Was</th><th>Art</th><th>Wert/Betrag</th><th>Datum</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') %}{{ m.value|eur }}{% else %}–{% endif %}</td>
|
||||
<td>{% if m.kind == 'ende' %}{{ m.end_date.strftime('%d.%m.%Y') if m.end_date else '–' }}{% else %}–{% 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>
|
||||
<td colspan="5">
|
||||
<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>Ziel-Typ
|
||||
<select name="target_type" data-type="str" onchange="onModTargetTypeChange(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>Art
|
||||
<select name="kind" data-type="str" onchange="onEntryArtChange(this)">
|
||||
{% for k in modifier_kinds %}<option value="{{ k }}" {% if k == m.kind %}selected{% endif %}>{{ k|de_label }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Wert <input type="text" name="value" data-type="amount" value="{{ m.value|eur }}" {% if m.kind not in ('percent', 'absolute') %}disabled{% endif %}></label>
|
||||
<label>Endet am <input type="date" name="end_date" value="{{ m.end_date.isoformat() if m.end_date else '' }}" {% if m.kind != 'ende' %}disabled{% endif %}></label>
|
||||
<button type="submit">Speichern</button>
|
||||
<button type="button" onclick="toggleEdit('mod', {{ m.id }}, false)">Abbrechen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for sp in row.planned_items %}
|
||||
<tr id="spi-row-{{ sp.id }}">
|
||||
<td>{{ sp.name }}</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>
|
||||
<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>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 %}
|
||||
<tr><td colspan="5">Keine Einträge.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Anlege-Formular „Neuer Eintrag"** — direkt nach der Tabelle (Default-Art ist die erste Option `percent` → Wert aktiv, Datumsfelder/Einmal-Felder disabled):
|
||||
|
||||
```html
|
||||
<form hx-ext="json-form" hx-post="/api/scenarios/{{ sc.id }}/modifiers" hx-swap="none"
|
||||
data-modifiers-url="/api/scenarios/{{ sc.id }}/modifiers"
|
||||
data-planned-url="/api/scenarios/{{ sc.id }}/planned"
|
||||
hx-on::after-request="if(event.detail.successful){window.location.reload()}">
|
||||
<strong>Neuer Eintrag:</strong>
|
||||
<label>Eintragsart
|
||||
<select name="kind" data-type="str" onchange="onEntryArtChange(this)">
|
||||
{% for k in modifier_kinds %}<option value="{{ k }}">{{ k|de_label }}</option>{% endfor %}
|
||||
<option value="einmal">Einmalzahlung</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>
|
||||
{% for r in recurring %}<option value="{{ r.id }}">{{ r.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</span>
|
||||
<label>Ziel-Typ
|
||||
<select name="target_type" data-type="str" onchange="onModTargetTypeChange(this)">
|
||||
<option value="category">Kategorie</option>
|
||||
<option value="recurring">Wiederkehrender Posten</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Wert <input type="text" name="value" data-type="amount" value="0"></label>
|
||||
<label>Endet am <input type="date" name="end_date" disabled></label>
|
||||
<label>Name <input type="text" name="name" disabled></label>
|
||||
<label>Betrag <input type="text" name="amount" data-type="amount" placeholder="5000,00" disabled></label>
|
||||
<label>Fällig am <input type="date" name="due" disabled></label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
WICHTIG: Der Ziel-Typ-Select muss VOR den beiden Ziel-Spans stehen wie im bisherigen Formular? Nein — Reihenfolge im bisherigen Formular war Ziel-Typ, dann Spans. Diese Reihenfolge beibehalten (Ziel-Typ zuerst, dann die Spans), damit das Formular lesbar bleibt — den obigen Block entsprechend anordnen: Eintragsart, Ziel-Typ, beide Spans, Wert, Endet am, Name, Betrag, Fällig am, Button.
|
||||
|
||||
- [ ] **Step 5: JS** — im `<script>`-Block von `planning.html` die Funktion `onModKindChange` ERSETZEN durch:
|
||||
|
||||
```javascript
|
||||
function onEntryArtChange(select) {
|
||||
// Umschaltmatrix "Neuer Eintrag"/Modifikator-Edit: Felder nur per
|
||||
// disabled schalten (UX-Regel). Fehlt ein Feld (Edit-Formulare haben
|
||||
// kein name/amount/due), wird es uebersprungen.
|
||||
var form = select.closest('form');
|
||||
var art = select.value;
|
||||
var isMod = art !== 'einmal';
|
||||
function setDisabled(name, disabled) {
|
||||
var el = form.querySelector('[name="' + name + '"]');
|
||||
if (el) { el.disabled = disabled; }
|
||||
}
|
||||
setDisabled('value', !(art === 'percent' || art === 'absolute'));
|
||||
setDisabled('end_date', art !== 'ende');
|
||||
setDisabled('name', isMod);
|
||||
setDisabled('amount', isMod);
|
||||
setDisabled('due', isMod);
|
||||
var typeSelect = form.querySelector('[name="target_type"]');
|
||||
if (typeSelect) {
|
||||
typeSelect.disabled = !isMod;
|
||||
var isCategory = typeSelect.value === 'category';
|
||||
var catSelect = form.querySelector('.mod-target-category select');
|
||||
var recSelect = form.querySelector('.mod-target-recurring select');
|
||||
if (catSelect) { catSelect.disabled = !isMod || !isCategory; }
|
||||
if (recSelect) { recSelect.disabled = !isMod || isCategory; }
|
||||
}
|
||||
// POST-Ziel nur beim Anlege-Formular wechseln (Edit-Formulare patchen fix).
|
||||
if (form.hasAttribute('data-planned-url')) {
|
||||
form.setAttribute('hx-post', isMod ? form.getAttribute('data-modifiers-url')
|
||||
: form.getAttribute('data-planned-url'));
|
||||
htmx.process(form);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`onModTargetTypeChange` und `toggleEdit` bleiben unverändert.
|
||||
|
||||
- [ ] **Step 6: Bestehenden Test anpassen** — `tests/test_gui.py::test_szenario_gui_ende_und_einmalzahlungen` (aus A5 Task 7): die Assertions `"Einmalzahlungen in diesem Szenario" in r` und `f'hx-post="/api/scenarios/{sc.id}/planned"' in r` ersetzen durch:
|
||||
|
||||
```python
|
||||
assert "Einträge" in r
|
||||
assert f'data-planned-url="/api/scenarios/{sc.id}/planned"' in r
|
||||
```
|
||||
|
||||
Die übrigen Assertions (`value="ende"`, `name="end_date"`, `31.08.2026`, `Zufluss-GUI`, `5.000,00`, `30.07.2026`) bleiben gültig. Danach: `grep -n "Einmalzahlungen in diesem Szenario" tests/` → 0 Treffer.
|
||||
|
||||
- [ ] **Step 7: Hilfe anpassen** — `app/templates/hilfe.html`, im Szenarien-Absatz (aus A5) den Satzteil zu Modifikatoren/Einmalzahlungen ersetzen durch (generisch, keine echten Daten):
|
||||
|
||||
```html
|
||||
Varianten wie „Best Case“ pflegst du in der Tabelle „Einträge“ des
|
||||
Szenarios: Art „Ende“ lässt einen wiederkehrenden Posten (z. B. eine
|
||||
auslaufende Miete) zu einem Datum enden, ohne den Posten selbst zu
|
||||
ändern; „Prozent“/„Absolut“ kürzen Beträge; „Entfällt“ blendet sie ganz
|
||||
aus; „Einmalzahlung“ ergänzt einen einmaligen Zu- oder Abfluss, der nur
|
||||
in diesem Szenario zählt. Jeder Eintrag ist über „Bearbeiten“ direkt
|
||||
änderbar.
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Tests + Suite grün**
|
||||
|
||||
Run: `.venv/bin/python -m pytest tests/test_gui.py -q && .venv/bin/python -m pytest -q`
|
||||
Expected: alles PASS
|
||||
|
||||
- [ ] **Step 9: Fable-Testagent-Abnahme** — Live-Anteil (TestClient-Approximation): gemischte Tabelle rendert beide Typen in korrekter Reihenfolge; Edit-Roundtrips für Modifikator (percent→ende inkl. Datumspflicht) und Einmalzahlung mit deutscher Betragseingabe; Umschaltmatrix-Handtrace für alle 5 Eintragsarten (inkl. disabled-Zustand von `kind`-Fremdfeld beim jeweils anderen Endpunkt); keine doppelten IDs. Erst nach VERIFIED weiter.
|
||||
|
||||
- [ ] **Step 10: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/wlfb/bin && git add finance/app/templates/ finance/app/routers/gui.py finance/tests/test_gui.py
|
||||
git commit -m "feat: Szenario-Eintraege als eine editierbare Tabelle"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Release v0.7.0 — Redeploy, Live-Check, Ledger
|
||||
|
||||
**Files:**
|
||||
- Modify: `finance/VERSION` (`0.6.0` → `0.7.0`)
|
||||
- Modify: `.superpowers/sdd/progress.md` (Ledger), Plan-Häkchen in dieser Datei
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tasks 1-2 committet, Suite grün, bestehendes Live-Szenario „Best Case" (id 4) mit 3 Einträgen.
|
||||
|
||||
- [ ] **Step 1: Suite final** — `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` → alle grün.
|
||||
|
||||
- [ ] **Step 2: Version + Commit**
|
||||
|
||||
```bash
|
||||
echo "0.7.0" > /home/wlfb/bin/finance/VERSION
|
||||
cd /home/wlfb/bin && git add finance/VERSION && git commit -m "chore: Version 0.7.0"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Redeploy** — `cd /home/wlfb/bin && ./create_pod_finance.sh` (keine Migration nötig; Entrypoint-`alembic upgrade head` ist idempotent). Expected: Service aktiv, Readiness 200.
|
||||
|
||||
- [ ] **Step 4: Live-Smoke** — `/api/version` == 0.7.0 (Bearer-Key aus `~/.local/share/finance_pod/.env`); GUI-Login; `/planung` zeigt im „Best Case"-Kasten die Einträge-Tabelle mit 3 Zeilen (2× Ende, 1× Einmalzahlung) OHNE aufklappen zu müssen; kein „Modifikatoren"-Summary mehr.
|
||||
|
||||
- [ ] **Step 5: Fable-Testagent-Abnahme (Release-Gate)** — Live: zusätzlich zu Step 4 ein kompletter Roundtrip in einem WEGWERF-Szenario „SMOKE-A6" (anlegen → Eintrag percent anlegen → per PATCH auf ende mit Datum ändern → Einmalzahlung anlegen → Betrag per PATCH ändern → Szenario löschen [räumt alles ab]); Bestandsdaten unverändert (confirmed-Tx-Count, „Best Case"-Einträge unangetastet). Erst nach VERIFIED weiter.
|
||||
|
||||
- [ ] **Step 6: Ledger + Plan-Häkchen + Push**
|
||||
|
||||
```bash
|
||||
cd /home/wlfb/bin && git add docs/superpowers/plans/2026-07-20-ausbaustufe-6.md && git commit -m "docs: Ledger/Plan Ausbaustufe 6" && git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (beim Planschreiben)
|
||||
|
||||
- **Spec-Abdeckung:** API-PATCH → Task 1; Tabelle/Anlege-Formular/Inline-Edit/JS/Sortierung/Hilfe → Task 2; Release/Live-Check → Task 3. Vollständig.
|
||||
- **Platzhalter:** keine.
|
||||
- **Typ-Konsistenz:** URLs/IDs (`mod-`/`spi-`), `data-modifiers-url`/`data-planned-url`, `onEntryArtChange` konsistent zwischen Tasks 1/2; Test-Anpassung des A5-Tests explizit benannt (Step 6), damit keine Suite-Regression entsteht.
|
||||
Reference in New Issue
Block a user