Files
timemaster/backend/app/routers/reports.py
T
sysops 1fedd683e0 Initial commit – TimeMaster Zeiterfassung & HR-Tool
Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:27 +02:00

211 lines
9.1 KiB
Python

from datetime import date, timedelta
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.dependencies import CurrentUser, require_role
from app.models.user import User, UserRole
from app.schemas.report import (
AbsenceReport,
CompanyDashboard,
EmployeeDashboard,
OvertimeReport,
OvertimeReportDetailed,
TeamDashboard,
TimeReport,
)
from app.services.report_service import report_service
router = APIRouter(tags=["Dashboard & Reports"])
_manager_roles = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
_admin_roles = (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
# ── Dashboard ──────────────────────────────────────────────────────────────────
@router.get("/dashboard/me", response_model=EmployeeDashboard)
async def my_dashboard(
current_user: CurrentUser,
db: AsyncSession = Depends(get_db),
):
"""Mitarbeiter-Dashboard: eigene Stunden, Urlaub, Status heute."""
return await report_service.employee_dashboard(current_user, db)
@router.get("/dashboard/team", response_model=TeamDashboard)
async def team_dashboard(
current_user: User = require_role(*_manager_roles),
db: AsyncSession = Depends(get_db),
):
"""Team-Dashboard: Anwesenheit, ausstehende Genehmigungen."""
return await report_service.team_dashboard(current_user, db)
@router.get("/dashboard/company", response_model=CompanyDashboard)
async def company_dashboard(
current_user: User = require_role(*_admin_roles),
db: AsyncSession = Depends(get_db),
):
"""Unternehmens-Dashboard: Gesamtübersicht, Überstunden, kommende Abwesenheiten."""
return await report_service.company_dashboard(current_user, db)
# ── Reports ────────────────────────────────────────────────────────────────────
def _default_date_from() -> date:
today = date.today()
return today.replace(day=1)
def _default_date_to() -> date:
return date.today()
@router.get("/reports/time", response_model=TimeReport)
async def time_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Zeiterfassungsbericht (JSON). EMPLOYEE sieht nur eigene Einträge."""
return await report_service.time_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
@router.get("/reports/absences", response_model=AbsenceReport)
async def absence_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Abwesenheitsbericht (JSON). EMPLOYEE sieht nur eigene Abwesenheiten."""
return await report_service.absence_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
@router.get("/reports/overtime", response_model=OvertimeReport)
async def overtime_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Überstundenbericht (JSON). EMPLOYEE sieht nur eigene Daten."""
return await report_service.overtime_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
@router.get("/reports/overtime/detail", response_model=OvertimeReportDetailed)
async def overtime_report_detail(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Erweiterter Überstundenbericht mit Wochen- und Tagesaufschlüsselung."""
return await report_service.overtime_report_detail(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
# ── Export ─────────────────────────────────────────────────────────────────────
@router.get("/reports/time/export")
async def export_time_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
format: str = Query("csv", pattern="^(csv|xlsx|pdf)$"),
db: AsyncSession = Depends(get_db),
):
"""Zeiterfassungsbericht als CSV, XLSX oder PDF herunterladen."""
report = await report_service.time_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
filename = f"zeiterfassung_{date_from}_{date_to}"
if format == "pdf":
content = report_service.time_report_to_pdf(report)
return Response(content=content, media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}.pdf"})
if format == "xlsx":
content = report_service.to_xlsx(report_service._time_rows_to_dicts(report.rows), sheet_name="Zeiterfassung")
return Response(content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}.xlsx"})
content = report_service.to_csv(report_service._time_rows_to_dicts(report.rows))
return Response(content=content, media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"})
@router.get("/reports/absences/export")
async def export_absence_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
format: str = Query("csv", pattern="^(csv|xlsx|pdf)$"),
db: AsyncSession = Depends(get_db),
):
"""Abwesenheitsbericht als CSV, XLSX oder PDF herunterladen."""
report = await report_service.absence_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
filename = f"abwesenheiten_{date_from}_{date_to}"
if format == "pdf":
content = report_service.absence_report_to_pdf(report)
return Response(content=content, media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}.pdf"})
if format == "xlsx":
content = report_service.to_xlsx(report_service._absence_rows_to_dicts(report.rows), sheet_name="Abwesenheiten")
return Response(content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}.xlsx"})
content = report_service.to_csv(report_service._absence_rows_to_dicts(report.rows))
return Response(content=content, media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"})
@router.get("/reports/overtime/export")
async def export_overtime_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
format: str = Query("csv", pattern="^(csv|xlsx|pdf)$"),
db: AsyncSession = Depends(get_db),
):
"""Überstundenbericht als CSV, XLSX oder PDF herunterladen (Detailansicht)."""
detail = await report_service.overtime_report_detail(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
filename = f"ueberstunden_{date_from}_{date_to}"
if format == "pdf":
content = report_service.overtime_detail_to_pdf(detail)
return Response(content=content, media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}.pdf"})
if format == "xlsx":
content = report_service.to_xlsx(report_service._overtime_detail_to_dicts(detail), sheet_name="Überstunden")
return Response(content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}.xlsx"})
content = report_service.to_csv(report_service._overtime_detail_to_dicts(detail))
return Response(content=content, media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"})