Neuer PDF-Export GET /reports/datev-monthly/export: ein Blatt pro Mitarbeiter/Monat im Layout der DATEV-Stundenaufzeichnungs-Vorlage (Beginn/Pause/Ende/Dauer + K/U/UU/F/SA/SU-Kürzel + Bemerkungen + Summe + Unterschriftsfelder). Führt time_entries, Absences und Feiertage pro Kalendertag zusammen. Außerdem: Mustervorlagen für neue Backend-Module (Model/Schema/Router/ Migration/Test) und eine Frontend-Page-Vorlage, abgeleitet vom hours_payouts-Modul als aktuellstem sauberen Muster. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CviFgc739S89xS97pvjszj
233 lines
10 KiB
Python
233 lines
10 KiB
Python
from datetime import date, timedelta
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, 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/datev-monthly/export")
|
|
async def export_datev_monthly(
|
|
current_user: CurrentUser,
|
|
user_id: UUID,
|
|
year: int = Query(..., ge=2000, le=2100),
|
|
month: int = Query(..., ge=1, le=12),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""DATEV-Monatsblatt (Blanko-Layout) als PDF für Steuerberater-Übergabe.
|
|
EMPLOYEE darf nur den eigenen Monat exportieren."""
|
|
if current_user.role not in _manager_roles and user_id != current_user.id:
|
|
raise HTTPException(403, "Nur eigenes Monatsblatt exportierbar")
|
|
|
|
sheet = await report_service.datev_monthly_report(
|
|
current_user.company_id, user_id, year, month, db
|
|
)
|
|
content = report_service.datev_monthly_report_to_pdf(sheet)
|
|
filename = f"datev_stundenaufzeichnung_{sheet.user_name.replace(' ', '_')}_{year}_{month:02d}.pdf"
|
|
return Response(content=content, media_type="application/pdf",
|
|
headers={"Content-Disposition": f"attachment; filename={filename}"})
|
|
|
|
|
|
@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"})
|