diff --git a/create_pod_finance.sh b/create_pod_finance.sh index 23874bc..9ef0a15 100755 --- a/create_pod_finance.sh +++ b/create_pod_finance.sh @@ -233,6 +233,7 @@ podman run -d --name "$GRAFANA_CTR_NAME" --pod "$POD_NAME" \ -e GF_SECURITY_ADMIN_PASSWORD="$FB_PASSWORD" \ -e GF_SECURITY_ALLOW_EMBEDDING=true \ -e GF_SECURITY_COOKIE_SAMESITE=lax \ + -e GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/finanzen.json \ -e FINANCE_READ_PASSWORD \ -v "$GRAFANA_PROVISIONING_DIR:/etc/grafana/provisioning:Z,ro" \ -v "$GRAFANA_DASHBOARDS_DIR:/var/lib/grafana/dashboards:Z,ro" \ diff --git a/finance/app/routers/gui.py b/finance/app/routers/gui.py index f29a3f3..ad10a97 100644 --- a/finance/app/routers/gui.py +++ b/finance/app/routers/gui.py @@ -19,10 +19,12 @@ from app.routers.planning import list_planned as _list_planned from app.routers.planning import list_recurring as _list_recurring from app.routers.planning import recurring_suggestions as _recurring_suggestions from app.routers.scenarios import list_scenarios as _list_scenarios -from app.routers.transactions import list_transactions +from app.routers.transactions import count_transactions, list_transactions from app.services.balances import account_balance, total_balance from app.version import get_version +PAGE_SIZE = 50 + templates = Jinja2Templates(directory="app/templates") templates.env.globals["app_version"] = get_version() router = APIRouter() @@ -81,8 +83,21 @@ def buchungen( category_id: int | None = None, q: str | None = None, status: str = "confirmed", + page: int = 1, session: Session = Depends(get_session), ): + page = max(1, page) + total = count_transactions( + account_id=account_id, + date_from=date_from, + date_to=date_to, + category_id=category_id, + q=q, + status=status, + session=session, + ) + total_pages = max(1, -(-total // PAGE_SIZE)) # ceil division + page = min(page, total_pages) txs = list_transactions( account_id=account_id, date_from=date_from, @@ -90,8 +105,8 @@ def buchungen( category_id=category_id, q=q, status=status, - limit=200, - offset=0, + limit=PAGE_SIZE, + offset=(page - 1) * PAGE_SIZE, session=session, ) accounts = session.execute(select(Account)).scalars().all() @@ -108,6 +123,8 @@ def buchungen( "q": q or "", "status": status, }, + "page": page, + "total_pages": total_pages, }) diff --git a/finance/app/routers/transactions.py b/finance/app/routers/transactions.py index ed3bc6a..b30b8a5 100644 --- a/finance/app/routers/transactions.py +++ b/finance/app/routers/transactions.py @@ -3,7 +3,7 @@ from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, ConfigDict -from sqlalchemy import or_, select +from sqlalchemy import func, or_, select from sqlalchemy.orm import Session from app.auth import require_auth @@ -46,19 +46,18 @@ class TransactionPatch(BaseModel): category_id: int | None = None -@router.get("", response_model=list[TransactionOut]) -def list_transactions( +def _apply_transaction_filters( + stmt, account_id: int | None = None, date_from: date | None = None, date_to: date | None = None, category_id: int | None = None, q: str | None = None, status: str = "confirmed", - limit: int = 200, - offset: int = 0, - session: Session = Depends(get_session), ): - stmt = select(Transaction) + """Wendet die Buchungs-Filter auf ein select()-Statement an. Wird sowohl fuer + die Liste als auch fuer die dazugehoerige Gesamtanzahl (Pagination) genutzt, + damit beide garantiert dieselben Ergebnisse zaehlen/zeigen.""" if status: stmt = stmt.where(Transaction.status == status) if account_id is not None: @@ -73,10 +72,47 @@ def list_transactions( like = f"%{q}%" stmt = stmt.where(or_(Transaction.purpose.ilike(like), Transaction.counterparty.ilike(like))) + return stmt + + +@router.get("", response_model=list[TransactionOut]) +def list_transactions( + account_id: int | None = None, + date_from: date | None = None, + date_to: date | None = None, + category_id: int | None = None, + q: str | None = None, + status: str = "confirmed", + limit: int = 200, + offset: int = 0, + session: Session = Depends(get_session), +): + stmt = _apply_transaction_filters( + select(Transaction), account_id=account_id, date_from=date_from, + date_to=date_to, category_id=category_id, q=q, status=status, + ) stmt = stmt.order_by(Transaction.booking_date, Transaction.id).offset(offset).limit(limit) return [TransactionOut.model_validate(t) for t in session.execute(stmt).scalars()] +def count_transactions( + account_id: int | None = None, + date_from: date | None = None, + date_to: date | None = None, + category_id: int | None = None, + q: str | None = None, + status: str = "confirmed", + session: Session = Depends(get_session), +) -> int: + """Gesamtanzahl der Buchungen fuer die gleichen Filter wie list_transactions + (ohne limit/offset) - Grundlage fuer die Seitenzahl in der GUI-Pagination.""" + stmt = _apply_transaction_filters( + select(func.count(Transaction.id)), account_id=account_id, date_from=date_from, + date_to=date_to, category_id=category_id, q=q, status=status, + ) + return session.execute(stmt).scalar_one() + + @router.post("", response_model=TransactionOut, status_code=201) def create_transaction(data: TransactionIn, session: Session = Depends(get_session)): if session.get(Account, data.account_id) is None: diff --git a/finance/app/static/style.css b/finance/app/static/style.css index 976c440..b6fdc8b 100644 --- a/finance/app/static/style.css +++ b/finance/app/static/style.css @@ -144,6 +144,10 @@ button { margin-right: 0.5rem; } +.muted { + color: #888; +} + section.planning-section { margin-bottom: 2rem; } @@ -155,5 +159,6 @@ section.planning-section fieldset { .version { margin-top: 2rem; color: #888; - font-size: 0.85rem; + text-align: center; + font-size: 0.75rem; } diff --git a/finance/app/templates/_import_list.html b/finance/app/templates/_import_list.html index 9b43ac3..110301b 100644 --- a/finance/app/templates/_import_list.html +++ b/finance/app/templates/_import_list.html @@ -34,12 +34,20 @@ hx-on::after-request="if(event.detail.successful){htmx.trigger(document.getElementById('imports'), 'refresh')}"> - {% endif %}
+ {% elif s.status == "confirmed" %} + Bestätigt — Buchungen übernommen + {% else %} + + {% endif %} {% else %} diff --git a/finance/app/templates/import.html b/finance/app/templates/import.html index 4356d67..dcdd1c6 100644 --- a/finance/app/templates/import.html +++ b/finance/app/templates/import.html @@ -7,6 +7,11 @@ Kontoauszug-PDF hier ablegen oder klicken zum Hochladen + +Durchsucht ~/.local/share/finance_pod/data/inbox/ nach neuen PDF-Kontoauszügen.
Kein Diagramm sichtbar? Einmal in Grafana anmelden (gleiches Passwort wie hier).
diff --git a/finance/app/templates/transactions.html b/finance/app/templates/transactions.html index 6548a67..65d411c 100644 --- a/finance/app/templates/transactions.html +++ b/finance/app/templates/transactions.html @@ -87,6 +87,24 @@ ++ {% set pager_params = { + 'account_id': filters.account_id, + 'date_from': filters.date_from.isoformat() if filters.date_from else None, + 'date_to': filters.date_to.isoformat() if filters.date_to else None, + 'category_id': filters.category_id, + 'q': filters.q, + 'status': filters.status, + } %} + {% if page > 1 %} + Zurück + {% endif %} + Seite {{ page }} von {{ total_pages }} + {% if page < total_pages %} + Weiter + {% endif %} +
+