Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions App/controllers/scheduler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from App.models import Staff, Shift

from App.strategies.schedule_generator import ScheduleGenerator
from App.strategies.evendistribution import EvenDistributionStrategy
from App.strategies.balancedaynight import BalanceDayNightStrategy
from App.strategies.minimizedays import MinimizeDaysStrategy




def auto_generate_schedule(strategy_name="even", week_start=None):
staff_list = Staff.query.all()

if not staff_list:
raise ValueError("No staff members available for scheduling")

generator = ScheduleGenerator()
generator.setStaffList(staff_list)

if strategy_name == "even":
generator.setStrategy(EvenDistributionStrategy())
elif strategy_name == "balance_day_night":
generator.setStrategy(BalanceDayNightStrategy())
elif strategy_name == "minimize_days":
generator.setStrategy(MinimizeDaysStrategy())
else:
raise ValueError(f"Unknown strategy name: {strategy_name}")

return generator.generateSchedule(week_start)
33 changes: 24 additions & 9 deletions App/models/schedule.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
from datetime import datetime
from App.database import db

#updated to match UML class diagram

class Schedule(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_by = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
weekStart = db.Column(db.Date, nullable=True)
shifts = db.relationship("Shift", backref="schedule", lazy=True)

def shift_count(self):
return len(self.shifts)
def get_all_shifts(self):
return self.shifts

def get_shifts_by_staff(self, staff):
return [shift for shift in self.shifts if shift.staff_id == staff.id]


def add_shift(self, shift):
shift.schedule_id = self.id
self.shifts.append(shift)

def validate_schedule(self):
if not self.shifts:
return False
for shift in self.shifts:
if shift.staff_id is None:
return False
return True


def get_json(self):

return {
"id": self.id,
"name": self.name,
"created_at": self.created_at.isoformat(),
"created_by": self.created_by,
"shift_count": self.shift_count(),
"weekStart": self.weekStart.strftime("%Y-%m-%d") if self.weekStart else None,
"shifts": [shift.get_json() for shift in self.shifts]
}

Expand Down
7 changes: 7 additions & 0 deletions App/strategies/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .scheduling_strategy import SchedulingStrategy
from .schedule_generator import ScheduleGenerator
from .evendistribution import EvenDistributionStrategy
from .balancedaynight import BalanceDayNightStrategy
from .minimizedays import MinimizeDaysStrategy

__all__ = ["SchedulingStrategy", "ScheduleGenerator","EvenDistributionStrategy", "MinimizeDaysStrategy", "BalanceDayNightStrategy"]
62 changes: 62 additions & 0 deletions App/strategies/balancedaynight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from .scheduling_strategy import SchedulingStrategy



# again this function was moved to here as it is the only strategy that uses it.

def get_shift_type(shift):
if shift is None:
return "day"

startTime = getattr(shift, "start_time", None)

if startTime is not None:
if hasattr(startTime, "hour"):
hour = startTime.hour
if hour >= 18 or hour < 6:
return "night"
else:
return "day"

return "day"


class BalanceDayNightStrategy(SchedulingStrategy):


def distribute(self, staff_list, shifts, week_start=None):
assignments = {}

if not staff_list:
return assignments

staff_ids = [str(staffMember.id) for staffMember in staff_list]

for staff_id in staff_ids:
assignments[staff_id] = []

counts = {staff_id: {'day': 0, 'night': 0} for staff_id in staff_ids}

for shift in shifts:
shift_type = get_shift_type(shift)

def score(staff_id):
return (counts[staff_id].get(shift_type, 0), len(assignments[staff_id]))

chosen_staff = min(staff_ids, key=score)
assignments[chosen_staff].append(shift)
counts[chosen_staff][shift_type] = counts[chosen_staff].get(shift_type, 0) + 1


return assignments

"""
This strategy uses a greedy algorithm to balance the number of day and night shifts assigned to each staff member.
It first determins whether the shift is a day or night shift using the get_shift_type function.
It calculates a score for each staff member based on how many shifts of that type they already have assigned, as well as their total number of assigned shifts.
The staff member with the lowest score is chosen to receive the shift, helping to ensure an even distribution of day and night shifts among all staff members.

e.g if s1 has 2 day shifts and s2 has 1, the next day shift will go to s2 to balance it out.

- VR.
"""
29 changes: 29 additions & 0 deletions App/strategies/evendistribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from .scheduling_strategy import SchedulingStrategy



class EvenDistributionStrategy(SchedulingStrategy):

def distribute(self, staff_list, shifts, week_start=None):
assignments = {}

if not staff_list:
return assignments


staff_ids = [str(staffMember.id) for staffMember in staff_list]

for staff_id in staff_ids:
assignments[staff_id] = []

i = 0
num_staff = len(staff_ids)

for shift in shifts:
staff_id = staff_ids[i % num_staff]
assignments[staff_id].append(shift)
i += 1

return assignments

# This use an even distribution, a round robin approach, to assign shifts to staff members in order. - VR
65 changes: 65 additions & 0 deletions App/strategies/minimizedays.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from .scheduling_strategy import SchedulingStrategy


#this method was relocated here as it is only used by this strategy

def get_shift_day(shift):
if shift is None:
return None

startTime = getattr(shift, "start_time", None)

if startTime is not None:
if hasattr(startTime, "date"):
return startTime.date()
return None


class MinimizeDaysStrategy(SchedulingStrategy):


def distribute(self, staff_list, shifts, week_start=None):
assignments = {}

if not staff_list:
return assignments

staff_ids = [str(staffMember.id) for staffMember in staff_list]

for staff_id in staff_ids:
assignments[staff_id] = []

days = {staff_id: set() for staff_id in staff_ids}

for shift in shifts:
shift_day = get_shift_day(shift)

chosen = None

for staff_id in staff_ids:
if shift_day in days[staff_id]:
chosen = staff_id
break

if chosen is None:
chosen = min(staff_ids, key = lambda x: (len(days[x]), len(assignments[x])))

assignments[chosen].append(shift)
if shift_day is not None:
days[chosen].add(shift_day)


return assignments


"""
This strategy as the name suggests aims to reduce the number of distinct days that each staff members comes in to work.
When a new shift is to be assigned, it first checks if there is any staff member who is already scheduled to work on that day.
If such a staff member is found, the shift is assigned to them, so that they can work multiple shifts on the same day and thus minimize the total number of days they need to come in.
If no staff member is already scheduled for that day, the strategy selects the staff member who currently has the fewest distinct working days.
If there is a tie, it further breaks the tie by choosing the staff member with the fewest total assigned shifts.
This gives the staff more complete days off by goruping all their shifts into fewer days instead of spreading them out.
- VR.


"""
44 changes: 44 additions & 0 deletions App/strategies/schedule_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from App.models import Schedule, Staff, Shift
from App.database import db



class ScheduleGenerator:
def __init__(self):
self.strategy = None
self.staffList = []

def setStrategy(self, strategy):
self.strategy = strategy

def setStaffList(self, staffList):
self.staffList = staffList

def generateSchedule(self, week_start=None):
if self.strategy is None:
raise ValueError("No scheduling strategy set")

if not self.staffList:
raise ValueError("Staff list is empty")

unassigned_shifts = Shift.query.filter_by(staff_id = None).all()

if not unassigned_shifts:
raise ValueError("No unassigned shifts available for scheduling")

assignments = self.strategy.distribute(self.staffList, unassigned_shifts, week_start)

new_schedule = Schedule(weekStart=week_start)

db.session.add(new_schedule)
db.session.flush()

for staff_id, shifts in assignments.items():
for shift in shifts:
shift.staff_id = int(staff_id)
shift.schedule_id = new_schedule.id

db.session.commit()

return new_schedule

6 changes: 6 additions & 0 deletions App/strategies/scheduling_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from abc import ABC, abstractmethod

class SchedulingStrategy(ABC):
@abstractmethod
def distribute(self, staff, shifts, week_start=None):
raise NotImplementedError("distribute must be implemented by subclasses")