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>
This commit is contained in:
sysops
2026-05-23 20:03:27 +02:00
commit 1fedd683e0
178 changed files with 29896 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
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"})