"""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