from datetime import date from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.dependencies import CurrentUser, require_role from app.models.time_entry import EntryStatus from app.models.user import User, UserRole from app.schemas.time_entry import ( BalanceResponse, ManualEntryCreate, RejectRequest, StampInRequest, StampOutRequest, TimeEntryListResponse, TimeEntryOut, TimeEntryUpdate, TimeEntryWithWarnings, WorkScheduleCreate, WorkScheduleOut, ) from app.services.time_service import time_service router = APIRouter(prefix="/time", tags=["Zeiterfassung"]) _manager_roles = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN) # ── Stempeluhr ──────────────────────────────────────────────────────────────── @router.post("/stamp-in", response_model=TimeEntryWithWarnings, status_code=201) async def stamp_in( data: StampInRequest, current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): """Einstempeln – startet einen neuen Zeiterfassungseintrag.""" entry, warnings = await time_service.stamp_in(current_user, data, db) await db.commit() await db.refresh(entry) return TimeEntryWithWarnings(entry=TimeEntryOut.model_validate(entry), warnings=warnings) @router.post("/stamp-out", response_model=TimeEntryWithWarnings) async def stamp_out( data: StampOutRequest, current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): """Ausstempeln – schließt den offenen Zeiterfassungseintrag.""" entry, warnings = await time_service.stamp_out(current_user, data.note, db) await db.commit() await db.refresh(entry) return TimeEntryWithWarnings(entry=TimeEntryOut.model_validate(entry), warnings=warnings) @router.post("/break-start", response_model=TimeEntryOut) async def break_start( current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): """Pause beginnen.""" entry = await time_service.break_start(current_user, db) await db.commit() await db.refresh(entry) return TimeEntryOut.model_validate(entry) @router.post("/break-end", response_model=TimeEntryOut) async def break_end( current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): """Pause beenden.""" entry = await time_service.break_end(current_user, db) await db.commit() await db.refresh(entry) return TimeEntryOut.model_validate(entry) # ── Heute ───────────────────────────────────────────────────────────────────── @router.get("/today", response_model=list[TimeEntryOut]) async def get_today( current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): """Alle Einträge des heutigen Tages für den aktuellen Benutzer.""" entries = await time_service.get_today(current_user, db) return [TimeEntryOut.model_validate(e) for e in entries] # ── Einträge ────────────────────────────────────────────────────────────────── @router.get("/entries", response_model=TimeEntryListResponse) async def list_entries( current_user: CurrentUser, user_id: UUID | None = Query(None), date_from: date | None = Query(None), date_to: date | None = Query(None), status: EntryStatus | None = Query(None), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), db: AsyncSession = Depends(get_db), ): total, entries = await time_service.list_entries( current_user.company_id, current_user, db, user_id=user_id, date_from=date_from, date_to=date_to, status=status, skip=skip, limit=limit, ) return TimeEntryListResponse(total=total, items=[TimeEntryOut.model_validate(e) for e in entries]) @router.post("/entries", response_model=TimeEntryWithWarnings, status_code=201) async def create_manual_entry( data: ManualEntryCreate, current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): """Manuellen Zeiterfassungseintrag anlegen.""" entry, warnings = await time_service.create_manual(data, current_user, db) await db.commit() await db.refresh(entry) return TimeEntryWithWarnings(entry=TimeEntryOut.model_validate(entry), warnings=warnings) @router.patch("/entries/{entry_id}", response_model=TimeEntryOut) async def update_entry( entry_id: UUID, data: TimeEntryUpdate, current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): """Zeiterfassungseintrag korrigieren.""" entry = await time_service.update_entry(entry_id, data, current_user, db) await db.commit() await db.refresh(entry) return TimeEntryOut.model_validate(entry) @router.post("/entries/{entry_id}/approve", response_model=TimeEntryOut) async def approve_entry( entry_id: UUID, current_user: User = require_role(*_manager_roles), db: AsyncSession = Depends(get_db), ): """Zeiterfassungseintrag genehmigen.""" entry = await time_service.approve_entry(entry_id, current_user, db) await db.commit() await db.refresh(entry) return TimeEntryOut.model_validate(entry) @router.post("/entries/{entry_id}/reject", response_model=TimeEntryOut) async def reject_entry( entry_id: UUID, data: RejectRequest, current_user: User = require_role(*_manager_roles), db: AsyncSession = Depends(get_db), ): """Zeiterfassungseintrag ablehnen.""" entry = await time_service.reject_entry(entry_id, current_user, data.rejection_note, db) await db.commit() await db.refresh(entry) return TimeEntryOut.model_validate(entry) @router.delete("/entries/{entry_id}", status_code=204) async def delete_entry( entry_id: UUID, current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): """Zeiteintrag löschen. Mitarbeiter: nur eigene offene/ausstehende Einträge. Manager: alle außer genehmigten (außer HR/Admin).""" await time_service.delete_entry(entry_id, current_user, db) await db.commit() # ── Überstundenkonto ────────────────────────────────────────────────────────── @router.get("/balance/me", response_model=BalanceResponse) async def get_own_balance( current_user: CurrentUser, period_start: date | None = Query(None), period_end: date | None = Query(None), db: AsyncSession = Depends(get_db), ): """Eigenes Überstundenkonto.""" return await time_service.get_balance(current_user.id, current_user, db, period_start, period_end) @router.get("/balance/{user_id}", response_model=BalanceResponse) async def get_balance( user_id: UUID, current_user: CurrentUser, period_start: date | None = Query(None), period_end: date | None = Query(None), db: AsyncSession = Depends(get_db), ): """Überstundenkonto für einen Benutzer.""" if user_id != current_user.id: if current_user.role == UserRole.EMPLOYEE: raise HTTPException(status_code=403, detail="Keine Berechtigung.") target_user = await db.get(User, user_id) if target_user is None or target_user.company_id != current_user.company_id: raise HTTPException(status_code=404, detail="Mitarbeiter nicht gefunden.") return await time_service.get_balance(user_id, current_user, db, period_start, period_end) # ── Arbeitspläne ────────────────────────────────────────────────────────────── @router.get("/schedules", response_model=list[WorkScheduleOut]) async def list_schedules( current_user: User = require_role(*_manager_roles), db: AsyncSession = Depends(get_db), ): schedules = await time_service.list_work_schedules(current_user.company_id, db) return [WorkScheduleOut.model_validate(s) for s in schedules] @router.post("/schedules", response_model=WorkScheduleOut, status_code=201) async def create_schedule( data: WorkScheduleCreate, current_user: User = require_role(UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN), db: AsyncSession = Depends(get_db), ): schedule = await time_service.create_work_schedule(current_user.company_id, data, db) await db.commit() await db.refresh(schedule) return WorkScheduleOut.model_validate(schedule) @router.patch("/schedules/{schedule_id}", response_model=WorkScheduleOut) async def update_schedule( schedule_id: UUID, data: WorkScheduleCreate, current_user: User = require_role(UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN), db: AsyncSession = Depends(get_db), ): from sqlalchemy import select as sa_select from app.models.work_schedule import WorkSchedule schedule = await db.scalar( sa_select(WorkSchedule).where( WorkSchedule.id == schedule_id, WorkSchedule.company_id == current_user.company_id, ) ) if not schedule: from fastapi import HTTPException raise HTTPException(status_code=404, detail="Arbeitsplan nicht gefunden") for field, value in data.model_dump().items(): setattr(schedule, field, value) await db.commit() await db.refresh(schedule) return WorkScheduleOut.model_validate(schedule) @router.delete("/schedules/{schedule_id}", status_code=204) async def delete_schedule( schedule_id: UUID, current_user: User = require_role(UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN), db: AsyncSession = Depends(get_db), ): from sqlalchemy import select as sa_select from app.models.work_schedule import WorkSchedule schedule = await db.scalar( sa_select(WorkSchedule).where( WorkSchedule.id == schedule_id, WorkSchedule.company_id == current_user.company_id, ) ) if not schedule: from fastapi import HTTPException raise HTTPException(status_code=404, detail="Arbeitsplan nicht gefunden") await db.delete(schedule) await db.commit()