"""Reports API — dashboard KPIs, weekly data, report generation, and download.""" import logging import os from datetime import date from io import BytesIO from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from app.core.dependencies import get_current_user, require_admin from app.database import get_db from app.models.report import WeeklyReport from app.models.user import User from app.schemas.report import ( DashboardResponse, ReportListResponse, ReportMeta, ) from app.services.audit_service import log_action logger = logging.getLogger(__name__) router = APIRouter() @router.get("/dashboard", response_model=DashboardResponse) def dashboard( jahr: int | None = Query(None), db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """Return dashboard KPIs and weekly time-series for the given year. Defaults to the current ISO year if *jahr* is not provided. Accessible to both admin and dak_mitarbeiter users. """ if not jahr: from app.utils.kw_utils import date_to_jahr jahr = date_to_jahr(date.today()) try: from app.services.report_service import ( calculate_dashboard_kpis, calculate_sheet1_data, ) kpis = calculate_dashboard_kpis(db, jahr) sheet1 = calculate_sheet1_data(db, jahr) return DashboardResponse(kpis=kpis, weekly=sheet1.get("weekly", [])) except ImportError: # report_service not yet implemented (parallel task) raise HTTPException(501, "Report service not yet available") @router.get("/weekly/{jahr}/{kw}") def weekly_report( jahr: int, kw: int, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """Return aggregated data for a single calendar week. Accessible to both admin and dak_mitarbeiter users. """ try: from app.services.report_service import calculate_sheet1_data data = calculate_sheet1_data(db, jahr) weekly = [w for w in data.get("weekly", []) if w.get("kw") == kw] return weekly[0] if weekly else {"kw": kw, "erstberatungen": 0} except ImportError: raise HTTPException(501, "Report service not yet available") @router.post("/generate", response_model=ReportMeta) def generate_report( jahr: int | None = Query(None), kw: int | None = Query(None), db: Session = Depends(get_db), user: User = Depends(require_admin), ): """Generate a full Berichtswesen Excel report and persist it to disk + DB. Admin only. Defaults to the current ISO year/week if not specified. Depends on report_service, excel_export, and vorjahr_service (parallel tasks). """ if not jahr: from app.utils.kw_utils import date_to_jahr, date_to_kw today = date.today() jahr = date_to_jahr(today) kw = kw or date_to_kw(today) if not kw: from app.utils.kw_utils import date_to_kw kw = date_to_kw(date.today()) try: from app.services.excel_export import generate_berichtswesen_xlsx from app.services.report_service import generate_full_report from app.services.vorjahr_service import get_vorjahr_summary report_data = generate_full_report(db, jahr, kw) vorjahr = get_vorjahr_summary(db, jahr) xlsx_bytes = generate_berichtswesen_xlsx(report_data, jahr, vorjahr) # Persist Excel file to disk reports_dir = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "reports", ) os.makedirs(reports_dir, exist_ok=True) filename = f"Berichtswesen_{jahr}_KW{kw:02d}.xlsx" filepath = os.path.join(reports_dir, filename) with open(filepath, "wb") as f: f.write(xlsx_bytes) # Upsert report metadata (replace if same jahr/kw exists) report = ( db.query(WeeklyReport) .filter(WeeklyReport.jahr == jahr, WeeklyReport.kw == kw) .first() ) if report: report.report_date = date.today() report.report_data = report_data report.report_file_path = filepath report.generated_by = user.id else: report = WeeklyReport( jahr=jahr, kw=kw, report_date=date.today(), report_data=report_data, generated_by=user.id, ) report.report_file_path = filepath db.add(report) db.commit() db.refresh(report) log_action( db, user_id=user.id, action="report_generated", entity_type="report", entity_id=report.id, new_values={"jahr": jahr, "kw": kw, "filename": filename}, ) return ReportMeta.model_validate(report) except ImportError as exc: raise HTTPException(501, f"Required service not yet available: {exc}") @router.get("/download/{report_id}") def download_report( report_id: int, request: Request, token: str | None = Query(None), db: Session = Depends(get_db), ): """Download a previously generated Berichtswesen Excel file. Supports both ``Authorization: Bearer`` header and ``?token=`` query parameter so the browser can open the URL directly in a new tab. """ from app.core.security import decode_access_token from jose import JWTError # Resolve token from header or query param raw_token = token if not raw_token: auth = request.headers.get("authorization", "") if auth.lower().startswith("bearer "): raw_token = auth[7:] if not raw_token: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated") try: payload = decode_access_token(raw_token) user_id = int(payload["sub"]) except (JWTError, KeyError, ValueError): raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token") user = db.query(User).filter(User.id == user_id, User.is_active == True).first() # noqa: E712 if not user: raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User not found") report = db.query(WeeklyReport).filter(WeeklyReport.id == report_id).first() if not report or not report.report_file_path: raise HTTPException(404, "Report not found") if not os.path.exists(report.report_file_path): raise HTTPException(404, "Report file not found on disk") with open(report.report_file_path, "rb") as f: content = f.read() filename = os.path.basename(report.report_file_path) return StreamingResponse( BytesIO(content), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) @router.delete("/delete") def delete_reports( ids: list[int], db: Session = Depends(get_db), user: User = Depends(require_admin), ): """Delete one or more reports by ID (admin only). Removes both the database record and the file on disk. """ deleted = 0 for report_id in ids: report = db.query(WeeklyReport).filter(WeeklyReport.id == report_id).first() if not report: continue if report.report_file_path and os.path.exists(report.report_file_path): os.remove(report.report_file_path) db.delete(report) deleted += 1 if deleted: db.commit() log_action( db, user_id=user.id, action="reports_deleted", entity_type="report", new_values={"ids": ids, "deleted": deleted}, ) return {"deleted": deleted} @router.get("/list", response_model=ReportListResponse) def list_reports( db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """List all generated reports, newest first. Accessible to both admin and dak_mitarbeiter users. """ reports = ( db.query(WeeklyReport).order_by(WeeklyReport.generated_at.desc()).all() ) return ReportListResponse( items=[ReportMeta.model_validate(r) for r in reports], total=len(reports), )