from datetime import date from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.dependencies import CurrentUser, require_role from app.models.project import Project from app.models.time_entry import TimeEntry, EntryStatus from app.models.user import User, UserRole from app.schemas.project import ( ProjectCreate, ProjectListResponse, ProjectOut, ProjectTimeReport, ProjectUpdate, ) router = APIRouter(prefix="/projects", tags=["Projekte"]) _manager_roles = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN) def _assert_company(project: Project, company_id: UUID) -> None: if project.company_id != company_id: raise HTTPException(404, "Projekt nicht gefunden") @router.get("", response_model=ProjectListResponse) async def list_projects( current_user: CurrentUser, db: AsyncSession = Depends(get_db), include_inactive: bool = Query(False), ): stmt = select(Project).where(Project.company_id == current_user.company_id) if not include_inactive: stmt = stmt.where(Project.is_active == True) stmt = stmt.order_by(Project.name) result = await db.scalars(stmt) items = list(result.all()) return ProjectListResponse(total=len(items), items=items) @router.post("", response_model=ProjectOut, status_code=201) async def create_project( data: ProjectCreate, current_user: User = require_role(*_manager_roles), db: AsyncSession = Depends(get_db), ): project = Project( company_id=current_user.company_id, name=data.name, description=data.description, color=data.color, budget_hours=data.budget_hours, ) db.add(project) await db.commit() await db.refresh(project) return project @router.get("/report/summary", response_model=list[ProjectTimeReport]) async def projects_summary( current_user: User = require_role(*_manager_roles), db: AsyncSession = Depends(get_db), date_from: date | None = Query(None), date_to: date | None = Query(None), ): projects = list((await db.scalars( select(Project).where(Project.company_id == current_user.company_id, Project.is_active == True) )).all()) result = [] for project in projects: stmt = ( select(TimeEntry) .join(TimeEntry.user) .where( TimeEntry.project_id == project.id, User.company_id == current_user.company_id, TimeEntry.status == EntryStatus.APPROVED, TimeEntry.end_time.is_not(None), ) ) if date_from: stmt = stmt.where(TimeEntry.date >= date_from) if date_to: stmt = stmt.where(TimeEntry.date <= date_to) entries = list((await db.scalars(stmt)).all()) total_minutes = sum(e.worked_minutes or 0 for e in entries) total_hours = round(total_minutes / 60, 2) budget_used_pct = None if project.budget_hours and float(project.budget_hours) > 0: budget_used_pct = round(total_hours / float(project.budget_hours) * 100, 1) result.append(ProjectTimeReport( project_id=project.id, project_name=project.name, project_color=project.color, total_hours=total_hours, entry_count=len(entries), budget_hours=float(project.budget_hours) if project.budget_hours else None, budget_used_pct=budget_used_pct, )) result.sort(key=lambda x: x.total_hours, reverse=True) return result @router.get("/{project_id}", response_model=ProjectOut) async def get_project( project_id: UUID, current_user: CurrentUser, db: AsyncSession = Depends(get_db), ): project = await db.get(Project, project_id) if not project: raise HTTPException(404, "Projekt nicht gefunden") _assert_company(project, current_user.company_id) return project @router.patch("/{project_id}", response_model=ProjectOut) async def update_project( project_id: UUID, data: ProjectUpdate, current_user: User = require_role(*_manager_roles), db: AsyncSession = Depends(get_db), ): project = await db.get(Project, project_id) if not project: raise HTTPException(404, "Projekt nicht gefunden") _assert_company(project, current_user.company_id) for field, value in data.model_dump(exclude_unset=True).items(): setattr(project, field, value) await db.commit() await db.refresh(project) return project @router.delete("/{project_id}", status_code=204) async def delete_project( project_id: UUID, current_user: User = require_role(*_manager_roles), db: AsyncSession = Depends(get_db), ): project = await db.get(Project, project_id) if not project: raise HTTPException(404, "Projekt nicht gefunden") _assert_company(project, current_user.company_id) project.is_active = False await db.commit() @router.get("/{project_id}/report", response_model=ProjectTimeReport) async def project_time_report( project_id: UUID, current_user: CurrentUser, db: AsyncSession = Depends(get_db), date_from: date | None = Query(None), date_to: date | None = Query(None), ): project = await db.get(Project, project_id) if not project: raise HTTPException(404, "Projekt nicht gefunden") _assert_company(project, current_user.company_id) stmt = ( select(TimeEntry) .join(TimeEntry.user) .where( TimeEntry.project_id == project_id, User.company_id == current_user.company_id, TimeEntry.status == EntryStatus.APPROVED, TimeEntry.end_time.is_not(None), ) ) if date_from: stmt = stmt.where(TimeEntry.date >= date_from) if date_to: stmt = stmt.where(TimeEntry.date <= date_to) entries = list((await db.scalars(stmt)).all()) total_minutes = sum(e.worked_minutes or 0 for e in entries) total_hours = round(total_minutes / 60, 2) budget_used_pct = None if project.budget_hours and float(project.budget_hours) > 0: budget_used_pct = round(total_hours / float(project.budget_hours) * 100, 1) return ProjectTimeReport( project_id=project.id, project_name=project.name, project_color=project.color, total_hours=total_hours, entry_count=len(entries), budget_hours=float(project.budget_hours) if project.budget_hours else None, budget_used_pct=budget_used_pct, )