"""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. Raises HTTPException if access is denied. """ # System admin can access all if current_user.is_system_admin: return # If querying specific user, must be self # (Phase 1: only self access for non-admin users) if target_user_id and target_user_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied: Cannot view other users' workload", ) # If querying by department, must be same department if department_id and department_id != current_user.department_id: 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, ) -> Optional[List[str]]: """ Filter user IDs to only those accessible by current user. Returns None if user can access all (system admin). """ # System admin can access all if current_user.is_system_admin: return 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( True, 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. 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) # Filter user_ids based on access accessible_user_ids = filter_accessible_users(current_user, 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. Returns: - Workload summary (same as heatmap) - List of tasks contributing to the workload """ # Check access check_workload_access(current_user, target_user_id=user_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