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, ) accounts = session.execute(select(Account)).scalars().all() return templates.TemplateResponse(request, "transactions.html", { "transactions": txs, "accounts": accounts, "account_names": {a.id: a.name for a in accounts}, "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", {})