From 36d3e4fc59a9b7054753acf0c8d2d94fa25ae74b4fae20e816bd4895fa92ebfe Mon Sep 17 00:00:00 2001 From: wlfb Date: Sun, 19 Jul 2026 14:50:39 +0200 Subject: [PATCH] feat: Konten umbenennbar, Betrag-Spalte mit Euro im Kopf Co-Authored-By: Claude Fable 5 --- finance/app/routers/accounts.py | 27 +++++++++++++++++- finance/app/static/style.css | 9 ++++++ finance/app/templates/base.html | 30 +++++++++++++++++++ finance/app/templates/index.html | 13 +++++++-- finance/app/templates/transactions.html | 38 ++----------------------- finance/tests/test_crud_api.py | 12 ++++++++ 6 files changed, 90 insertions(+), 39 deletions(-) diff --git a/finance/app/routers/accounts.py b/finance/app/routers/accounts.py index 93ff18f..948ae02 100644 --- a/finance/app/routers/accounts.py +++ b/finance/app/routers/accounts.py @@ -1,7 +1,7 @@ from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, field_validator from sqlalchemy import select from sqlalchemy.orm import Session @@ -27,6 +27,18 @@ class AccountOut(AccountIn): balance: Decimal = Decimal("0") +class AccountPatch(BaseModel): + name: str + + @field_validator("name") + @classmethod + def name_not_blank(cls, v: str) -> str: + v = v.strip() + if not v or len(v) > 100: + raise ValueError("Name muss 1–100 Zeichen lang sein") + return v + + @router.get("", response_model=list[AccountOut]) def list_accounts(session: Session = Depends(get_session)): out = [] @@ -56,3 +68,16 @@ def create_account(data: AccountIn, session: Session = Depends(get_session)): session.commit() session.refresh(acc) return AccountOut.model_validate(acc) + + +@router.patch("/{account_id}", response_model=AccountOut) +def patch_account(account_id: int, data: AccountPatch, session: Session = Depends(get_session)): + acc = session.get(Account, account_id) + if acc is None: + raise HTTPException(404, "Konto nicht gefunden") + acc.name = data.name + session.commit() + session.refresh(acc) + item = AccountOut.model_validate(acc) + item.balance = account_balance(session, acc) + return item diff --git a/finance/app/static/style.css b/finance/app/static/style.css index 1a0313b..86df18f 100644 --- a/finance/app/static/style.css +++ b/finance/app/static/style.css @@ -51,6 +51,15 @@ tr:nth-child(even) { color: #b00; } +.amount { + text-align: right; +} + +td.account { + word-break: break-all; + max-width: 12rem; +} + .warning { background: #fff3cd; border: 1px solid #e0c26b; diff --git a/finance/app/templates/base.html b/finance/app/templates/base.html index 38ba76a..daedb4f 100644 --- a/finance/app/templates/base.html +++ b/finance/app/templates/base.html @@ -25,6 +25,36 @@ document.body.addEventListener('htmx:responseError', function (e) { alert('Fehler: ' + (e.detail.xhr.responseText || e.detail.xhr.status)); }); + + // Minimale JSON-Kodierung für htmx-Formulare/Selects, die gegen die JSON-APIs + // (POST/PATCH /api/...) senden. Felder mit data-type="int" werden als Zahl + // kodiert, leere Werte als null, alles andere bleibt String. + htmx.defineExtension('json-form', { + onEvent: function (name, evt) { + if (name === 'htmx:configRequest') { + evt.detail.headers['Content-Type'] = 'application/json'; + } + }, + encodeParameters: function (xhr, parameters, elt) { + var out = {}; + for (var k in parameters) { + var v = parameters[k]; + var field = elt.matches && elt.matches('[name="' + k + '"]') + ? elt + : elt.querySelector('[name="' + k + '"]'); + var kind = field && field.dataset ? field.dataset.type : null; + if (v === '') { + out[k] = null; + } else if (kind === 'int') { + out[k] = parseInt(v, 10); + } else { + out[k] = v; + } + } + xhr.overrideMimeType('text/json'); + return JSON.stringify(out); + } + }); diff --git a/finance/app/templates/index.html b/finance/app/templates/index.html index 9b6408d..38a3515 100644 --- a/finance/app/templates/index.html +++ b/finance/app/templates/index.html @@ -16,17 +16,24 @@ - + {% for account, balance in balances %} - + + {% else %} - + {% endfor %}
KontoBankSaldo
KontoBankSaldoUmbenennen
{{ account.name }} {{ account.bank }} {{ '%.2f'|format(balance) }} € +
+ + +
+
Keine Konten angelegt.
Keine Konten angelegt.
diff --git a/finance/app/templates/transactions.html b/finance/app/templates/transactions.html index 9c8d8b6..6548a67 100644 --- a/finance/app/templates/transactions.html +++ b/finance/app/templates/transactions.html @@ -43,17 +43,17 @@ DatumKontoVerwendungszweckEmpfänger/Zahler - BetragKategorie + Betrag (€)Kategorie {% for tx in transactions %} {{ tx.booking_date.strftime('%d.%m.%Y') }} - {{ account_names.get(tx.account_id, tx.account_id) }} + {{ account_names.get(tx.account_id, tx.account_id) }} {{ tx.purpose }} {{ tx.counterparty }} - {{ '%.2f'|format(tx.amount) }} € + {{ '%.2f'|format(tx.amount) }}