197 lines
5.5 KiB
Python
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
|