adding endpoints

This commit is contained in:
2026-09-18 12:26:25 -04:00
parent d5c341d1fc
commit f38a178fcd
44 changed files with 1302 additions and 4 deletions
+3
View File
@@ -0,0 +1,3 @@
__version__ = "1.0.0"
__all__ = ["__version__"]
+13
View File
@@ -0,0 +1,13 @@
import os
import uvicorn
def main():
host = os.environ.get("APP_HOST", "127.0.0.1")
port = int(os.environ.get("APP_PORT", "5006"))
uvicorn.run("scheduler.main:app", host=host, port=port)
if __name__ == "__main__":
main()
+8
View File
@@ -0,0 +1,8 @@
from . import company_routes, employee_routes, health_routes, schedule_routes
__all__ = [
"company_routes",
"employee_routes",
"health_routes",
"schedule_routes",
]
+35
View File
@@ -0,0 +1,35 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
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"])
@router.get("", response_model=list[Company])
def list_companies(service: Annotated[CompanyService, Depends(get_company_service)]):
return service.list_companies()
@router.post("", response_model=Company)
def create_company(payload: CreateCompanyRequest, service: Annotated[CompanyService, Depends(get_company_service)]):
return service.create_company(payload.name)
@router.get("/{id}", response_model=Company)
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")
return company
@router.delete("/{id}", response_model=Company)
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")
return company
+50
View File
@@ -0,0 +1,50 @@
from collections.abc import Generator
from dotenv import load_dotenv
from scheduler.core.services.company_service import CompanyService
from scheduler.core.services.employee_service import EmployeeService
from scheduler.core.services.schedule_service import ScheduleService
from scheduler.database import CompanyRepository, Database, EmployeeRepository, ScheduleRepository
def get_database() -> Database:
load_dotenv()
return Database()
def get_db() -> Generator:
connection = get_database().get_connection()
try:
yield connection
finally:
connection.close()
def get_company_service() -> Generator:
connection = get_database().get_connection()
try:
yield CompanyService(CompanyRepository(connection))
finally:
connection.close()
def get_employee_service() -> Generator:
connection = get_database().get_connection()
try:
yield EmployeeService(EmployeeRepository(connection), CompanyRepository(connection))
finally:
connection.close()
def get_schedule_service() -> Generator:
connection = get_database().get_connection()
try:
yield ScheduleService(
ScheduleRepository(connection),
EmployeeRepository(connection),
CompanyRepository(connection),
)
finally:
connection.close()
+109
View File
@@ -0,0 +1,109 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from scheduler.api.dependencies import get_employee_service
from scheduler.core.schemas.company import CompanyUser
from scheduler.core.schemas.employee import (
CreateEmployeeRequest,
EmployeeNamesResponse,
EmployeeResponse,
RegisterPreferenceRequest,
UpdateEmployeeRequest,
WeeklyPreferenceRequest,
)
from scheduler.core.services.employee_service import EmployeeService
router = APIRouter(tags=["employees"])
@router.get("/employees", response_model=list[EmployeeResponse])
def list_employees(service: Annotated[EmployeeService, Depends(get_employee_service)]):
return service.list_employees()
@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)
if employee is None:
raise HTTPException(status_code=404, detail="Company not found")
return employee
@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)
if employee is None:
raise HTTPException(status_code=404, detail="Employee or company not found")
return employee
@router.patch("/employees/{id}", response_model=EmployeeResponse)
def patch_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)
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)]):
employee = service.delete_employee(id)
if employee is None:
raise HTTPException(status_code=404, detail="Employee not found")
return employee
@router.put("/employees/{id}/preference", response_model=EmployeeResponse)
def register_employee_preference(
id: int,
payload: RegisterPreferenceRequest,
service: Annotated[EmployeeService, Depends(get_employee_service)],
):
employee = service.update_preference(id, payload.preference)
if employee is None:
raise HTTPException(status_code=404, detail="Employee not found")
return employee
@router.put("/employees/{id}/weekly-preferences", response_model=EmployeeResponse)
def register_weekly_employee_preferences(
id: int,
payload: WeeklyPreferenceRequest,
service: Annotated[EmployeeService, Depends(get_employee_service)],
):
employee = service.save_weekly_preferences(id, payload.preferences)
if employee is None:
raise HTTPException(status_code=404, detail="Employee not found")
return employee
@router.post("/companies/{company_id}/employees/{employee_id}", response_model=CompanyUser)
def assign_employee_to_company(
company_id: int,
employee_id: int,
service: Annotated[EmployeeService, Depends(get_employee_service)],
):
company_user = service.assign_to_company(company_id, employee_id)
if company_user is None:
raise HTTPException(status_code=404, detail="Company or employee not found")
return company_user
@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)],
):
employees = service.get_names_by_company_name(company_name)
if employees is None:
raise HTTPException(status_code=404, detail="Company not found")
return {"employees": employees}
+10
View File
@@ -0,0 +1,10 @@
from fastapi import APIRouter
from scheduler.core.schemas.health import HealthResponse
router = APIRouter(tags=["health"])
@router.get("/health", response_model=HealthResponse)
def health_check():
return {"status": "ok"}
+68
View File
@@ -0,0 +1,68 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from scheduler.api.dependencies import get_schedule_service
from scheduler.core.schemas.schedule import (
ClockInRequest,
ClockOutRequest,
CreateScheduleRequest,
GenerateWeeklyScheduleRequest,
ScheduleResponse,
WeeklyScheduleResponse,
)
from scheduler.core.services.schedule_service import ScheduleService
router = APIRouter(tags=["schedules"])
@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)
if schedule is None:
raise HTTPException(status_code=404, detail="Employee not found")
return schedule
@router.post("/employees/{id}/clock-in", response_model=ScheduleResponse)
def clock_in(
id: int,
payload: ClockInRequest,
service: Annotated[ScheduleService, Depends(get_schedule_service)],
):
schedule = service.clock_in(
id,
day_of_week=payload.day_of_week,
enter_time=payload.enter_time,
)
if schedule is None:
raise HTTPException(status_code=404, detail="Employee not found")
return schedule
@router.put("/employees/{id}/clock-out", response_model=ScheduleResponse)
def clock_out(
id: int,
payload: ClockOutRequest,
service: Annotated[ScheduleService, Depends(get_schedule_service)],
):
schedule = service.clock_out(id, payload.day_of_week, payload.out_time)
if schedule is None:
raise HTTPException(status_code=404, detail="Schedule not found")
return schedule
@router.get("/schedules/weekly", response_model=WeeklyScheduleResponse)
def get_weekly_schedule(service: Annotated[ScheduleService, Depends(get_schedule_service)]):
return service.get_weekly_schedule()
@router.post("/schedules/weekly/generate", response_model=WeeklyScheduleResponse)
def generate_weekly_schedule(
payload: GenerateWeeklyScheduleRequest,
service: Annotated[ScheduleService, Depends(get_schedule_service)],
):
schedule = service.generate_weekly_schedule(payload.company_id)
if schedule is None:
raise HTTPException(status_code=404, detail="Company not found")
return schedule
+46
View File
@@ -0,0 +1,46 @@
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from scheduler.api import company_routes, employee_routes, health_routes, schedule_routes
from scheduler.database import Database
@asynccontextmanager
async def lifespan(app: FastAPI):
Database().initialize_schema()
yield
def create_app() -> FastAPI:
load_dotenv()
app = FastAPI(
title="manager_employees_python",
lifespan=lifespan,
openapi_url="/api-docs/openapi.json",
docs_url="/swagger-ui/",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(health_routes.router)
app.include_router(company_routes.router)
app.include_router(employee_routes.router)
app.include_router(schedule_routes.router)
@app.get("/api-docs/openai.json", include_in_schema=False)
def openai_json_alias():
return app.openapi()
return app
app = create_app()
+4
View File
@@ -0,0 +1,4 @@
from scheduler.core.enums.day_of_week import DayOfWeek
from scheduler.core.enums.work_time import WorkTime
__all__ = ["DayOfWeek", "WorkTime"]
+11
View File
@@ -0,0 +1,11 @@
from enum import Enum
class DayOfWeek(str, Enum):
SUNDAY = "sunday"
MONDAY = "monday"
TUESDAY = "tuesday"
WEDNESDAY = "wednesday"
THURSDAY = "thursday"
FRIDAY = "friday"
SATURDAY = "saturday"
+7
View File
@@ -0,0 +1,7 @@
from enum import Enum
class WorkTime(str, Enum):
MORNING = "morning"
AFTERNOON = "afternoon"
EVENING = "evening"
+22
View File
@@ -0,0 +1,22 @@
from datetime import datetime
from pydantic import BaseModel
class CreateCompanyRequest(BaseModel):
name: str
class Company(BaseModel):
id: int
name: str
date_created: datetime
date_updated: datetime
class CompanyUser(BaseModel):
id: int
user_id: int
company_id: int
date_created: datetime
date_updated: datetime
+44
View File
@@ -0,0 +1,44 @@
from pydantic import BaseModel
from scheduler.core.enums import DayOfWeek, WorkTime
class CreateEmployeeRequest(BaseModel):
company_id: int
name: str
last_name: str
preference: WorkTime
class RegisterPreferenceRequest(BaseModel):
preference: WorkTime
class DayPreferenceRequest(BaseModel):
day_of_week: DayOfWeek
first: WorkTime
second: WorkTime
third: WorkTime
class WeeklyPreferenceRequest(BaseModel):
preferences: list[DayPreferenceRequest]
class EmployeeNamesResponse(BaseModel):
employees: list[str]
class UpdateEmployeeRequest(BaseModel):
company_id: int | None = None
name: str
last_name: str
preference: WorkTime
class EmployeeResponse(BaseModel):
id: int
company_id: int
name: str
last_name: str
preference: WorkTime
+5
View File
@@ -0,0 +1,5 @@
from pydantic import BaseModel
class HealthResponse(BaseModel):
status: str
+38
View File
@@ -0,0 +1,38 @@
from datetime import datetime
from pydantic import BaseModel
from scheduler.core.enums import DayOfWeek
class CreateScheduleRequest(BaseModel):
user_id: int
enter_time: datetime
day_of_week: DayOfWeek
out_time: datetime
class ClockInRequest(BaseModel):
day_of_week: DayOfWeek
enter_time: datetime
class ClockOutRequest(BaseModel):
day_of_week: DayOfWeek
out_time: datetime
class GenerateWeeklyScheduleRequest(BaseModel):
company_id: int
class ScheduleResponse(BaseModel):
id: int
user_id: int
enter_time: datetime
day_of_week: DayOfWeek
out_time: datetime | None = None
class WeeklyScheduleResponse(BaseModel):
schedules: list[ScheduleResponse]
@@ -0,0 +1,18 @@
from scheduler.database.company_repository import CompanyRepository
class CompanyService:
def __init__(self, repository: CompanyRepository):
self.repository = repository
def list_companies(self):
return self.repository.list()
def create_company(self, name: str):
return self.repository.create(name)
def get_company(self, company_id: int):
return self.repository.get(company_id)
def delete_company(self, company_id: int):
return self.repository.delete(company_id)
@@ -0,0 +1,57 @@
from scheduler.database.company_repository import CompanyRepository
from scheduler.database.employee_repository import EmployeeRepository
class EmployeeService:
def __init__(self, employee_repository: EmployeeRepository, company_repository: CompanyRepository):
self.employee_repository = employee_repository
self.company_repository = company_repository
def list_employees(self):
return self.employee_repository.list()
def create_employee(self, company_id: int, name: str, last_name: str, preference: str):
if self.company_repository.get(company_id) is None:
return None
return self.employee_repository.create(company_id, name, last_name, preference)
def delete_employee(self, employee_id: int):
return self.employee_repository.delete(employee_id)
def update_preference(self, employee_id: int, preference: str):
return self.employee_repository.update_preference(employee_id, preference)
def update_employee(
self,
employee_id: int,
name: str,
last_name: str,
preference: str,
company_id: int | None,
):
if self.employee_repository.get(employee_id) is None:
return None
if company_id is not None and self.company_repository.get(company_id) is None:
return None
updated = self.employee_repository.update(employee_id, name, last_name, preference)
if company_id is not None:
updated = self.employee_repository.update_company(employee_id, company_id)
return updated
def assign_to_company(self, company_id: int, employee_id: int):
if self.company_repository.get(company_id) is None:
return None
if self.employee_repository.get(employee_id) is None:
return None
return self.employee_repository.assign_to_company(company_id, employee_id)
def get_names_by_company_name(self, company_name: str):
company = self.company_repository.get_by_name(company_name)
if company is None:
return None
return self.employee_repository.get_names_by_company(company["id"])
def save_weekly_preferences(self, employee_id: int, preferences: list):
if self.employee_repository.get(employee_id) is None:
return None
return self.employee_repository.save_weekly_preferences(employee_id, preferences)
@@ -0,0 +1,46 @@
from datetime import datetime
from scheduler.database.company_repository import CompanyRepository
from scheduler.database.employee_repository import EmployeeRepository
from scheduler.database.schedule_repository import ScheduleRepository
class ScheduleService:
def __init__(
self,
schedule_repository: ScheduleRepository,
employee_repository: EmployeeRepository,
company_repository: CompanyRepository,
):
self.schedule_repository = schedule_repository
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):
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)
def clock_in(
self,
user_id: int,
day_of_week: str,
enter_time: datetime,
):
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)
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 get_weekly_schedule(self, company_id: int | None = None):
return {"schedules": self.schedule_repository.list_weekly(company_id)}
def generate_weekly_schedule(self, company_id: int):
if self.company_repository.get(company_id) is None:
return None
return self.get_weekly_schedule(company_id)
+6
View File
@@ -0,0 +1,6 @@
from scheduler.database.company_repository import CompanyRepository
from scheduler.database.database import Database
from scheduler.database.employee_repository import EmployeeRepository
from scheduler.database.schedule_repository import ScheduleRepository
__all__ = ["CompanyRepository", "Database", "EmployeeRepository", "ScheduleRepository"]
@@ -0,0 +1,66 @@
class CompanyRepository:
def __init__(self, connection):
self.connection = connection
def list(self):
with self.connection.cursor() as cursor:
cursor.execute(
"""
SELECT id, name, date_created, date_updated
FROM companies
ORDER BY id
"""
)
return cursor.fetchall()
def create(self, name: str):
with self.connection.cursor() as cursor:
cursor.execute(
"""
INSERT INTO companies (name)
VALUES (%s)
RETURNING id, name, date_created, date_updated
""",
(name,),
)
company = cursor.fetchone()
self.connection.commit()
return company
def get(self, company_id: int):
with self.connection.cursor() as cursor:
cursor.execute(
"""
SELECT id, name, date_created, date_updated
FROM companies
WHERE id = %s
""",
(company_id,),
)
return cursor.fetchone()
def get_by_name(self, name: str):
with self.connection.cursor() as cursor:
cursor.execute(
"""
SELECT id, name, date_created, date_updated
FROM companies
WHERE name = %s
""",
(name,),
)
return cursor.fetchone()
def delete(self, company_id: int):
with self.connection.cursor() as cursor:
cursor.execute(
"""
DELETE FROM companies
WHERE id = %s
RETURNING id, name, date_created, date_updated
""",
(company_id,),
)
company = cursor.fetchone()
self.connection.commit()
return company
+105
View File
@@ -0,0 +1,105 @@
import os
import psycopg
from psycopg.rows import dict_row
DEFAULT_DATABASE_URL = "postgres://carlos:password@localhost/rust_manager_employees"
class Database:
def __init__(self, database_url: str | None = None):
self.database_url = database_url or os.environ.get("DATABASE_URL", DEFAULT_DATABASE_URL)
def get_connection(self):
return psycopg.connect(self.database_url, row_factory=dict_row)
def initialize_schema(self):
with self.get_connection() as connection:
with connection.cursor() as cursor:
cursor.execute(
"""
DO $$
BEGIN
CREATE TYPE work_time AS ENUM ('morning', 'afternoon', 'evening');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
"""
)
cursor.execute("ALTER TYPE work_time ADD VALUE IF NOT EXISTS 'afternoon';")
cursor.execute(
"""
DO $$
BEGIN
CREATE TYPE day_of_week AS ENUM (
'sunday', 'monday', 'tuesday', 'wednesday',
'thursday', 'friday', 'saturday'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS companies (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
date_updated TIMESTAMP NOT NULL DEFAULT NOW()
);
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
last_name TEXT NOT NULL,
preference work_time NOT NULL,
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
date_updated TIMESTAMP NOT NULL DEFAULT NOW()
);
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS company_users (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
date_updated TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (user_id, company_id)
);
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS schedules (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
enter_time TIMESTAMP NOT NULL,
day_of_week day_of_week NOT NULL,
out_time TIMESTAMP,
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
date_updated TIMESTAMP NOT NULL DEFAULT NOW()
);
"""
)
cursor.execute("ALTER TABLE schedules ALTER COLUMN out_time DROP NOT NULL;")
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS weekly_preferences (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
day_of_week day_of_week NOT NULL,
first work_time NOT NULL,
second work_time NOT NULL,
third work_time NOT NULL,
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
date_updated TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (user_id, day_of_week)
);
"""
)
connection.commit()
@@ -0,0 +1,167 @@
class EmployeeRepository:
def __init__(self, connection):
self.connection = connection
def list(self):
with self.connection.cursor() as cursor:
cursor.execute(
"""
SELECT users.id, company_users.company_id, users.name, users.last_name, users.preference
FROM users
INNER JOIN company_users ON company_users.user_id = users.id
ORDER BY users.id
"""
)
return cursor.fetchall()
def create(self, company_id: int, name: str, last_name: str, preference: str):
with self.connection.cursor() as cursor:
cursor.execute(
"""
INSERT INTO users (name, last_name, preference)
VALUES (%s, %s, %s)
RETURNING id, name, last_name, preference
""",
(name, last_name, preference),
)
user = cursor.fetchone()
cursor.execute(
"""
INSERT INTO company_users (user_id, company_id)
VALUES (%s, %s)
RETURNING company_id
""",
(user["id"], company_id),
)
company_user = cursor.fetchone()
self.connection.commit()
return {**user, "company_id": company_user["company_id"]}
def get(self, employee_id: int):
with self.connection.cursor() as cursor:
cursor.execute(
"""
SELECT users.id, company_users.company_id, users.name, users.last_name, users.preference
FROM users
INNER JOIN company_users ON company_users.user_id = users.id
WHERE users.id = %s
""",
(employee_id,),
)
return cursor.fetchone()
def delete(self, employee_id: int):
employee = self.get(employee_id)
if employee is None:
return None
with self.connection.cursor() as cursor:
cursor.execute("DELETE FROM users WHERE id = %s", (employee_id,))
self.connection.commit()
return employee
def update_preference(self, employee_id: int, preference: str):
with self.connection.cursor() as cursor:
cursor.execute(
"""
UPDATE users
SET preference = %s, date_updated = NOW()
WHERE id = %s
""",
(preference, employee_id),
)
self.connection.commit()
return self.get(employee_id)
def update(self, employee_id: int, name: str | None, last_name: str | None, preference: str | None):
updates: list[str] = []
params: list[object] = []
if name is not None:
updates.append("name = %s")
params.append(name)
if last_name is not None:
updates.append("last_name = %s")
params.append(last_name)
if preference is not None:
updates.append("preference = %s")
params.append(preference)
if updates:
updates.append("date_updated = NOW()")
params.append(employee_id)
with self.connection.cursor() as cursor:
cursor.execute(
f"""
UPDATE users
SET {", ".join(updates)}
WHERE id = %s
""",
tuple(params),
)
self.connection.commit()
return self.get(employee_id)
def update_company(self, employee_id: int, company_id: int):
with self.connection.cursor() as cursor:
cursor.execute("DELETE FROM company_users WHERE user_id = %s", (employee_id,))
cursor.execute(
"""
INSERT INTO company_users (user_id, company_id)
VALUES (%s, %s)
ON CONFLICT (user_id, company_id) DO UPDATE SET date_updated = NOW()
""",
(employee_id, company_id),
)
self.connection.commit()
return self.get(employee_id)
def assign_to_company(self, company_id: int, employee_id: int):
with self.connection.cursor() as cursor:
cursor.execute(
"""
INSERT INTO company_users (user_id, company_id)
VALUES (%s, %s)
ON CONFLICT (user_id, company_id) DO UPDATE SET date_updated = NOW()
RETURNING id, user_id, company_id, date_created, date_updated
""",
(employee_id, company_id),
)
company_user = cursor.fetchone()
self.connection.commit()
return company_user
def get_names_by_company(self, company_id: int):
with self.connection.cursor() as cursor:
cursor.execute(
"""
SELECT users.name, users.last_name
FROM users
INNER JOIN company_users ON company_users.user_id = users.id
WHERE company_users.company_id = %s
ORDER BY users.name, users.last_name
""",
(company_id,),
)
return [f"{row['name']} {row['last_name']}" for row in cursor.fetchall()]
def save_weekly_preferences(self, employee_id: int, preferences: list):
with self.connection.cursor() as cursor:
for preference in preferences:
cursor.execute(
"""
INSERT INTO weekly_preferences (user_id, day_of_week, first, second, third)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (user_id, day_of_week) DO UPDATE SET
first = EXCLUDED.first,
second = EXCLUDED.second,
third = EXCLUDED.third,
date_updated = NOW()
""",
(
employee_id,
preference.day_of_week,
preference.first,
preference.second,
preference.third,
),
)
self.connection.commit()
return self.get(employee_id)
@@ -0,0 +1,61 @@
from datetime import datetime
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):
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
""",
(user_id, enter_time, day_of_week, out_time),
)
schedule = cursor.fetchone()
self.connection.commit()
return schedule
def list_weekly(self, company_id: int | None = None):
params = ()
where = ""
if company_id is not None:
where = "WHERE company_users.company_id = %s"
params = (company_id,)
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
FROM schedules
INNER JOIN company_users ON company_users.user_id = schedules.user_id
{where}
ORDER BY schedules.day_of_week, schedules.enter_time, schedules.id
""",
params,
)
return cursor.fetchall()
def update_clock_out(self, user_id: int, day_of_week: str, out_time: datetime):
with self.connection.cursor() as cursor:
cursor.execute(
"""
UPDATE schedules
SET out_time = %s, date_updated = NOW()
WHERE id = (
SELECT id
FROM schedules
WHERE user_id = %s AND day_of_week = %s
ORDER BY date_created DESC
LIMIT 1
)
RETURNING id, user_id, enter_time, day_of_week, out_time
""",
(out_time, user_id, day_of_week),
)
schedule = cursor.fetchone()
self.connection.commit()
return schedule
+93
View File
@@ -0,0 +1,93 @@
from scheduler.database.company_repository import CompanyRepository
from scheduler.database.database import Database
from scheduler.database.employee_repository import EmployeeRepository
from scheduler.database.schedule_repository import ScheduleRepository
def get_connection():
return Database().get_connection()
def initialize_schema(connection=None):
Database().initialize_schema()
def add_company(connection, name):
return CompanyRepository(connection).create(name)
def get_company_by_name(connection, name):
return CompanyRepository(connection).get_by_name(name)
def remove_company(connection, company_id):
return CompanyRepository(connection).delete(company_id)
def get_employee_names_by_company(connection, company_id):
return EmployeeRepository(connection).get_names_by_company(company_id)
def add_user(connection, name, last_name, preference):
with connection.cursor() as cursor:
cursor.execute(
"""
INSERT INTO users (name, last_name, preference)
VALUES (%s, %s, %s)
RETURNING id, name, last_name, preference, date_created, date_updated
""",
(name, last_name, preference),
)
user = cursor.fetchone()
connection.commit()
return user
def update_user_preference(connection, user_id, preference):
return EmployeeRepository(connection).update_preference(user_id, preference)
def add_company_user(connection, company_id, user_id):
return EmployeeRepository(connection).assign_to_company(company_id, user_id)
def remove_user(connection, user_id):
return EmployeeRepository(connection).delete(user_id)
def add_schedule(connection, user_id, enter_time, day_of_week, out_time):
return ScheduleRepository(connection).create(user_id, enter_time, day_of_week, out_time)
def get_days_worked_by_user(connection, user_id):
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT DISTINCT day_of_week
FROM schedules
WHERE user_id = %s
ORDER BY day_of_week
""",
(user_id,),
)
return [row["day_of_week"] for row in cursor.fetchall()]
def get_user_enter_time_by_day(connection, user_id, day_of_week):
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT enter_time
FROM schedules
WHERE user_id = %s AND day_of_week = %s
ORDER BY date_created DESC
LIMIT 1
""",
(user_id, day_of_week),
)
row = cursor.fetchone()
return None if row is None else row["enter_time"]
def update_day_end_by_user_and_day(connection, user_id, day_of_week, out_time):
return ScheduleRepository(connection).update_clock_out(user_id, day_of_week, out_time)
+3
View File
@@ -0,0 +1,3 @@
from scheduler.app import app, create_app
__all__ = ["app", "create_app"]
+3
View File
@@ -0,0 +1,3 @@
from . import conflict, scheduler, validator
__all__ = ["conflict", "scheduler", "validator"]
+32
View File
@@ -0,0 +1,32 @@
# Conflict resolution guide.
# Import validator functions before moving employees between shifts or days.
def detect_preferred_shift_conflict(connection, user_id, day_of_week, preferred_shift):
# check if preferred shift is already full for selected day
# check if user already works that day
# check if user already worked five days this week
pass
def resolve_conflict_same_day(connection, user_id, day_of_week):
# loop over other shifts on the same day
# choose the first shift where user can legally work
# assign only after validator confirms the schedule is valid
pass
def resolve_conflict_next_day(connection, user_id, current_day):
# if same-day alternatives fail, move to next day
# loop through next-day shifts by preference priority
# assign only after validator confirms the schedule is valid
pass
def resolve_missing_second_employee(connection, company_id, day_of_week, shift):
# find shifts with only one employee assigned
# loop over available employees in company
# skip employees who already work that day
# skip employees who already worked five days this week
# assign a valid extra employee to satisfy minimum coverage
pass
+52
View File
@@ -0,0 +1,52 @@
# Schedule generation guide.
# Import database.storage for queries and manager.validator/conflict for rules.
def generate_weekly_schedule(connection, company_id):
# load all employees for selected company
# load employee preferences for all seven days
# loop through each day of the week
# loop through morning, afternoon, and evening shifts
# assign employees based on preferences first
# validate one-shift-per-day before each assignment
# validate five-days-per-week before each assignment
# ensure at least two employees per shift after assignments
# resolve conflicts by assigning another shift on same day or next day
# save final schedule rows to the schedules table
pass
def assign_preferred_shift(connection, user_id, day_of_week, preferred_shift):
# validate employee can join selected shift
# call storage.add_schedule when assignment is valid
# return validation error if assignment is not valid
pass
def fill_missing_shift_coverage(connection, company_id, day_of_week, shift):
# find company shift with fewer than two employees
# if only one employee is assigned, search for another valid employee
# randomly choose or rotate through employees who have not worked five days
# add the selected employee to the schedule
pass
def register_clock_in(connection, user_id, day_of_week, enter_time):
# reject if user already worked the same day
# reject if user already worked five days this week
# save enter_time through storage.add_schedule or update an open schedule row
pass
def register_clock_out(connection, user_id, day_of_week, out_time):
# call storage.update_day_end_by_user_and_day
# keep the existing schedule row and update out_time only
pass
def print_final_schedule(connection, company_id):
# query final schedule with employee names
# group output by day and shift
# print employees assigned to each shift
# print warnings for shifts with fewer than two employees
pass
+38
View File
@@ -0,0 +1,38 @@
# Validation guide for schedule rules.
# Import scheduler.database.storage query functions before implementing these checks.
def validate_user_has_not_worked_same_day(connection, user_id, day_of_week):
# query schedules for user_id and day_of_week
# if a schedule exists, reject the new assignment
# otherwise allow scheduler to continue
pass
def validate_user_has_not_worked_more_than_five_days(connection, user_id):
# call storage.get_days_worked_by_user(connection, user_id)
# count distinct worked days with a loop
# reject if count is five or more
# otherwise allow assignment
pass
def validate_shift_has_minimum_two_employees(connection, company_id, day_of_week, shift):
# query schedules for company_id, day_of_week, and shift
# count assigned employees
# if fewer than two, scheduler must add another available employee
pass
def validate_employee_can_join_shift(connection, user_id, day_of_week, shift):
# check one-shift-per-day rule first
# check five-days-per-week rule second
# check shift coverage and capacity rules last
pass
def validate_company_has_enough_employees(connection, company_id):
# get employee names by company
# reject schedule generation if company has fewer than two employees
# otherwise continue schedule generation
pass
View File
+3
View File
@@ -0,0 +1,3 @@
from . import employee, schedule, shift
__all__ = ["employee", "schedule", "shift"]
+27
View File
@@ -0,0 +1,27 @@
from scheduler.database import storage
def define_employee_fields():
# include id from users table
# include company_id from company_users table
# include name and last_name from users table
# include preferred shift for each day of the week
# include optional priority ranking for bonus preference support
pass
def register_employee_preference(connection, user_id, preference):
return storage.update_user_preference(connection, user_id, preference)
def get_employee_names_by_company(connection, company_name):
company = storage.get_company_by_name(connection, company_name)
if company is None:
return None
return storage.get_employee_names_by_company(connection, company["id"])
def register_user_into_company(connection, company_id, name, last_name, preference):
user = storage.add_user(connection, name, last_name, preference)
storage.add_company_user(connection, company_id, user["id"])
return user
+23
View File
@@ -0,0 +1,23 @@
from scheduler.database import storage
def define_schedule_fields():
# include user_id to identify the employee
# include company_id if schedules need to be filtered by company
# include day_of_week for the assigned day
# include shift value for morning, afternoon, or evening
# include enter_time and out_time for clock-in and clock-out
pass
def register_user_enter_and_out(connection, user_id, enter_time, day_of_week, out_time):
# Add manager validation before saving once scheduling rules are complete.
return storage.add_schedule(connection, user_id, enter_time, day_of_week, out_time)
def output_weekly_schedule():
# group saved schedules by day of week
# inside each day, group schedules by shift
# list employee names for each shift
# show missing coverage when a shift has fewer than two employees
pass
+22
View File
@@ -0,0 +1,22 @@
# Shift model guide.
# Keep shift names consistent with the PostgreSQL work_time enum.
def define_shift_values():
# morning maps to work_time value "morning"
# afternoon can map to "evening" if you keep the Rust enum name
# evening maps to work_time value "evening"
pass
def define_day_values():
# use sunday through saturday to match the day_of_week enum
# loop over all seven days when generating the weekly schedule
pass
def compare_shift_capacity():
# count employees assigned to one shift on one day
# if the count is fewer than two, scheduler must add another employee
# if the preferred shift is full, conflict manager must choose another shift
pass
View File