From 6bfdd25cd877964dd79e521672d058669abcf07c Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Fri, 18 Sep 2026 17:14:12 -0400 Subject: [PATCH] fixing the company and schedule endpoints --- pyproject.toml | 1 + src/scheduler/api/company_routes.py | 35 ++++++++- src/scheduler/api/employee_routes.py | 53 +++++++++++-- src/scheduler/api/schedule_routes.py | 57 +++++++++++++- src/scheduler/core/schemas/schedule.py | 19 ++++- .../core/services/schedule_service.py | 21 +++++- src/scheduler/database/schedule_repository.py | 74 +++++++++++++++++-- 7 files changed, 231 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 630dd0d..716f6df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "psycopg[binary]>=3.2", "python-dotenv>=1.0", "uvicorn[standard]>=0.37", + "python-multipart>=0.0.9", ] [project.urls] diff --git a/src/scheduler/api/company_routes.py b/src/scheduler/api/company_routes.py index 6abc51b..d04f4d8 100644 --- a/src/scheduler/api/company_routes.py +++ b/src/scheduler/api/company_routes.py @@ -1,12 +1,15 @@ +import codecs +import csv from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, FastAPI, File, HTTPException, UploadFile from scheduler.api.dependencies import get_company_service from scheduler.core.schemas.company import Company, CreateCompanyRequest from scheduler.core.services.company_service import CompanyService router = APIRouter(prefix="/companies", tags=["companies"]) +app = FastAPI() @router.get("", response_model=list[Company]) @@ -15,12 +18,34 @@ def list_companies(service: Annotated[CompanyService, Depends(get_company_servic @router.post("", response_model=Company) -def create_company(payload: CreateCompanyRequest, service: Annotated[CompanyService, Depends(get_company_service)]): +def create_company( + payload: CreateCompanyRequest, + service: Annotated[CompanyService, Depends(get_company_service)], +): return service.create_company(payload.name) +@router.post("/upload") +def upload( + service: Annotated[CompanyService, Depends(get_company_service)], + file: UploadFile = File(...), +): + csvReader = csv.DictReader(codecs.iterdecode(file.file, "utf-8")) + data = {} + idx = 0 + for rows in csvReader: + service.create_company(rows["name"]) + data[idx] = rows["name"] + idx += 1 + + file.file.close() + return data + + @router.get("/{id}", response_model=Company) -def get_company(id: int, service: Annotated[CompanyService, Depends(get_company_service)]): +def get_company( + id: int, service: Annotated[CompanyService, Depends(get_company_service)] +): company = service.get_company(id) if company is None: raise HTTPException(status_code=404, detail="Company not found") @@ -28,7 +53,9 @@ def get_company(id: int, service: Annotated[CompanyService, Depends(get_company_ @router.delete("/{id}", response_model=Company) -def delete_company(id: int, service: Annotated[CompanyService, Depends(get_company_service)]): +def delete_company( + id: int, service: Annotated[CompanyService, Depends(get_company_service)] +): company = service.delete_company(id) if company is None: raise HTTPException(status_code=404, detail="Company not found") diff --git a/src/scheduler/api/employee_routes.py b/src/scheduler/api/employee_routes.py index 6917967..ae80dc5 100644 --- a/src/scheduler/api/employee_routes.py +++ b/src/scheduler/api/employee_routes.py @@ -1,6 +1,8 @@ +import codecs +import csv from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, FastAPI, File, HTTPException, UploadFile from scheduler.api.dependencies import get_employee_service from scheduler.core.schemas.company import CompanyUser @@ -15,6 +17,7 @@ from scheduler.core.schemas.employee import ( from scheduler.core.services.employee_service import EmployeeService router = APIRouter(tags=["employees"]) +app = FastAPI() @router.get("/employees", response_model=list[EmployeeResponse]) @@ -23,20 +26,46 @@ def list_employees(service: Annotated[EmployeeService, Depends(get_employee_serv @router.post("/employees", response_model=EmployeeResponse) -def create_employee(payload: CreateEmployeeRequest, service: Annotated[EmployeeService, Depends(get_employee_service)]): - employee = service.create_employee(payload.company_id, payload.name, payload.last_name, payload.preference) +def create_employee( + payload: CreateEmployeeRequest, + service: Annotated[EmployeeService, Depends(get_employee_service)], +): + employee = service.create_employee( + payload.company_id, payload.name, payload.last_name, payload.preference + ) if employee is None: raise HTTPException(status_code=404, detail="Company not found") return employee +@router.post("/employees/upload") +def upload( + service: Annotated[EmployeeService, Depends(get_employee_service)], + file: UploadFile = File(...), +): + csvReader = csv.DictReader(codecs.iterdecode(file.file, "utf-8")) + data = {} + idx = 0 + for rows in csvReader: + service.create_employee( + int(rows["company_id"]), rows["name"], rows["last_name"], rows["preference"] + ) + data[idx] = rows["name"] + idx += 1 + + file.file.close() + return data + + @router.put("/employees/{id}", response_model=EmployeeResponse) def update_employee( id: int, payload: UpdateEmployeeRequest, service: Annotated[EmployeeService, Depends(get_employee_service)], ): - employee = service.update_employee(id, payload.name, payload.last_name, payload.preference, payload.company_id) + employee = service.update_employee( + id, payload.name, payload.last_name, payload.preference, payload.company_id + ) if employee is None: raise HTTPException(status_code=404, detail="Employee or company not found") return employee @@ -48,14 +77,18 @@ def patch_employee( payload: UpdateEmployeeRequest, service: Annotated[EmployeeService, Depends(get_employee_service)], ): - employee = service.update_employee(id, payload.name, payload.last_name, payload.preference, payload.company_id) + employee = service.update_employee( + id, payload.name, payload.last_name, payload.preference, payload.company_id + ) if employee is None: raise HTTPException(status_code=404, detail="Employee or company not found") return employee @router.delete("/employees/{id}", response_model=EmployeeResponse) -def delete_employee(id: int, service: Annotated[EmployeeService, Depends(get_employee_service)]): +def delete_employee( + id: int, service: Annotated[EmployeeService, Depends(get_employee_service)] +): employee = service.delete_employee(id) if employee is None: raise HTTPException(status_code=404, detail="Employee not found") @@ -86,7 +119,9 @@ def register_weekly_employee_preferences( return employee -@router.post("/companies/{company_id}/employees/{employee_id}", response_model=CompanyUser) +@router.post( + "/companies/{company_id}/employees/{employee_id}", response_model=CompanyUser +) def assign_employee_to_company( company_id: int, employee_id: int, @@ -98,7 +133,9 @@ def assign_employee_to_company( return company_user -@router.get("/companies/{company_name}/employees/names", response_model=EmployeeNamesResponse) +@router.get( + "/companies/{company_name}/employees/names", response_model=EmployeeNamesResponse +) def get_employee_names_by_company( company_name: str, service: Annotated[EmployeeService, Depends(get_employee_service)], diff --git a/src/scheduler/api/schedule_routes.py b/src/scheduler/api/schedule_routes.py index 86ea4f0..408431b 100644 --- a/src/scheduler/api/schedule_routes.py +++ b/src/scheduler/api/schedule_routes.py @@ -1,6 +1,9 @@ +import codecs +import csv +from datetime import datetime from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, FastAPI, File, HTTPException, UploadFile from scheduler.api.dependencies import get_schedule_service from scheduler.core.schemas.schedule import ( @@ -14,16 +17,60 @@ from scheduler.core.schemas.schedule import ( from scheduler.core.services.schedule_service import ScheduleService router = APIRouter(tags=["schedules"]) +app = FastAPI() @router.post("/schedules", response_model=ScheduleResponse) -def create_schedule(payload: CreateScheduleRequest, service: Annotated[ScheduleService, Depends(get_schedule_service)]): - schedule = service.create_schedule(payload.user_id, payload.enter_time, payload.day_of_week, payload.out_time) +def create_schedule( + payload: CreateScheduleRequest, + service: Annotated[ScheduleService, Depends(get_schedule_service)], +): + schedule = service.create_schedule( + payload.user_id, + payload.enter_time, + payload.day_of_week, + payload.out_time, + payload.shift, + ) if schedule is None: raise HTTPException(status_code=404, detail="Employee not found") return schedule +@router.post("/schedules/upload") +def upload( + service: Annotated[ScheduleService, Depends(get_schedule_service)], + file: UploadFile = File(...), +): + csvReader = csv.DictReader(codecs.iterdecode(file.file, "utf-8-sig")) + data = {} + idx = 0 + date_format = "%Y-%m-%d %H:%M:%S" + for rows in csvReader: + enter_time = datetime.strptime(rows["enter_time"], date_format) + out_time = datetime.strptime(rows["out_time"], date_format) + schedule = service.create_schedule( + int(rows["user_id"]), + enter_time, + rows["day_of_week"], + None, + rows["shift"], + ) + if schedule is None: + continue + + schedule = service.clock_in_schedule( + schedule["id"], + enter_time, + ) + schedule = service.clock_out_schedule(schedule["id"], out_time) + data[idx] = schedule + idx += 1 + + file.file.close() + return data + + @router.post("/employees/{id}/clock-in", response_model=ScheduleResponse) def clock_in( id: int, @@ -53,7 +100,9 @@ def clock_out( @router.get("/schedules/weekly", response_model=WeeklyScheduleResponse) -def get_weekly_schedule(service: Annotated[ScheduleService, Depends(get_schedule_service)]): +def get_weekly_schedule( + service: Annotated[ScheduleService, Depends(get_schedule_service)], +): return service.get_weekly_schedule() diff --git a/src/scheduler/core/schemas/schedule.py b/src/scheduler/core/schemas/schedule.py index 686b421..f27250f 100644 --- a/src/scheduler/core/schemas/schedule.py +++ b/src/scheduler/core/schemas/schedule.py @@ -2,7 +2,7 @@ from datetime import datetime from pydantic import BaseModel -from scheduler.core.enums import DayOfWeek +from scheduler.core.enums import DayOfWeek, WorkTime class CreateScheduleRequest(BaseModel): @@ -34,5 +34,20 @@ class ScheduleResponse(BaseModel): out_time: datetime | None = None +class WeeklyScheduleUserResponse(BaseModel): + name: str + last_name: str + preference: WorkTime + + +class WeeklyScheduleItemResponse(BaseModel): + id: int + user: WeeklyScheduleUserResponse + company: str + enter_time: datetime + day_of_week: DayOfWeek + out_time: datetime | None = None + + class WeeklyScheduleResponse(BaseModel): - schedules: list[ScheduleResponse] + schedules: list[WeeklyScheduleItemResponse] diff --git a/src/scheduler/core/services/schedule_service.py b/src/scheduler/core/services/schedule_service.py index 3e44ba4..782a076 100644 --- a/src/scheduler/core/services/schedule_service.py +++ b/src/scheduler/core/services/schedule_service.py @@ -16,10 +16,19 @@ class ScheduleService: self.employee_repository = employee_repository self.company_repository = company_repository - def create_schedule(self, user_id: int, enter_time: datetime, day_of_week: str, out_time: datetime): + def create_schedule( + self, + user_id: int, + enter_time: datetime, + day_of_week: str, + out_time: datetime, + shift: str, + ): if self.employee_repository.get(user_id) is None: return None - return self.schedule_repository.create(user_id, enter_time, day_of_week, out_time) + return self.schedule_repository.create( + user_id, enter_time, day_of_week, out_time, shift + ) def clock_in( self, @@ -30,13 +39,19 @@ class ScheduleService: employee = self.employee_repository.get(user_id) if employee is None: return None - return self.schedule_repository.create(user_id, enter_time, day_of_week, None) + return self.schedule_repository.create(user_id, enter_time, day_of_week, None, None) + + def clock_in_schedule(self, schedule_id: int, enter_time: datetime): + return self.schedule_repository.update_clock_in_by_id(schedule_id, enter_time) def clock_out(self, user_id: int, day_of_week: str, out_time: datetime): if self.employee_repository.get(user_id) is None: return None return self.schedule_repository.update_clock_out(user_id, day_of_week, out_time) + def clock_out_schedule(self, schedule_id: int, out_time: datetime): + return self.schedule_repository.update_clock_out_by_id(schedule_id, out_time) + def get_weekly_schedule(self, company_id: int | None = None): return {"schedules": self.schedule_repository.list_weekly(company_id)} diff --git a/src/scheduler/database/schedule_repository.py b/src/scheduler/database/schedule_repository.py index 56324cf..9a0022f 100644 --- a/src/scheduler/database/schedule_repository.py +++ b/src/scheduler/database/schedule_repository.py @@ -5,15 +5,22 @@ class ScheduleRepository: def __init__(self, connection): self.connection = connection - def create(self, user_id: int, enter_time: datetime, day_of_week: str, out_time: datetime | None): + def create( + self, + user_id: int, + enter_time: datetime, + day_of_week: str, + out_time: datetime | None, + shift: str | None, + ): with self.connection.cursor() as cursor: cursor.execute( """ - INSERT INTO schedules (user_id, enter_time, day_of_week, out_time) - VALUES (%s, %s, %s, %s) - RETURNING id, user_id, enter_time, day_of_week, out_time + INSERT INTO schedules (user_id, enter_time, day_of_week, out_time, shift) + VALUES (%s, %s, %s, %s, %s) + RETURNING id, user_id, enter_time, day_of_week, out_time, shift """, - (user_id, enter_time, day_of_week, out_time), + (user_id, enter_time, day_of_week, out_time, shift), ) schedule = cursor.fetchone() self.connection.commit() @@ -28,16 +35,37 @@ class ScheduleRepository: with self.connection.cursor() as cursor: cursor.execute( f""" - SELECT schedules.id, schedules.user_id, schedules.enter_time, - schedules.day_of_week, schedules.out_time + SELECT schedules.id, schedules.enter_time, schedules.day_of_week, + schedules.out_time, users.name AS user_name, + users.last_name AS user_last_name, + users.preference AS user_preference, + companies.name AS company FROM schedules INNER JOIN company_users ON company_users.user_id = schedules.user_id + INNER JOIN users ON users.id = schedules.user_id + INNER JOIN companies ON companies.id = company_users.company_id {where} ORDER BY schedules.day_of_week, schedules.enter_time, schedules.id """, params, ) - return cursor.fetchall() + schedules = cursor.fetchall() + + return [ + { + "id": schedule["id"], + "user": { + "name": schedule["user_name"], + "last_name": schedule["user_last_name"], + "preference": schedule["user_preference"], + }, + "company": schedule["company"], + "enter_time": schedule["enter_time"], + "day_of_week": schedule["day_of_week"], + "out_time": schedule["out_time"], + } + for schedule in schedules + ] def update_clock_out(self, user_id: int, day_of_week: str, out_time: datetime): with self.connection.cursor() as cursor: @@ -59,3 +87,33 @@ class ScheduleRepository: schedule = cursor.fetchone() self.connection.commit() return schedule + + def update_clock_in_by_id(self, schedule_id: int, enter_time: datetime): + with self.connection.cursor() as cursor: + cursor.execute( + """ + UPDATE schedules + SET enter_time = %s, date_updated = NOW() + WHERE id = %s + RETURNING id, user_id, enter_time, day_of_week, out_time, shift + """, + (enter_time, schedule_id), + ) + schedule = cursor.fetchone() + self.connection.commit() + return schedule + + def update_clock_out_by_id(self, schedule_id: int, out_time: datetime): + with self.connection.cursor() as cursor: + cursor.execute( + """ + UPDATE schedules + SET out_time = %s, date_updated = NOW() + WHERE id = %s + RETURNING id, user_id, enter_time, day_of_week, out_time, shift + """, + (out_time, schedule_id), + ) + schedule = cursor.fetchone() + self.connection.commit() + return schedule