feat: Import-Seite mit Drag-and-Drop und Planungs-Seite

Ersetzt die Platzhalter-Routen aus Task 12 durch echte GUI-Seiten: /import
mit Drop-Zone (Fetch-Upload), gepollter Importliste (Status-Badges,
aufklappbarer Vorschau samt Saldo-/Duplikat-Anzeige, Uebernehmen/Verwerfen)
und /planung mit den vier Abschnitten Fixposten (inkl. Vorschlaege),
Einmalposten, Kredite (Tilgungsplan-Aufklapper) und Szenarien
(Kredit-Zuordnung, Modifikatoren, Durchrechnen mit Ergebnisanzeige).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 19:05:58 +02:00
parent 3847398055
commit afbbcbe9a5
8 changed files with 681 additions and 15 deletions

View File

@@ -10,7 +10,15 @@ 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)
ProjectionResult, RecurringItem,
ScenarioLoan, ScenarioModifier)
from app.routers.imports import list_imports as _list_imports
from app.routers.imports import preview as _preview_import
from app.routers.planning import list_loans as _list_loans
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.services.balances import account_balance, total_balance
@@ -102,12 +110,70 @@ def buchungen(
@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", {})
def import_page(request: Request, session: Session = Depends(get_session)):
return templates.TemplateResponse(request, "import.html", {
"statements": _list_imports(session=session),
})
@router.get("/import/list", dependencies=[Depends(gui_session)])
def import_list_fragment(request: Request, session: Session = Depends(get_session)):
return templates.TemplateResponse(request, "_import_list.html", {
"statements": _list_imports(session=session),
})
@router.get("/import/{statement_id}/preview-fragment", dependencies=[Depends(gui_session)])
def import_preview_fragment(statement_id: int, request: Request,
session: Session = Depends(get_session)):
result = _preview_import(statement_id, session=session)
return templates.TemplateResponse(request, "_preview_table.html", {
"statement": result.statement,
"transactions": result.transactions,
"balance_ok": result.balance_ok,
"duplicates": result.duplicates,
})
def _scenario_rows(session: Session, scenarios: list, loans: list) -> list[dict]:
"""Baut je Szenario den Kontext fürs Template: zugeordnete Kredite,
Modifikatoren und (falls vorhanden) das letzte Projektionsergebnis."""
rows = []
for sc in scenarios:
assigned_loan_ids = {
sl.loan_id for sl in session.execute(
select(ScenarioLoan).where(ScenarioLoan.scenario_id == sc.id)
).scalars()
}
modifiers = session.execute(
select(ScenarioModifier).where(ScenarioModifier.scenario_id == sc.id)
).scalars().all()
rows.append({
"scenario": sc,
"assigned_loan_ids": assigned_loan_ids,
"modifiers": modifiers,
"result": session.get(ProjectionResult, sc.id),
})
return rows
@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", {})
def planung_page(request: Request, session: Session = Depends(get_session)):
categories = session.execute(select(Category)).scalars().all()
recurring = _list_recurring(session=session)
planned = _list_planned(session=session)
loans = _list_loans(session=session)
scenarios = _list_scenarios(session=session)
return templates.TemplateResponse(request, "planning.html", {
"categories": categories,
"category_names": {c.id: c.name for c in categories},
"recurring": recurring,
"recurring_names": {r.id: r.name for r in recurring},
"suggestions": _recurring_suggestions(session=session),
"planned": planned,
"loans": loans,
"scenario_rows": _scenario_rows(session, scenarios, loans),
"rhythms": ["monthly", "quarterly", "yearly"],
"repayment_types": ["annuity", "bullet"],
"modifier_kinds": ["percent", "absolute", "remove"],
})