feat: Web-GUI Basis, Uebersicht, Buchungsliste
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
111
finance/app/routers/gui.py
Normal file
111
finance/app/routers/gui.py
Normal 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", {})
|
||||
Reference in New Issue
Block a user