PMFlow/backend/app/api/workloads.py

197 lines
5.5 KiB
Python

"""工作负载 API。"""
from __future__ import annotations
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from ..database import get_db
from ..dependencies import (
get_current_user,
get_tenant_id,
require_manager_user,
require_roles,
require_self_or_manager,
)
from ..models import User
from ..services import workload_service
router = APIRouter(prefix="/workloads", tags=["workloads"])
class WorkloadHoursUpdateRequest(BaseModel):
max_weekly_hours: float = Field(..., gt=0, le=120)
class AIOptimizePreviewRequest(BaseModel):
project_id: Optional[int] = None
max_moves: int = Field(default=20, ge=1, le=100)
class AIOptimizeApplyRequest(BaseModel):
proposals: list[dict]
@router.get("/")
async def get_team_workloads(
project_id: Optional[int] = Query(None),
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_roles("BOSS", "ADMIN")),
db: AsyncSession = Depends(get_db),
):
_ = current_user
rows = await workload_service.get_team_workload_board(
db,
tenant_id=tenant_id,
project_id=project_id,
)
total = len(rows)
avg_ratio = round(sum(float(r.get("workload_ratio", 0)) for r in rows) / total, 2) if total else 0.0
overloaded = sum(1 for r in rows if float(r.get("workload_ratio", 0)) > 100)
idle = sum(1 for r in rows if float(r.get("workload_ratio", 0)) < 70)
return {
"items": rows,
"total": total,
"summary": {
"avg_ratio": avg_ratio,
"overloaded_count": overloaded,
"idle_count": idle,
"normal_count": max(0, total - overloaded - idle),
},
}
@router.get("/summary")
async def get_workload_summary(
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
_ = current_user
rows = await workload_service.get_team_workload_board(db, tenant_id=tenant_id)
total = len(rows)
avg_ratio = round(sum(float(r.get("workload_ratio", 0)) for r in rows) / total, 2) if total else 0.0
return {
"team_size": total,
"avg_ratio": avg_ratio,
"overloaded_count": sum(1 for r in rows if float(r.get("workload_ratio", 0)) > 100),
"normal_count": sum(
1
for r in rows
if 70 <= float(r.get("workload_ratio", 0)) <= 100
),
"idle_count": sum(1 for r in rows if float(r.get("workload_ratio", 0)) < 70),
}
@router.get("/{user_id}")
async def get_user_workload(
user_id: int,
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_self_or_manager),
db: AsyncSession = Depends(get_db),
):
_ = current_user
item = await workload_service.get_user_personal_workload(db, tenant_id, user_id)
if not item:
raise HTTPException(status_code=404, detail="User workload not found")
return item
@router.put("/{user_id}")
async def update_weekly_hours(
user_id: int,
body: WorkloadHoursUpdateRequest,
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_self_or_manager),
db: AsyncSession = Depends(get_db),
):
_ = current_user
result = await workload_service.update_user_max_weekly_hours(
db,
tenant_id=tenant_id,
user_id=user_id,
max_weekly_hours=body.max_weekly_hours,
)
if not result:
raise HTTPException(status_code=404, detail="User not found")
return result
@router.get("/{user_id}/tasks")
async def get_user_workload_tasks(
user_id: int,
status_value: Optional[str] = Query(None, alias="status"),
priority: Optional[str] = Query(None),
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_self_or_manager),
db: AsyncSession = Depends(get_db),
):
_ = current_user
items = await workload_service.list_user_assigned_tasks(
db,
tenant_id=tenant_id,
user_id=user_id,
status=status_value,
priority=priority,
)
return {"items": items, "total": len(items)}
@router.get("/{user_id}/trend")
async def get_user_workload_trend(
user_id: int,
days: int = Query(7, ge=1, le=30),
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_self_or_manager),
db: AsyncSession = Depends(get_db),
):
_ = current_user
items = await workload_service.get_user_workload_trend(
db,
tenant_id=tenant_id,
user_id=user_id,
days=days,
)
return {"items": items, "total": len(items)}
@router.post("/ai-optimize")
async def ai_optimize_preview(
body: AIOptimizePreviewRequest,
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_manager_user),
db: AsyncSession = Depends(get_db),
):
_ = current_user
proposals = await workload_service.build_ai_optimize_preview(
db,
tenant_id=tenant_id,
project_id=body.project_id,
max_moves=body.max_moves,
)
return {"proposals": proposals, "total": len(proposals)}
@router.post("/ai-optimize/apply")
async def apply_ai_optimize(
body: AIOptimizeApplyRequest,
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_manager_user),
db: AsyncSession = Depends(get_db),
):
_ = current_user
result = await workload_service.apply_ai_optimize_plan(
db,
tenant_id=tenant_id,
proposals=body.proposals,
)
return result