Files
PROJECT-CONTORL/backend/app/api/workload/router.py
2026-01-11 08:37:21 +08:00

293 lines
9.3 KiB
Python

"""Workload API endpoints.
Provides endpoints for workload heatmap, user workload details,
and capacity management.
"""
from datetime import date
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.middleware.auth import get_current_user
from app.models.user import User
from app.schemas.workload import (
WorkloadHeatmapResponse,
UserWorkloadDetail,
CapacityUpdate,
UserWorkloadSummary,
)
from app.services.workload_service import (
get_week_bounds,
get_current_week_start,
get_workload_heatmap,
get_user_workload_detail,
)
from app.services.workload_cache import (
get_cached_heatmap,
set_cached_heatmap,
)
router = APIRouter()
def check_workload_access(
current_user: User,
target_user_id: Optional[str] = None,
target_user_department_id: Optional[str] = None,
department_id: Optional[str] = None,
) -> None:
"""
Check if current user has access to view workload data.
Access rules:
- System admin: can access all workloads
- Department manager: can access workloads of users in their department
- Regular user: can only access their own workload
Raises HTTPException if access is denied.
"""
# System admin can access all
if current_user.is_system_admin:
return
# If querying specific user
if target_user_id and target_user_id != current_user.id:
# Department manager can view subordinates' workload
if current_user.is_department_manager:
# Manager can view users in their department
# target_user_department_id must be provided for this check
if target_user_department_id and target_user_department_id == current_user.department_id:
return
# Access denied for non-manager or user not in same department
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied: Cannot view other users' workload",
)
# If querying by department
if department_id and department_id != current_user.department_id:
# Department manager can only query their own department
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied: Cannot view other departments' workload",
)
def filter_accessible_users(
current_user: User,
user_ids: Optional[List[str]] = None,
db: Optional[Session] = None,
) -> Optional[List[str]]:
"""
Filter user IDs to only those accessible by current user.
Returns None if user can access all (system admin).
Access rules:
- System admin: can see all users
- Department manager: can see all users in their department
- Regular user: can only see themselves
"""
# System admin can access all
if current_user.is_system_admin:
return user_ids
# Department manager can see all users in their department
if current_user.is_department_manager and current_user.department_id and db:
# Get all users in the same department
department_users = db.query(User.id).filter(
User.department_id == current_user.department_id,
User.is_active == True
).all()
department_user_ids = {u.id for u in department_users}
if user_ids:
# Filter to only users in manager's department
accessible = [uid for uid in user_ids if uid in department_user_ids]
if not accessible:
return [current_user.id]
return accessible
else:
# No filter specified, return all department users
return list(department_user_ids)
# Regular user can only see themselves
if user_ids:
# Filter to only accessible users
accessible = [uid for uid in user_ids if uid == current_user.id]
if not accessible:
return [current_user.id] # Default to self if no accessible users
return accessible
else:
# No filter specified, return only self
return [current_user.id]
@router.get("/heatmap", response_model=WorkloadHeatmapResponse)
async def get_heatmap(
week_start: Optional[date] = Query(
None,
description="Start of week (ISO date, defaults to current Monday)"
),
department_id: Optional[str] = Query(
None,
description="Filter by department ID"
),
user_ids: Optional[str] = Query(
None,
description="Comma-separated list of user IDs to include"
),
hide_empty: bool = Query(
False,
description="Hide users with no tasks assigned for the week"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Get workload heatmap for users.
Access Rules:
- System admin: Can view all users' workload
- Department manager: Can view workload of all users in their department
- Regular user: Can only view their own workload
Returns workload summaries for users showing:
- allocated_hours: Total estimated hours from tasks due this week
- capacity_hours: User's weekly capacity
- load_percentage: Percentage of capacity used
- load_level: normal (<80%), warning (80-99%), overloaded (>=100%)
"""
# Parse user_ids if provided
parsed_user_ids = None
if user_ids:
parsed_user_ids = [uid.strip() for uid in user_ids.split(",") if uid.strip()]
# Check department access
if department_id:
check_workload_access(current_user, department_id=department_id)
# Determine accessible users for this requester
accessible_user_ids = filter_accessible_users(current_user, None, db)
# If specific user_ids are requested, ensure access is permitted
if parsed_user_ids:
if accessible_user_ids is not None:
requested_ids = set(parsed_user_ids)
allowed_ids = set(accessible_user_ids)
if not requested_ids.issubset(allowed_ids):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied: Cannot view other users' workload",
)
accessible_user_ids = parsed_user_ids
# Normalize week_start
if week_start is None:
week_start = get_current_week_start()
else:
week_start = get_week_bounds(week_start)[0]
week_start, week_end = get_week_bounds(week_start)
# Try cache first (only use cache for default hide_empty=True)
cached = None
if hide_empty:
cached = get_cached_heatmap(week_start, department_id, accessible_user_ids)
if cached:
return WorkloadHeatmapResponse(
week_start=week_start,
week_end=week_end,
users=cached,
)
# Calculate from database
summaries = get_workload_heatmap(
db=db,
week_start=week_start,
department_id=department_id,
user_ids=accessible_user_ids,
hide_empty=hide_empty,
)
# Cache the result (only cache when hide_empty=True, the default)
if hide_empty:
set_cached_heatmap(week_start, summaries, department_id, accessible_user_ids)
return WorkloadHeatmapResponse(
week_start=week_start,
week_end=week_end,
users=summaries,
)
@router.get("/user/{user_id}", response_model=UserWorkloadDetail)
async def get_user_workload(
user_id: str,
week_start: Optional[date] = Query(
None,
description="Start of week (ISO date, defaults to current Monday)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Get detailed workload for a specific user.
Access rules:
- System admin: can view any user's workload
- Department manager: can view workload of users in their department
- Regular user: can only view their own workload
Returns:
- Workload summary (same as heatmap)
- List of tasks contributing to the workload
"""
# Get target user's department for manager access check
target_user = db.query(User).filter(User.id == user_id).first()
target_user_department_id = target_user.department_id if target_user else None
# Check access (pass target user's department for manager check)
check_workload_access(
current_user,
target_user_id=user_id,
target_user_department_id=target_user_department_id
)
# Calculate workload detail
detail = get_user_workload_detail(db, user_id, week_start)
if detail is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return detail
@router.get("/me", response_model=UserWorkloadDetail)
async def get_my_workload(
week_start: Optional[date] = Query(
None,
description="Start of week (ISO date, defaults to current Monday)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Get workload for the current authenticated user.
Convenience endpoint that doesn't require specifying user ID.
"""
detail = get_user_workload_detail(db, current_user.id, week_start)
if detail is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to calculate workload",
)
return detail