Files
timemaster/backend/app/routers/projects.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

207 lines
6.5 KiB
Python

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,
)