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:
@@ -0,0 +1,206 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user