feat: Web-GUI Basis, Uebersicht, Buchungsliste

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 18:45:54 +02:00
parent e8ea948b38
commit a6009c47e3
11 changed files with 488 additions and 9 deletions

111
finance/app/routers/gui.py Normal file
View File

@@ -0,0 +1,111 @@
from datetime import date, timedelta
from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.templating import Jinja2Templates
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.auth import COOKIE, session_valid
from app.db import get_session
from app.engine.recurrence import occurrences
from app.models.tables import (Account, Category, PlannedItem,
ProjectionResult, RecurringItem)
from app.routers.transactions import list_transactions
from app.services.balances import account_balance, total_balance
templates = Jinja2Templates(directory="app/templates")
router = APIRouter()
def gui_session(request: Request):
if not session_valid(request.cookies.get(COOKIE)):
raise _redirect()
def _redirect():
exc = HTTPException(302)
exc.headers = {"Location": "/login"}
return exc
def _upcoming_items(session: Session, today: date, days: int = 30, limit: int = 10) -> list[dict]:
"""Nächste Posten aus RecurringItems (Termine) und PlannedItems der nächsten `days` Tage."""
end = today + timedelta(days=days)
items: list[dict] = []
for r in session.execute(select(RecurringItem)).scalars():
for d in occurrences(r.rhythm, r.due_day, today, end, r.start_date, r.end_date):
items.append({"date": d, "name": r.name, "amount": Decimal(r.amount)})
for p in session.execute(
select(PlannedItem).where(PlannedItem.due >= today, PlannedItem.due <= end)
).scalars():
items.append({"date": p.due, "name": p.name, "amount": Decimal(p.amount)})
items.sort(key=lambda i: i["date"])
return items[:limit]
def _latest_projection(session: Session) -> ProjectionResult | None:
return session.execute(
select(ProjectionResult).order_by(ProjectionResult.computed_at.desc())
).scalars().first()
@router.get("/", dependencies=[Depends(gui_session)])
def index(request: Request, session: Session = Depends(get_session)):
accounts = session.execute(select(Account)).scalars().all()
balances = [(a, account_balance(session, a)) for a in accounts]
return templates.TemplateResponse(request, "index.html", {
"balances": balances,
"total": total_balance(session),
"upcoming": _upcoming_items(session, date.today()),
"projection": _latest_projection(session),
})
@router.get("/buchungen", dependencies=[Depends(gui_session)])
def buchungen(
request: Request,
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),
):
txs = list_transactions(
account_id=account_id,
date_from=date_from,
date_to=date_to,
category_id=category_id,
q=q,
status=status,
limit=200,
offset=0,
session=session,
)
return templates.TemplateResponse(request, "transactions.html", {
"transactions": txs,
"accounts": session.execute(select(Account)).scalars().all(),
"categories": session.execute(select(Category)).scalars().all(),
"filters": {
"account_id": account_id,
"date_from": date_from,
"date_to": date_to,
"category_id": category_id,
"q": q or "",
"status": status,
},
})
@router.get("/import", dependencies=[Depends(gui_session)])
def import_page(request: Request):
# Platzhalter, wird in Task 13 mit Upload-/Vorschau-Funktion gefüllt.
return templates.TemplateResponse(request, "import.html", {})
@router.get("/planung", dependencies=[Depends(gui_session)])
def planung_page(request: Request):
# Platzhalter, wird in Task 13 mit Fixposten-/Szenario-Verwaltung gefüllt.
return templates.TemplateResponse(request, "planung.html", {})