diff --git a/api/routers/v1/projects.py b/api/routers/v1/projects.py index ffe885235..8bd83dc3b 100644 --- a/api/routers/v1/projects.py +++ b/api/routers/v1/projects.py @@ -1,12 +1,14 @@ +from datetime import date from typing import List from fastapi import APIRouter, Depends, status from sqlalchemy.orm import Session from models.project import Project -from schemas.project import Project as ProjectSchema +from schemas.project import Project as ProjectSchema, ProjectAllocationInDb, BaseProjectAllocation from services.projects import ProjectService from db.db_connection import get_db +from dependencies import get_current_user from auth.auth_bearer import BearerToken router = APIRouter( @@ -28,3 +30,35 @@ async def get_projects(db: Session = Depends(get_db), offset: int = 0, limit: in @router.get("/{project_id}", response_model=ProjectSchema) async def get_project(project_id: int, db: Session = Depends(get_db)): return db.query(Project).filter(Project.id == project_id).first() + + +@router.get("/{project_id}/allocations", dependencies=[Depends(BearerToken())]) +async def get_project_allocations( + project_id: int, + start: date = date.today(), + end: date = date.today(), + current_user=Depends(get_current_user), + db: Session = Depends(get_db), +): + return ProjectService(db).get_project_allocations(project_id, start, end) + + +@router.post( + "/{project_id}/allocations", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(BearerToken())], + response_model=ProjectAllocationInDb, +) +async def add_project_allocations( + project_id: int, + allocation: BaseProjectAllocation, + current_user=Depends(get_current_user), + db: Session = Depends(get_db), +): + """ + Create a project allocation. Required fields are: `projecId` that comes from the url, + `userId`, `hoursPerDay`, `startDate`, and `endDate`. + """ + # TODO we need to validate for overlapping, overbooking, etc + result = ProjectService(db).create_project_allocation(allocation, current_user.username) + return result diff --git a/api/schemas/project.py b/api/schemas/project.py index 90c0fc7c7..634505b9c 100644 --- a/api/schemas/project.py +++ b/api/schemas/project.py @@ -1,7 +1,7 @@ from datetime import date from pydantic import ConfigDict, BaseModel from pydantic.alias_generators import to_camel -from typing import Optional +from typing import Optional, Dict class Project(BaseModel): @@ -18,4 +18,30 @@ class Project(BaseModel): customer_id: int customer_name: Optional[str] = None area_id: int + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + +class BaseProjectAllocation(BaseModel): + user_id: int + project_id: int + start_date: Optional[date] = None + end_date: Optional[date] = None + hours_per_day: Optional[float] = None + fte: Optional[float] = None + is_tentative: Optional[bool] = None + is_billable: Optional[bool] = None + notes: Optional[str] = None + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + +class ProjectAllocationInDb(BaseProjectAllocation): + id: int + total_hours: float + username: str + + +class ProjectAllocationPerUser(BaseModel): + username: str + hours: Dict diff --git a/api/services/projects.py b/api/services/projects.py index 4952952d2..ca3f2698a 100644 --- a/api/services/projects.py +++ b/api/services/projects.py @@ -1,7 +1,40 @@ from typing import List - +from datetime import date, datetime, timedelta from services.main import AppService -from models.project import Project +from models.project import Project, ProjectAllocation +from schemas.project import BaseProjectAllocation, ProjectAllocationPerUser + + +def group_allocations_per_user(allocations): + allocations_grouped_per_user = {} + + for allocation in allocations: + username = allocation.username + current_date = allocation.start_date + while current_date <= allocation.end_date: + iso_week = current_date.isocalendar()[1] + month_name = current_date.strftime("%b") + day_of_week = current_date.weekday() + days_until_monday = (day_of_week - 0) % 7 + first_day_of_week = current_date - timedelta(days=days_until_monday) + week_key = f"{first_day_of_week.year}-{iso_week}" + if username not in allocations_grouped_per_user: + allocations_grouped_per_user[username] = {"username": username, "hours": {}} + if week_key not in allocations_grouped_per_user[username]["hours"]: + allocations_grouped_per_user[username]["hours"][week_key] = { + "days": [], + "ISOWeek": iso_week, + "month": month_name, + "totalHours": 0, + "project": allocation.project_id, + "isLeave": False, # TODO Replace with the logic to determine if it's leave + } + if day_of_week < 5: + allocations_grouped_per_user[username]["hours"][week_key]["totalHours"] += allocation.hours_per_day + allocations_grouped_per_user[username]["hours"][week_key]["days"].append(str(current_date)) + + current_date += timedelta(days=1) + return list(allocations_grouped_per_user.values()) class ProjectService(AppService): @@ -18,3 +51,33 @@ def is_project_active(self, project_id) -> bool: if project is not None: return project.is_active return False + + def get_project_allocations( + self, project_id: int, start: date = None, end: date = None + ) -> List[ProjectAllocationPerUser]: + # TODO improve the date filter logic to get more precise results + query = self.db.query(ProjectAllocation).where( + ProjectAllocation.project_id == project_id, ProjectAllocation.start_date.between(start, end) + ) + allocations = query.all() or [] + + return group_allocations_per_user(allocations) + + def create_project_allocation(self, allocation: BaseProjectAllocation, created_by: str) -> ProjectAllocation: + new_allocation = ProjectAllocation( + user_id=allocation.user_id, + project_id=allocation.project_id, + start_date=allocation.start_date, + end_date=allocation.end_date, + hours_per_day=allocation.hours_per_day, + fte=allocation.fte, + is_tentative=allocation.is_tentative, + is_billable=allocation.is_billable, + notes=allocation.notes, + created_at=datetime.now(), + created_by=created_by, + ) + self.db.add(new_allocation) + self.db.commit() + self.db.refresh(new_allocation) + return new_allocation