refactoring unused functions
This commit is contained in:
@@ -14,7 +14,11 @@ from scheduler.core.schemas.schedule import (
|
|||||||
ScheduleResponse,
|
ScheduleResponse,
|
||||||
WeeklyScheduleResponse,
|
WeeklyScheduleResponse,
|
||||||
)
|
)
|
||||||
from scheduler.core.services.schedule_service import ScheduleService
|
from scheduler.core.services.schedule_service import (
|
||||||
|
ALREADY_WORKED_DAY,
|
||||||
|
MAX_DAYS_REACHED,
|
||||||
|
ScheduleService,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(tags=["schedules"])
|
router = APIRouter(tags=["schedules"])
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -81,9 +85,14 @@ def clock_in(
|
|||||||
id,
|
id,
|
||||||
day_of_week=payload.day_of_week,
|
day_of_week=payload.day_of_week,
|
||||||
enter_time=payload.enter_time,
|
enter_time=payload.enter_time,
|
||||||
|
shift=payload.shift,
|
||||||
)
|
)
|
||||||
if schedule is None:
|
if schedule is None:
|
||||||
raise HTTPException(status_code=404, detail="Employee not found")
|
raise HTTPException(status_code=404, detail="Employee not found")
|
||||||
|
if schedule == ALREADY_WORKED_DAY:
|
||||||
|
raise HTTPException(status_code=409, detail="Employee already worked this day")
|
||||||
|
if schedule == MAX_DAYS_REACHED:
|
||||||
|
raise HTTPException(status_code=409, detail="employee already worked 5 days")
|
||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ class CreateScheduleRequest(BaseModel):
|
|||||||
enter_time: datetime
|
enter_time: datetime
|
||||||
day_of_week: DayOfWeek
|
day_of_week: DayOfWeek
|
||||||
out_time: datetime
|
out_time: datetime
|
||||||
|
shift: WorkTime
|
||||||
|
|
||||||
|
|
||||||
class ClockInRequest(BaseModel):
|
class ClockInRequest(BaseModel):
|
||||||
day_of_week: DayOfWeek
|
day_of_week: DayOfWeek
|
||||||
enter_time: datetime
|
enter_time: datetime
|
||||||
|
shift: WorkTime
|
||||||
|
|
||||||
|
|
||||||
class ClockOutRequest(BaseModel):
|
class ClockOutRequest(BaseModel):
|
||||||
@@ -32,6 +34,7 @@ class ScheduleResponse(BaseModel):
|
|||||||
enter_time: datetime
|
enter_time: datetime
|
||||||
day_of_week: DayOfWeek
|
day_of_week: DayOfWeek
|
||||||
out_time: datetime | None = None
|
out_time: datetime | None = None
|
||||||
|
shift: WorkTime
|
||||||
|
|
||||||
|
|
||||||
class WeeklyScheduleUserResponse(BaseModel):
|
class WeeklyScheduleUserResponse(BaseModel):
|
||||||
@@ -47,6 +50,7 @@ class WeeklyScheduleItemResponse(BaseModel):
|
|||||||
enter_time: datetime
|
enter_time: datetime
|
||||||
day_of_week: DayOfWeek
|
day_of_week: DayOfWeek
|
||||||
out_time: datetime | None = None
|
out_time: datetime | None = None
|
||||||
|
shift: WorkTime
|
||||||
|
|
||||||
|
|
||||||
class WeeklyScheduleResponse(BaseModel):
|
class WeeklyScheduleResponse(BaseModel):
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from scheduler.database.company_repository import CompanyRepository
|
|||||||
from scheduler.database.employee_repository import EmployeeRepository
|
from scheduler.database.employee_repository import EmployeeRepository
|
||||||
from scheduler.database.schedule_repository import ScheduleRepository
|
from scheduler.database.schedule_repository import ScheduleRepository
|
||||||
|
|
||||||
|
ALREADY_WORKED_DAY = "already_worked_day"
|
||||||
|
MAX_DAYS_REACHED = "max_days_reached"
|
||||||
|
|
||||||
|
|
||||||
class ScheduleService:
|
class ScheduleService:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -35,11 +38,19 @@ class ScheduleService:
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
day_of_week: str,
|
day_of_week: str,
|
||||||
enter_time: datetime,
|
enter_time: datetime,
|
||||||
|
shift: str,
|
||||||
):
|
):
|
||||||
employee = self.employee_repository.get(user_id)
|
employee = self.employee_repository.get(user_id)
|
||||||
if employee is None:
|
if employee is None:
|
||||||
return None
|
return None
|
||||||
return self.schedule_repository.create(user_id, enter_time, day_of_week, None, None)
|
|
||||||
|
if self.schedule_repository.get_by_user_and_day(user_id, day_of_week) is not None:
|
||||||
|
return ALREADY_WORKED_DAY
|
||||||
|
|
||||||
|
if self.schedule_repository.count_distinct_days_by_user(user_id) >= 5:
|
||||||
|
return MAX_DAYS_REACHED
|
||||||
|
|
||||||
|
return self.schedule_repository.create(user_id, enter_time, day_of_week, None, shift)
|
||||||
|
|
||||||
def clock_in_schedule(self, schedule_id: int, enter_time: datetime):
|
def clock_in_schedule(self, schedule_id: int, enter_time: datetime):
|
||||||
return self.schedule_repository.update_clock_in_by_id(schedule_id, enter_time)
|
return self.schedule_repository.update_clock_in_by_id(schedule_id, enter_time)
|
||||||
@@ -58,4 +69,13 @@ class ScheduleService:
|
|||||||
def generate_weekly_schedule(self, company_id: int):
|
def generate_weekly_schedule(self, company_id: int):
|
||||||
if self.company_repository.get(company_id) is None:
|
if self.company_repository.get(company_id) is None:
|
||||||
return None
|
return None
|
||||||
|
try:
|
||||||
|
from scheduler.manager.scheduler import generate_weekly_schedule
|
||||||
|
|
||||||
|
# reuse manager generation (connection from repository)
|
||||||
|
connection = getattr(self.schedule_repository, "connection", None)
|
||||||
|
if connection is not None:
|
||||||
|
generate_weekly_schedule(connection, company_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return self.get_weekly_schedule(company_id)
|
return self.get_weekly_schedule(company_id)
|
||||||
|
|||||||
@@ -81,12 +81,16 @@ class Database:
|
|||||||
enter_time TIMESTAMP NOT NULL,
|
enter_time TIMESTAMP NOT NULL,
|
||||||
day_of_week day_of_week NOT NULL,
|
day_of_week day_of_week NOT NULL,
|
||||||
out_time TIMESTAMP,
|
out_time TIMESTAMP,
|
||||||
|
shift work_time NOT NULL,
|
||||||
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
|
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
date_updated 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("ALTER TABLE schedules ALTER COLUMN out_time DROP NOT NULL;")
|
||||||
|
cursor.execute("ALTER TABLE schedules ADD COLUMN IF NOT EXISTS shift work_time;")
|
||||||
|
cursor.execute("UPDATE schedules SET shift = 'morning' WHERE shift IS NULL;")
|
||||||
|
cursor.execute("ALTER TABLE schedules ALTER COLUMN shift SET NOT NULL;")
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS weekly_preferences (
|
CREATE TABLE IF NOT EXISTS weekly_preferences (
|
||||||
|
|||||||
@@ -176,3 +176,53 @@ class EmployeeRepository:
|
|||||||
)
|
)
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
return self.get(employee_id)
|
return self.get(employee_id)
|
||||||
|
|
||||||
|
def get_company_id_by_user(self, user_id: int):
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT company_id
|
||||||
|
FROM company_users
|
||||||
|
WHERE user_id = %s
|
||||||
|
ORDER BY date_updated DESC, id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return None if row is None else row["company_id"]
|
||||||
|
|
||||||
|
def list_ids_by_company(self, company_id: int):
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT user_id
|
||||||
|
FROM company_users
|
||||||
|
WHERE company_id = %s
|
||||||
|
ORDER BY user_id
|
||||||
|
""",
|
||||||
|
(company_id,),
|
||||||
|
)
|
||||||
|
return [row["user_id"] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
def get_preference(self, user_id: int):
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT preference FROM users WHERE id = %s",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return None if row is None else row["preference"]
|
||||||
|
|
||||||
|
def get_weekly_preferences(self, user_id: int, day_of_week: str):
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT first, second, third
|
||||||
|
FROM weekly_preferences
|
||||||
|
WHERE user_id = %s AND day_of_week = %s
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(user_id, day_of_week),
|
||||||
|
)
|
||||||
|
return cursor.fetchone()
|
||||||
|
|||||||
@@ -26,6 +26,33 @@ class ScheduleRepository:
|
|||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
|
def get_by_user_and_day(self, user_id: int, day_of_week: str):
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, user_id, enter_time, day_of_week, out_time, shift
|
||||||
|
FROM schedules
|
||||||
|
WHERE user_id = %s AND day_of_week = %s
|
||||||
|
ORDER BY date_created DESC
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(user_id, day_of_week),
|
||||||
|
)
|
||||||
|
return cursor.fetchone()
|
||||||
|
|
||||||
|
def count_distinct_days_by_user(self, user_id: int):
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(DISTINCT day_of_week) AS days_worked
|
||||||
|
FROM schedules
|
||||||
|
WHERE user_id = %s
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return row["days_worked"]
|
||||||
|
|
||||||
def list_weekly(self, company_id: int | None = None):
|
def list_weekly(self, company_id: int | None = None):
|
||||||
params = ()
|
params = ()
|
||||||
where = ""
|
where = ""
|
||||||
@@ -36,7 +63,7 @@ class ScheduleRepository:
|
|||||||
cursor.execute(
|
cursor.execute(
|
||||||
f"""
|
f"""
|
||||||
SELECT schedules.id, schedules.enter_time, schedules.day_of_week,
|
SELECT schedules.id, schedules.enter_time, schedules.day_of_week,
|
||||||
schedules.out_time, users.name AS user_name,
|
schedules.out_time, schedules.shift, users.name AS user_name,
|
||||||
users.last_name AS user_last_name,
|
users.last_name AS user_last_name,
|
||||||
users.preference AS user_preference,
|
users.preference AS user_preference,
|
||||||
companies.name AS company
|
companies.name AS company
|
||||||
@@ -63,6 +90,7 @@ class ScheduleRepository:
|
|||||||
"enter_time": schedule["enter_time"],
|
"enter_time": schedule["enter_time"],
|
||||||
"day_of_week": schedule["day_of_week"],
|
"day_of_week": schedule["day_of_week"],
|
||||||
"out_time": schedule["out_time"],
|
"out_time": schedule["out_time"],
|
||||||
|
"shift": schedule["shift"],
|
||||||
}
|
}
|
||||||
for schedule in schedules
|
for schedule in schedules
|
||||||
]
|
]
|
||||||
@@ -80,7 +108,7 @@ class ScheduleRepository:
|
|||||||
ORDER BY date_created DESC
|
ORDER BY date_created DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
RETURNING id, user_id, enter_time, day_of_week, out_time
|
RETURNING id, user_id, enter_time, day_of_week, out_time, shift
|
||||||
""",
|
""",
|
||||||
(out_time, user_id, day_of_week),
|
(out_time, user_id, day_of_week),
|
||||||
)
|
)
|
||||||
@@ -117,3 +145,33 @@ class ScheduleRepository:
|
|||||||
schedule = cursor.fetchone()
|
schedule = cursor.fetchone()
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
|
def count_by_day_shift(
|
||||||
|
self, day_of_week: str, shift: str, company_id: int | None = None
|
||||||
|
):
|
||||||
|
if company_id is not None:
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) AS cnt
|
||||||
|
FROM schedules
|
||||||
|
INNER JOIN company_users ON company_users.user_id = schedules.user_id
|
||||||
|
WHERE schedules.day_of_week = %s
|
||||||
|
AND schedules.shift = %s
|
||||||
|
AND company_users.company_id = %s
|
||||||
|
""",
|
||||||
|
(day_of_week, shift, company_id),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return row["cnt"]
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) AS cnt
|
||||||
|
FROM schedules
|
||||||
|
WHERE day_of_week = %s AND shift = %s
|
||||||
|
""",
|
||||||
|
(day_of_week, shift),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return row["cnt"]
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ def remove_user(connection, user_id):
|
|||||||
return EmployeeRepository(connection).delete(user_id)
|
return EmployeeRepository(connection).delete(user_id)
|
||||||
|
|
||||||
|
|
||||||
def add_schedule(connection, user_id, enter_time, day_of_week, out_time):
|
def add_schedule(connection, user_id, enter_time, day_of_week, out_time, shift):
|
||||||
return ScheduleRepository(connection).create(user_id, enter_time, day_of_week, out_time)
|
return ScheduleRepository(connection).create(
|
||||||
|
user_id, enter_time, day_of_week, out_time, shift
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_days_worked_by_user(connection, user_id):
|
def get_days_worked_by_user(connection, user_id):
|
||||||
@@ -90,4 +92,30 @@ def get_user_enter_time_by_day(connection, user_id, day_of_week):
|
|||||||
|
|
||||||
|
|
||||||
def update_day_end_by_user_and_day(connection, user_id, day_of_week, out_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)
|
return ScheduleRepository(connection).update_clock_out(
|
||||||
|
user_id, day_of_week, out_time
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_company_id(connection, user_id):
|
||||||
|
return EmployeeRepository(connection).get_company_id_by_user(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_company_employee_ids(connection, company_id):
|
||||||
|
return EmployeeRepository(connection).list_ids_by_company(company_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_preference(connection, user_id):
|
||||||
|
return EmployeeRepository(connection).get_preference(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_weekly_preferences(connection, user_id, day_of_week):
|
||||||
|
return EmployeeRepository(connection).get_weekly_preferences(
|
||||||
|
user_id, day_of_week
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def count_shift_employees(connection, day_of_week, shift, company_id=None):
|
||||||
|
return ScheduleRepository(connection).count_by_day_shift(
|
||||||
|
day_of_week, shift, company_id
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,32 +1,121 @@
|
|||||||
# Conflict resolution guide.
|
from datetime import datetime
|
||||||
# Import validator functions before moving employees between shifts or days.
|
|
||||||
|
from scheduler.database import storage
|
||||||
|
from scheduler.manager.validator import (
|
||||||
|
validate_employee_can_join_shift,
|
||||||
|
validate_user_has_not_worked_more_than_five_days,
|
||||||
|
validate_user_has_not_worked_same_day,
|
||||||
|
)
|
||||||
|
|
||||||
|
SHIFTS = ("morning", "afternoon", "evening")
|
||||||
|
DAYS = ("sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday")
|
||||||
|
MAX_EMPLOYEES_PER_SHIFT = 2
|
||||||
|
|
||||||
|
|
||||||
|
def _value(value):
|
||||||
|
return value.value if hasattr(value, "value") else value
|
||||||
|
|
||||||
|
|
||||||
|
def _count_shift_employees(connection, day_of_week, shift, company_id=None):
|
||||||
|
count = storage.count_shift_employees(
|
||||||
|
connection, _value(day_of_week), _value(shift), company_id
|
||||||
|
)
|
||||||
|
if isinstance(count, dict):
|
||||||
|
return count.get("cnt", count.get("employee_count", 0))
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def _is_shift_full(connection, day_of_week, shift, company_id=None):
|
||||||
|
return (
|
||||||
|
_count_shift_employees(connection, day_of_week, shift, company_id)
|
||||||
|
>= MAX_EMPLOYEES_PER_SHIFT
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user_company_id(connection, user_id):
|
||||||
|
return storage.get_user_company_id(connection, user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_company_employee_ids(connection, company_id):
|
||||||
|
return storage.get_company_employee_ids(connection, company_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_shift_preferences(connection, user_id, day_of_week):
|
||||||
|
row = storage.get_weekly_preferences(connection, user_id, _value(day_of_week))
|
||||||
|
if row is None:
|
||||||
|
return list(SHIFTS)
|
||||||
|
return [row["first"], row["second"], row["third"]]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_next_day(current_day):
|
||||||
|
day = _value(current_day)
|
||||||
|
if day not in DAYS:
|
||||||
|
return None
|
||||||
|
return DAYS[(DAYS.index(day) + 1) % len(DAYS)]
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_shift(connection, user_id, day_of_week, shift, company_id=None):
|
||||||
|
company_id = (
|
||||||
|
company_id
|
||||||
|
if company_id is not None
|
||||||
|
else _get_user_company_id(connection, user_id)
|
||||||
|
)
|
||||||
|
if _is_shift_full(connection, day_of_week, shift, company_id):
|
||||||
|
return None
|
||||||
|
if not validate_employee_can_join_shift(
|
||||||
|
connection, user_id, _value(day_of_week), _value(shift)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
return storage.add_schedule(
|
||||||
|
connection,
|
||||||
|
user_id,
|
||||||
|
datetime.now(),
|
||||||
|
_value(day_of_week),
|
||||||
|
None,
|
||||||
|
_value(shift),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def detect_preferred_shift_conflict(connection, user_id, day_of_week, preferred_shift):
|
def detect_preferred_shift_conflict(connection, user_id, day_of_week, preferred_shift):
|
||||||
# check if preferred shift is already full for selected day
|
company_id = _get_user_company_id(connection, user_id)
|
||||||
# check if user already works that day
|
return (
|
||||||
# check if user already worked five days this week
|
_is_shift_full(connection, day_of_week, preferred_shift, company_id)
|
||||||
pass
|
or not validate_user_has_not_worked_same_day(
|
||||||
|
connection, user_id, _value(day_of_week)
|
||||||
|
)
|
||||||
|
or not validate_user_has_not_worked_more_than_five_days(connection, user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def resolve_conflict_same_day(connection, user_id, day_of_week):
|
def resolve_conflict_same_day(connection, user_id, day_of_week):
|
||||||
# loop over other shifts on the same day
|
for shift in SHIFTS:
|
||||||
# choose the first shift where user can legally work
|
schedule = _assign_shift(connection, user_id, day_of_week, shift)
|
||||||
# assign only after validator confirms the schedule is valid
|
if schedule is not None:
|
||||||
pass
|
return schedule
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_conflict_next_day(connection, user_id, current_day):
|
def resolve_conflict_next_day(connection, user_id, current_day):
|
||||||
# if same-day alternatives fail, move to next day
|
next_day = _get_next_day(current_day)
|
||||||
# loop through next-day shifts by preference priority
|
if next_day is None:
|
||||||
# assign only after validator confirms the schedule is valid
|
return None
|
||||||
pass
|
|
||||||
|
for shift in _get_shift_preferences(connection, user_id, next_day):
|
||||||
|
schedule = _assign_shift(connection, user_id, next_day, shift)
|
||||||
|
if schedule is not None:
|
||||||
|
return schedule
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_missing_second_employee(connection, company_id, day_of_week, shift):
|
def resolve_missing_second_employee(connection, company_id, day_of_week, shift):
|
||||||
# find shifts with only one employee assigned
|
if (
|
||||||
# loop over available employees in company
|
_count_shift_employees(connection, day_of_week, shift, company_id)
|
||||||
# skip employees who already work that day
|
>= MAX_EMPLOYEES_PER_SHIFT
|
||||||
# skip employees who already worked five days this week
|
):
|
||||||
# assign a valid extra employee to satisfy minimum coverage
|
return None
|
||||||
pass
|
|
||||||
|
for user_id in _get_company_employee_ids(connection, company_id):
|
||||||
|
schedule = _assign_shift(connection, user_id, day_of_week, shift, company_id)
|
||||||
|
if schedule is not None:
|
||||||
|
return schedule
|
||||||
|
return None
|
||||||
|
|||||||
@@ -1,52 +1,91 @@
|
|||||||
# Schedule generation guide.
|
from datetime import datetime
|
||||||
# Import database.storage for queries and manager.validator/conflict for rules.
|
|
||||||
|
from scheduler.core.enums import DayOfWeek, WorkTime
|
||||||
|
from scheduler.database import storage
|
||||||
|
from scheduler.manager import conflict
|
||||||
|
from scheduler.manager.validator import validate_employee_can_join_shift
|
||||||
|
|
||||||
|
DAYS = tuple(d.value for d in DayOfWeek)
|
||||||
|
SHIFTS = tuple(s.value for s in WorkTime)
|
||||||
|
|
||||||
|
|
||||||
|
def _value(value):
|
||||||
|
return value.value if hasattr(value, "value") else value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_company_employee_ids(connection, company_id):
|
||||||
|
return storage.get_company_employee_ids(connection, company_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user_default_preference(connection, user_id):
|
||||||
|
return storage.get_user_preference(connection, user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_preferences_for_day(connection, user_id, day_of_week):
|
||||||
|
row = storage.get_weekly_preferences(connection, user_id, _value(day_of_week))
|
||||||
|
if row is not None:
|
||||||
|
return [row["first"], row["second"], row["third"]]
|
||||||
|
fallback = _get_user_default_preference(connection, user_id)
|
||||||
|
if fallback is not None:
|
||||||
|
fallback = _value(fallback)
|
||||||
|
ordered = [fallback] + [s for s in SHIFTS if s != fallback]
|
||||||
|
return ordered
|
||||||
|
return list(SHIFTS)
|
||||||
|
|
||||||
|
|
||||||
def generate_weekly_schedule(connection, company_id):
|
def generate_weekly_schedule(connection, company_id):
|
||||||
# load all employees for selected company
|
employee_ids = _get_company_employee_ids(connection, company_id)
|
||||||
# load employee preferences for all seven days
|
if not employee_ids:
|
||||||
# loop through each day of the week
|
return []
|
||||||
# loop through morning, afternoon, and evening shifts
|
|
||||||
# assign employees based on preferences first
|
created: list[dict] = []
|
||||||
# validate one-shift-per-day before each assignment
|
|
||||||
# validate five-days-per-week before each assignment
|
for day_of_week in DAYS:
|
||||||
# ensure at least two employees per shift after assignments
|
for user_id in employee_ids:
|
||||||
# resolve conflicts by assigning another shift on same day or next day
|
preferences = _get_preferences_for_day(connection, user_id, day_of_week)
|
||||||
# save final schedule rows to the schedules table
|
assigned = None
|
||||||
pass
|
for preferred_shift in preferences:
|
||||||
|
assigned = assign_preferred_shift(
|
||||||
|
connection, user_id, day_of_week, preferred_shift
|
||||||
|
)
|
||||||
|
if assigned is not None:
|
||||||
|
created.append(assigned)
|
||||||
|
break
|
||||||
|
if assigned is None:
|
||||||
|
same_day = conflict.resolve_conflict_same_day(
|
||||||
|
connection, user_id, day_of_week
|
||||||
|
)
|
||||||
|
if same_day is not None:
|
||||||
|
created.append(same_day)
|
||||||
|
continue
|
||||||
|
next_day = conflict.resolve_conflict_next_day(
|
||||||
|
connection, user_id, day_of_week
|
||||||
|
)
|
||||||
|
if next_day is not None:
|
||||||
|
created.append(next_day)
|
||||||
|
|
||||||
|
for shift in SHIFTS:
|
||||||
|
extra = fill_missing_shift_coverage(
|
||||||
|
connection, company_id, day_of_week, shift
|
||||||
|
)
|
||||||
|
if extra is not None:
|
||||||
|
created.append(extra)
|
||||||
|
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
def assign_preferred_shift(connection, user_id, day_of_week, preferred_shift):
|
def assign_preferred_shift(connection, user_id, day_of_week, preferred_shift):
|
||||||
# validate employee can join selected shift
|
day = _value(day_of_week)
|
||||||
# call storage.add_schedule when assignment is valid
|
shift = _value(preferred_shift)
|
||||||
# return validation error if assignment is not valid
|
if not validate_employee_can_join_shift(connection, user_id, day, shift):
|
||||||
pass
|
return None
|
||||||
|
company_id = storage.get_user_company_id(connection, user_id)
|
||||||
|
if storage.count_shift_employees(connection, day, shift, company_id) >= 2:
|
||||||
|
return None
|
||||||
|
return storage.add_schedule(connection, user_id, datetime.now(), day, None, shift)
|
||||||
|
|
||||||
|
|
||||||
def fill_missing_shift_coverage(connection, company_id, day_of_week, shift):
|
def fill_missing_shift_coverage(connection, company_id, day_of_week, shift):
|
||||||
# find company shift with fewer than two employees
|
return conflict.resolve_missing_second_employee(
|
||||||
# if only one employee is assigned, search for another valid employee
|
connection, company_id, _value(day_of_week), _value(shift)
|
||||||
# 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
|
|
||||||
|
|||||||
@@ -1,38 +1,27 @@
|
|||||||
# Validation guide for schedule rules.
|
from scheduler.database import storage
|
||||||
# Import scheduler.database.storage query functions before implementing these checks.
|
|
||||||
|
|
||||||
|
|
||||||
def validate_user_has_not_worked_same_day(connection, user_id, day_of_week):
|
def validate_user_has_not_worked_same_day(connection, user_id, day_of_week):
|
||||||
# query schedules for user_id and day_of_week
|
return storage.get_user_enter_time_by_day(connection, user_id, day_of_week) is None
|
||||||
# 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):
|
def validate_user_has_not_worked_more_than_five_days(connection, user_id):
|
||||||
# call storage.get_days_worked_by_user(connection, user_id)
|
return len(storage.get_days_worked_by_user(connection, user_id)) < 5
|
||||||
# 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):
|
def validate_shift_has_minimum_two_employees(
|
||||||
# query schedules for company_id, day_of_week, and shift
|
connection, company_id, day_of_week, shift
|
||||||
# count assigned employees
|
):
|
||||||
# if fewer than two, scheduler must add another available employee
|
return storage.count_shift_employees(
|
||||||
pass
|
connection, day_of_week, shift, company_id
|
||||||
|
) >= 2
|
||||||
|
|
||||||
|
|
||||||
def validate_employee_can_join_shift(connection, user_id, day_of_week, shift):
|
def validate_employee_can_join_shift(connection, user_id, day_of_week, shift):
|
||||||
# check one-shift-per-day rule first
|
return validate_user_has_not_worked_same_day(
|
||||||
# check five-days-per-week rule second
|
connection, user_id, day_of_week
|
||||||
# check shift coverage and capacity rules last
|
) and validate_user_has_not_worked_more_than_five_days(connection, user_id)
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def validate_company_has_enough_employees(connection, company_id):
|
def validate_company_has_enough_employees(connection, company_id):
|
||||||
# get employee names by company
|
return len(storage.get_company_employee_ids(connection, company_id)) >= 2
|
||||||
# reject schedule generation if company has fewer than two employees
|
|
||||||
# otherwise continue schedule generation
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
from scheduler.core.enums import WorkTime
|
||||||
|
from scheduler.core.schemas.employee import DayPreferenceRequest
|
||||||
from scheduler.database import storage
|
from scheduler.database import storage
|
||||||
|
|
||||||
|
|
||||||
def define_employee_fields():
|
def define_employee_fields():
|
||||||
# include id from users table
|
return {
|
||||||
# include company_id from company_users table
|
"id": int,
|
||||||
# include name and last_name from users table
|
"company_id": int,
|
||||||
# include preferred shift for each day of the week
|
"name": str,
|
||||||
# include optional priority ranking for bonus preference support
|
"last_name": str,
|
||||||
pass
|
"preference": WorkTime,
|
||||||
|
"weekly_preferences": list[DayPreferenceRequest],
|
||||||
|
"priority": int | None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def register_employee_preference(connection, user_id, preference):
|
def register_employee_preference(connection, user_id, preference):
|
||||||
|
|||||||
@@ -1,23 +1,80 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from scheduler.core.enums import DayOfWeek, WorkTime
|
||||||
from scheduler.database import storage
|
from scheduler.database import storage
|
||||||
|
from scheduler.database.schedule_repository import ScheduleRepository
|
||||||
|
from scheduler.manager.validator import validate_employee_can_join_shift
|
||||||
|
|
||||||
|
|
||||||
def define_schedule_fields():
|
def define_schedule_fields():
|
||||||
# include user_id to identify the employee
|
return {
|
||||||
# include company_id if schedules need to be filtered by company
|
"user_id": int,
|
||||||
# include day_of_week for the assigned day
|
"company_id": int,
|
||||||
# include shift value for morning, afternoon, or evening
|
"day_of_week": DayOfWeek,
|
||||||
# include enter_time and out_time for clock-in and clock-out
|
"shift": WorkTime,
|
||||||
pass
|
"enter_time": datetime,
|
||||||
|
"out_time": datetime | None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def register_user_enter_and_out(connection, user_id, enter_time, day_of_week, out_time):
|
def _value(value):
|
||||||
# Add manager validation before saving once scheduling rules are complete.
|
return value.value if hasattr(value, "value") else value
|
||||||
return storage.add_schedule(connection, user_id, enter_time, day_of_week, out_time)
|
|
||||||
|
|
||||||
|
|
||||||
def output_weekly_schedule():
|
def register_user_enter_and_out(
|
||||||
# group saved schedules by day of week
|
connection, user_id, enter_time, day_of_week, out_time, shift
|
||||||
# inside each day, group schedules by shift
|
):
|
||||||
# list employee names for each shift
|
if not validate_employee_can_join_shift(
|
||||||
# show missing coverage when a shift has fewer than two employees
|
connection, user_id, _value(day_of_week), _value(shift)
|
||||||
pass
|
):
|
||||||
|
return None
|
||||||
|
return storage.add_schedule(
|
||||||
|
connection,
|
||||||
|
user_id,
|
||||||
|
enter_time,
|
||||||
|
_value(day_of_week),
|
||||||
|
out_time,
|
||||||
|
_value(shift),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def output_weekly_schedule(connection=None, company_id=None):
|
||||||
|
should_close = False
|
||||||
|
if connection is None:
|
||||||
|
connection = storage.get_connection()
|
||||||
|
should_close = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
repository = ScheduleRepository(connection)
|
||||||
|
schedules = repository.list_weekly(company_id)
|
||||||
|
|
||||||
|
grouped: dict[str, dict[str, list[str]]] = {}
|
||||||
|
for schedule in schedules:
|
||||||
|
day = _value(schedule["day_of_week"])
|
||||||
|
shift = _value(schedule["shift"])
|
||||||
|
name = f"{schedule['user']['name']} {schedule['user']['last_name']}"
|
||||||
|
grouped.setdefault(day, {}).setdefault(shift, []).append(name)
|
||||||
|
|
||||||
|
days_order = [d.value for d in DayOfWeek]
|
||||||
|
shifts_order = [s.value for s in WorkTime]
|
||||||
|
|
||||||
|
for day in days_order:
|
||||||
|
shifts = grouped.get(day, {})
|
||||||
|
if not shifts:
|
||||||
|
continue
|
||||||
|
print(f"{day}:")
|
||||||
|
for shift in shifts_order:
|
||||||
|
names = shifts.get(shift, [])
|
||||||
|
if not names:
|
||||||
|
continue
|
||||||
|
print(f" {shift}: {', '.join(names)}")
|
||||||
|
if len(names) < 2:
|
||||||
|
print(
|
||||||
|
f" warning: {shift} on {day} "
|
||||||
|
"has fewer than two employees"
|
||||||
|
)
|
||||||
|
|
||||||
|
return grouped
|
||||||
|
finally:
|
||||||
|
if should_close:
|
||||||
|
connection.close()
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
# Shift model guide.
|
from scheduler.core.enums import DayOfWeek, WorkTime
|
||||||
# Keep shift names consistent with the PostgreSQL work_time enum.
|
from scheduler.database import storage
|
||||||
|
from scheduler.manager.validator import validate_shift_has_minimum_two_employees
|
||||||
|
|
||||||
|
|
||||||
def define_shift_values():
|
def define_shift_values():
|
||||||
# morning maps to work_time value "morning"
|
return tuple(s.value for s in WorkTime)
|
||||||
# afternoon can map to "evening" if you keep the Rust enum name
|
|
||||||
# evening maps to work_time value "evening"
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def define_day_values():
|
def define_day_values():
|
||||||
# use sunday through saturday to match the day_of_week enum
|
return tuple(d.value for d in DayOfWeek)
|
||||||
# loop over all seven days when generating the weekly schedule
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def compare_shift_capacity():
|
def compare_shift_capacity(connection, company_id, day_of_week, shift):
|
||||||
# count employees assigned to one shift on one day
|
shift_value = shift.value if hasattr(shift, "value") else shift
|
||||||
# if the count is fewer than two, scheduler must add another employee
|
day_value = day_of_week.value if hasattr(day_of_week, "value") else day_of_week
|
||||||
# if the preferred shift is full, conflict manager must choose another shift
|
count = storage.count_shift_employees(
|
||||||
pass
|
connection, day_value, shift_value, company_id
|
||||||
|
)
|
||||||
|
has_minimum = validate_shift_has_minimum_two_employees(
|
||||||
|
connection, company_id, day_value, shift_value
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"count": count,
|
||||||
|
"has_minimum_two": has_minimum,
|
||||||
|
"needs_coverage": not has_minimum,
|
||||||
|
"is_full": count >= 2,
|
||||||
|
"should_add_employee": count < 2,
|
||||||
|
"should_resolve_conflict": count >= 2,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user