From c2b17fc5f7d00d57a8e53150f49322922983546f Mon Sep 17 00:00:00 2001 From: VisheshRampersad Date: Sat, 22 Nov 2025 04:00:49 +0000 Subject: [PATCH 1/7] Added the strategy pattern for the autoscheduling, it includes the init file, schedule class, the three scheduling methods. --- App/strategies/__init__.py | 6 ++ App/strategies/balancedaynight.py | 42 +++++++++++ App/strategies/evendistribution.py | 28 ++++++++ App/strategies/minimizedays.py | 50 +++++++++++++ App/strategies/strategy.py | 110 +++++++++++++++++++++++++++++ 5 files changed, 236 insertions(+) create mode 100644 App/strategies/__init__.py create mode 100644 App/strategies/balancedaynight.py create mode 100644 App/strategies/evendistribution.py create mode 100644 App/strategies/minimizedays.py create mode 100644 App/strategies/strategy.py diff --git a/App/strategies/__init__.py b/App/strategies/__init__.py new file mode 100644 index 0000000..1dbf826 --- /dev/null +++ b/App/strategies/__init__.py @@ -0,0 +1,6 @@ +from .strategy import Schedule, ScheduleStrategy, get_staff_id, get_shift_id, get_shift_day, get_shift_type +from .evendistribution import EvenDistribution +from .balancedaynight import BalanceDayNight +from .minimizedays import MinimizeDays + +__all__ = ["Schedule", "ScheduleStrategy", "get_staff_id", "get_shift_id", "get_shift_day", "get_shift_type", "EvenDistribution", "BalanceDayNight", "MinimizeDays"] \ No newline at end of file diff --git a/App/strategies/balancedaynight.py b/App/strategies/balancedaynight.py new file mode 100644 index 0000000..9e1ac1b --- /dev/null +++ b/App/strategies/balancedaynight.py @@ -0,0 +1,42 @@ +from .strategy import * + + + +class BalanceDayNight(ScheduleStrategy): + + + def distribute_shifts(self, staff, shifts, week_start=None): + assignments = {} + + if not staff: + return Schedule(assignments) + + staff_ids = [get_staff_id(member) for member in staff] + 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 Schedule(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. +""" \ No newline at end of file diff --git a/App/strategies/evendistribution.py b/App/strategies/evendistribution.py new file mode 100644 index 0000000..276f430 --- /dev/null +++ b/App/strategies/evendistribution.py @@ -0,0 +1,28 @@ +from .strategy import * + + + +class EvenDistribution(ScheduleStrategy): + + def distribute_shifts(self, staff, shifts, week_start=None): + assignments = {} + + if not staff: + return Schedule(assignments) + + + staff_ids = [get_staff_id(member) for member in staff] + 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 Schedule(assignments) + +# This use an even distribution, a round robin approach, to assign shifts to staff members in order. - VR \ No newline at end of file diff --git a/App/strategies/minimizedays.py b/App/strategies/minimizedays.py new file mode 100644 index 0000000..5092df2 --- /dev/null +++ b/App/strategies/minimizedays.py @@ -0,0 +1,50 @@ +from .strategy import * + +class MinimizeDays(ScheduleStrategy): + + + def distribute_shifts(self, staff, shifts, week_start=None): + assignments = {} + + if not staff: + return Schedule(assignments) + + staff_ids = [get_staff_id(member) for member in staff] + + 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 Schedule(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. + + +""" diff --git a/App/strategies/strategy.py b/App/strategies/strategy.py new file mode 100644 index 0000000..c166531 --- /dev/null +++ b/App/strategies/strategy.py @@ -0,0 +1,110 @@ +from abc import ABC, abstractmethod + +class Schedule: + def __init__(self, assignments): + if assignments is not None: + self.assignments = assignments + else: + self.assignments = {} + + def to_dict(self): + out = {} + for shiftID, shifts in self.assignments.items(): + out[shiftID] = [] + for item in shifts: + if isinstance(item, dict): + out[shiftID].append(item) + elif hasattr(item, "get_json"): + out[shiftID].append(item.get_json()) + else: + out[shiftID].append(str(item)) + return out + +class ScheduleStrategy(ABC): + @abstractmethod + def distribute_shifts(self, staff, shifts, week_start=None): + raise NotImplementedError("distribute_shifts must be implemented by subclasses") + + +def get_staff_id(staff_member): + if staff_member is None: + raise ValueError("Staff member cannot be None") + + if isinstance(staff_member, dict): + if "id" in staff_member and staff_member["id"] is not None: + return str(staff_member["id"]) + else: + val = getattr(staff_member, "id", None) + if val is not None: + return str(val) + + raise ValueError("Unable to determine staff member ID") + + +def get_shift_day(shift): + if shift is None: + return None + + if isinstance(shift, dict): + st = shift.get("start_time") + else: + st = getattr(shift, "start_time", None) + + if st is not None: + if hasattr(st, "date"): + return st.date() + if hasattr(st, "strftime"): + return st.strftime("%Y-%m-%d") + if isinstance(st, str): + return st.split("T")[0] + + return None + + +def get_shift_type(shift): + if shift is None: + return "day" + + if isinstance(shift, dict): + startTime = shift.get("start_time") + else: + 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" + + if isinstance(startTime, str): + try: + if "T" in startTime: + timePart = startTime.split("T")[1] + hour = int(timePart.split(":")[0]) + else: + hour = int(startTime.split(":")[0]) + if (hour >= 18 or hour < 6): + return "night" + else: + return "day" + + except Exception: + return "day" + + return "day" + + +def get_shift_id(shift): + if shift is None: + return None + + if isinstance(shift, dict): + if "id" in shift and shift["id"] is not None: + return shift["id"] + else: + val = getattr(shift, "id", None) + if val is not None: + return val + return None \ No newline at end of file From 4a9075db7c52b58f6deafe9d56a9e7b576a948c8 Mon Sep 17 00:00:00 2001 From: VisheshRampersad Date: Sat, 22 Nov 2025 23:20:40 +0000 Subject: [PATCH 2/7] auto scheduler controller --- App/controllers/scheduler.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 App/controllers/scheduler.py diff --git a/App/controllers/scheduler.py b/App/controllers/scheduler.py new file mode 100644 index 0000000..2552f66 --- /dev/null +++ b/App/controllers/scheduler.py @@ -0,0 +1,22 @@ +from App.models import Staff, Shift +from App.controllers.user import get_user +from App.strategies.evendistribution import EvenDistribution +from App.strategies.balancedaynight import BalanceDayNight +from App.strategies.minimizedays import MinimizeDays + +STRATEGIES = { "even": EvenDistribution, "balance_day_night": BalanceDayNight, "minimize_days": MinimizeDays } + +def auto_generate_schedule(admin_id, strategy_name="even"): + admin = get_user(admin_id) + if admin.role != "admin": + raise PermissionError("Only admins can generate schedules") + + strategy_class = STRATEGIES.get(strategy_name) + if not strategy_class: + raise ValueError(f"Invalid strategy '{strategy_name}'. Valid strategies are: {list(STRATEGIES.keys())}") + + strategy = strategy_class() + staff_list = Staff.query.all() + shifts = Shift.query.all() + + return strategy.distribute_shifts(staff_list, shifts) From 8d9ace9656de8fcee945c5de0f989fe2666eff3a Mon Sep 17 00:00:00 2001 From: VisheshRampersad Date: Sun, 23 Nov 2025 04:16:56 +0000 Subject: [PATCH 3/7] Fixed strategy pattern to reflect UML diagram --- App/controllers/scheduler.py | 41 ++++++---- App/models/schedule.py | 33 +++++--- App/strategies/__init__.py | 11 +-- App/strategies/balancedaynight.py | 34 ++++++-- App/strategies/evendistribution.py | 15 ++-- App/strategies/minimizedays.py | 29 +++++-- App/strategies/schedule_generator.py | 44 +++++++++++ App/strategies/scheduling_strategy.py | 6 ++ App/strategies/strategy.py | 110 -------------------------- 9 files changed, 161 insertions(+), 162 deletions(-) create mode 100644 App/strategies/schedule_generator.py create mode 100644 App/strategies/scheduling_strategy.py delete mode 100644 App/strategies/strategy.py diff --git a/App/controllers/scheduler.py b/App/controllers/scheduler.py index 2552f66..ed32507 100644 --- a/App/controllers/scheduler.py +++ b/App/controllers/scheduler.py @@ -1,22 +1,29 @@ from App.models import Staff, Shift -from App.controllers.user import get_user -from App.strategies.evendistribution import EvenDistribution -from App.strategies.balancedaynight import BalanceDayNight -from App.strategies.minimizedays import MinimizeDays -STRATEGIES = { "even": EvenDistribution, "balance_day_night": BalanceDayNight, "minimize_days": MinimizeDays } +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(admin_id, strategy_name="even"): - admin = get_user(admin_id) - if admin.role != "admin": - raise PermissionError("Only admins can generate schedules") - - strategy_class = STRATEGIES.get(strategy_name) - if not strategy_class: - raise ValueError(f"Invalid strategy '{strategy_name}'. Valid strategies are: {list(STRATEGIES.keys())}") - - strategy = strategy_class() + + + +def auto_generate_schedule(strategy_name="even", week_start=None): staff_list = Staff.query.all() - shifts = Shift.query.all() - return strategy.distribute_shifts(staff_list, shifts) + 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) diff --git a/App/models/schedule.py b/App/models/schedule.py index 64c0e24..4721595 100644 --- a/App/models/schedule.py +++ b/App/models/schedule.py @@ -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] } diff --git a/App/strategies/__init__.py b/App/strategies/__init__.py index 1dbf826..0bcf862 100644 --- a/App/strategies/__init__.py +++ b/App/strategies/__init__.py @@ -1,6 +1,7 @@ -from .strategy import Schedule, ScheduleStrategy, get_staff_id, get_shift_id, get_shift_day, get_shift_type -from .evendistribution import EvenDistribution -from .balancedaynight import BalanceDayNight -from .minimizedays import MinimizeDays +from .scheduling_strategy import SchedulingStrategy +from .schedule_generator import ScheduleGenerator +from .evendistribution import EvenDistributionStrategy +from .balancedaynight import BalanceDayNightStrategy +from .minimizedays import MinimizeDaysStrategy -__all__ = ["Schedule", "ScheduleStrategy", "get_staff_id", "get_shift_id", "get_shift_day", "get_shift_type", "EvenDistribution", "BalanceDayNight", "MinimizeDays"] \ No newline at end of file +__all__ = ["SchedulingStrategy", "ScheduleGenerator","EvenDistributionStrategy", "MinimizeDaysStrategy", "BalanceDayNightStrategy"] \ No newline at end of file diff --git a/App/strategies/balancedaynight.py b/App/strategies/balancedaynight.py index 9e1ac1b..b854e35 100644 --- a/App/strategies/balancedaynight.py +++ b/App/strategies/balancedaynight.py @@ -1,17 +1,37 @@ -from .strategy import * +from .scheduling_strategy import SchedulingStrategy -class BalanceDayNight(ScheduleStrategy): +# 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" - def distribute_shifts(self, staff, shifts, week_start=None): + +class BalanceDayNightStrategy(SchedulingStrategy): + + + def distribute(self, staff_list, shifts, week_start=None): assignments = {} - if not staff: - return Schedule(assignments) + if not staff_list: + return assignments - staff_ids = [get_staff_id(member) for member in staff] + staff_ids = [str(staffMember.id) for staffMember in staff_list] + for staff_id in staff_ids: assignments[staff_id] = [] @@ -28,7 +48,7 @@ def score(staff_id): counts[chosen_staff][shift_type] = counts[chosen_staff].get(shift_type, 0) + 1 - return Schedule(assignments) + return assignments """ This strategy uses a greedy algorithm to balance the number of day and night shifts assigned to each staff member. diff --git a/App/strategies/evendistribution.py b/App/strategies/evendistribution.py index 276f430..7fefc7e 100644 --- a/App/strategies/evendistribution.py +++ b/App/strategies/evendistribution.py @@ -1,17 +1,18 @@ -from .strategy import * +from .scheduling_strategy import SchedulingStrategy -class EvenDistribution(ScheduleStrategy): +class EvenDistributionStrategy(SchedulingStrategy): - def distribute_shifts(self, staff, shifts, week_start=None): + def distribute(self, staff_list, shifts, week_start=None): assignments = {} - if not staff: - return Schedule(assignments) + if not staff_list: + return assignments - staff_ids = [get_staff_id(member) for member in staff] + staff_ids = [str(staffMember.id) for staffMember in staff_list] + for staff_id in staff_ids: assignments[staff_id] = [] @@ -23,6 +24,6 @@ def distribute_shifts(self, staff, shifts, week_start=None): assignments[staff_id].append(shift) i += 1 - return Schedule(assignments) + return assignments # This use an even distribution, a round robin approach, to assign shifts to staff members in order. - VR \ No newline at end of file diff --git a/App/strategies/minimizedays.py b/App/strategies/minimizedays.py index 5092df2..485728f 100644 --- a/App/strategies/minimizedays.py +++ b/App/strategies/minimizedays.py @@ -1,15 +1,30 @@ -from .strategy import * +from .scheduling_strategy import SchedulingStrategy -class MinimizeDays(ScheduleStrategy): +#this method was relocated here as it is only used by this strategy - def distribute_shifts(self, staff, shifts, week_start=None): +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: - return Schedule(assignments) + if not staff_list: + return assignments - staff_ids = [get_staff_id(member) for member in staff] + staff_ids = [str(staffMember.id) for staffMember in staff_list] for staff_id in staff_ids: assignments[staff_id] = [] @@ -34,7 +49,7 @@ def distribute_shifts(self, staff, shifts, week_start=None): days[chosen].add(shift_day) - return Schedule(assignments) + return assignments """ diff --git a/App/strategies/schedule_generator.py b/App/strategies/schedule_generator.py new file mode 100644 index 0000000..bd6311c --- /dev/null +++ b/App/strategies/schedule_generator.py @@ -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 + diff --git a/App/strategies/scheduling_strategy.py b/App/strategies/scheduling_strategy.py new file mode 100644 index 0000000..4b78615 --- /dev/null +++ b/App/strategies/scheduling_strategy.py @@ -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") \ No newline at end of file diff --git a/App/strategies/strategy.py b/App/strategies/strategy.py deleted file mode 100644 index c166531..0000000 --- a/App/strategies/strategy.py +++ /dev/null @@ -1,110 +0,0 @@ -from abc import ABC, abstractmethod - -class Schedule: - def __init__(self, assignments): - if assignments is not None: - self.assignments = assignments - else: - self.assignments = {} - - def to_dict(self): - out = {} - for shiftID, shifts in self.assignments.items(): - out[shiftID] = [] - for item in shifts: - if isinstance(item, dict): - out[shiftID].append(item) - elif hasattr(item, "get_json"): - out[shiftID].append(item.get_json()) - else: - out[shiftID].append(str(item)) - return out - -class ScheduleStrategy(ABC): - @abstractmethod - def distribute_shifts(self, staff, shifts, week_start=None): - raise NotImplementedError("distribute_shifts must be implemented by subclasses") - - -def get_staff_id(staff_member): - if staff_member is None: - raise ValueError("Staff member cannot be None") - - if isinstance(staff_member, dict): - if "id" in staff_member and staff_member["id"] is not None: - return str(staff_member["id"]) - else: - val = getattr(staff_member, "id", None) - if val is not None: - return str(val) - - raise ValueError("Unable to determine staff member ID") - - -def get_shift_day(shift): - if shift is None: - return None - - if isinstance(shift, dict): - st = shift.get("start_time") - else: - st = getattr(shift, "start_time", None) - - if st is not None: - if hasattr(st, "date"): - return st.date() - if hasattr(st, "strftime"): - return st.strftime("%Y-%m-%d") - if isinstance(st, str): - return st.split("T")[0] - - return None - - -def get_shift_type(shift): - if shift is None: - return "day" - - if isinstance(shift, dict): - startTime = shift.get("start_time") - else: - 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" - - if isinstance(startTime, str): - try: - if "T" in startTime: - timePart = startTime.split("T")[1] - hour = int(timePart.split(":")[0]) - else: - hour = int(startTime.split(":")[0]) - if (hour >= 18 or hour < 6): - return "night" - else: - return "day" - - except Exception: - return "day" - - return "day" - - -def get_shift_id(shift): - if shift is None: - return None - - if isinstance(shift, dict): - if "id" in shift and shift["id"] is not None: - return shift["id"] - else: - val = getattr(shift, "id", None) - if val is not None: - return val - return None \ No newline at end of file From ef29caeffa6e20467281d5d8543eece8de0b9b0a Mon Sep 17 00:00:00 2001 From: Tyler-Baksh Date: Tue, 25 Nov 2025 17:18:43 -0400 Subject: [PATCH 4/7] Add new api endpoints and update postman postman collection --- App/controllers/__init__.py | 1 + App/controllers/admin.py | 6 +- App/views/adminView.py | 61 +++- RosterAPI.postman_collection.json | 509 +++++++++++++++++++++++++++++- 4 files changed, 564 insertions(+), 13 deletions(-) diff --git a/App/controllers/__init__.py b/App/controllers/__init__.py index 0cb8fd1..7ff6494 100644 --- a/App/controllers/__init__.py +++ b/App/controllers/__init__.py @@ -3,3 +3,4 @@ from .initialize import * from .admin import * from .staff import * +from .scheduler import * diff --git a/App/controllers/admin.py b/App/controllers/admin.py index aa2d9ca..307300e 100644 --- a/App/controllers/admin.py +++ b/App/controllers/admin.py @@ -8,15 +8,13 @@ from datetime import datetime from App.controllers.user import get_user -def create_schedule(admin_id, scheduleName): #Not sure why this was missing +def create_schedule(admin_id, week_start): #Not sure why this was missing admin = get_user(admin_id) if not admin or admin.role != "admin": raise PermissionError("Only admins can create schedules") new_schedule = Schedule( - created_by=admin_id, - name=scheduleName, - created_at=datetime.utcnow() + weekStart=week_start ) db.session.add(new_schedule) diff --git a/App/views/adminView.py b/App/views/adminView.py index dfbfe76..f823777 100644 --- a/App/views/adminView.py +++ b/App/views/adminView.py @@ -2,6 +2,8 @@ from flask import Blueprint, jsonify, request from datetime import datetime from App.controllers import staff, auth, admin +from App.controllers.user import get_user +from App.controllers.scheduler import auto_generate_schedule from flask_jwt_extended import jwt_required, get_jwt_identity from sqlalchemy.exc import SQLAlchemyError @@ -27,8 +29,10 @@ def createSchedule(): try: admin_id = get_jwt_identity() data = request.get_json() - scheduleName = data.get("scheduleName") # gets the scheduleName from the request body - schedule = admin.create_schedule(admin_id, scheduleName) # Call controller method + week_start = data.get("week_start") # gets the start week from the request body + date_format = "%Y-%m-%d" + formatted_week_start = datetime.strptime(week_start, date_format) + schedule = admin.create_schedule(admin_id, formatted_week_start) # Call controller method return jsonify(schedule.get_json()), 200 # Return the created schedule as JSON except (PermissionError, ValueError) as e: @@ -74,4 +78,55 @@ def shiftReport(): except (PermissionError, ValueError) as e: return jsonify({"error": str(e)}), 403 except SQLAlchemyError: - return jsonify({"error": "Database error"}), 500 \ No newline at end of file + return jsonify({"error": "Database error"}), 500 + +def _generate_schedule_handler(schedule_type): + """Helper function to handle schedule generation""" + admin_id = get_jwt_identity() + admin = get_user(admin_id) + + if not admin: + return jsonify({"error": "User not found"}), 404 + if admin.role != "admin": + return jsonify({"error": "Only admins can create schedules"}), 403 + + try: + data = request.get_json() + if not data: + return jsonify({"error": "Request body is required"}), 400 + + week_start = data.get("week_start") + if not week_start: + return jsonify({"error": "week_start is required"}), 400 + + date_format = "%Y-%m-%d" + formatted_week_start = datetime.strptime(week_start, date_format) + schedule = auto_generate_schedule(schedule_type, formatted_week_start) + + if not schedule: + return jsonify({"error": "Failed to generate schedule"}), 500 + + return jsonify(schedule.get_json()), 200 + + except ValueError as e: + return jsonify({"error": f"Invalid input: {str(e)}"}), 400 + except SQLAlchemyError as e: + return jsonify({"error": "Database error"}), 500 + except Exception as e: + return jsonify({"error": "An unexpected error occurred"}), 500 + + +@admin_view.route('/autoGenerateSchedule/even', methods=['POST']) +@jwt_required() +def autoGenerateEvenSchedule(): + return _generate_schedule_handler("even") + +@admin_view.route('/autoGenerateSchedule/balanceDayNight', methods=['POST']) +@jwt_required() +def autoGenerateBalanceDayNightSchedule(): + return _generate_schedule_handler("balance_day_night") + +@admin_view.route('/autoGenerateSchedule/minimizeDays', methods=['POST']) +@jwt_required() +def autoGenerateMinimizeDaysSchedule(): + return _generate_schedule_handler("minimize_days") \ No newline at end of file diff --git a/RosterAPI.postman_collection.json b/RosterAPI.postman_collection.json index 4ffdb60..85515ee 100644 --- a/RosterAPI.postman_collection.json +++ b/RosterAPI.postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "982ba4f2-1a0e-43bc-803b-5217c9831e62", + "_postman_id": "cc9c7bff-d18e-4286-8078-407d20e81496", "name": "RosterAPI", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "20616724", - "_collection_link": "https://manutd-3640.postman.co/workspace/ManUtd-Workspace~2fb3167d-d7e4-4b28-a65f-08d0dd95df9b/collection/20616724-982ba4f2-1a0e-43bc-803b-5217c9831e62?action=share&source=collection_link&creator=20616724" + "_exporter_id": "49511481", + "_collection_link": "https://tylerbaksh163-4871962.postman.co/workspace/Tyler-Baksh's-Workspace~1c148b6f-bd3c-4d7e-8995-6b8cf5e4eb21/collection/49511481-cc9c7bff-d18e-4286-8078-407d20e81496?action=share&source=collection_link&creator=49511481" }, "item": [ { @@ -19,7 +19,15 @@ "exec": [ "// js code to save the access_token for user authentication\r", "let jsonData = JSON.parse(responseBody);\r", - "pm.environment.set(\"access_token\",jsonData.access_token) // creates access_token variable in postman and sets it to the value of the access_token in the response body." + "pm.environment.set(\"access_token\",jsonData.access_token) // creates access_token variable in postman and sets it to the value of the access_token in the response body.\r", + "\r", + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Access token has been set\", function(){\r", + " pm.expect(pm.environment.get(\"access_token\")).to.eql(jsonData.access_token);\r", + "});" ], "type": "text/javascript", "packages": {}, @@ -54,6 +62,34 @@ }, { "name": "Admin: Create_Schedule", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Response contains schedule object\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"shifts\");\r", + " pm.expect(json).to.have.property(\"weekStart\");\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "request": { "auth": { "type": "bearer", @@ -69,7 +105,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"scheduleName\": \"Morning Shift\"\r\n}", + "raw": "{\r\n \"week_start\": \"2025-11-25\"\r\n}", "options": { "raw": { "language": "json" @@ -90,6 +126,39 @@ }, { "name": "Admin: Create_Shift", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Response contains shift object\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"clock_in\");\r", + " pm.expect(json).to.have.property(\"clock_out\");\r", + " pm.expect(json).to.have.property(\"end_time\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"schedule_id\");\r", + " pm.expect(json).to.have.property(\"staff_id\");\r", + " pm.expect(json).to.have.property(\"staff_name\");\r", + " pm.expect(json).to.have.property(\"start_time\");\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "request": { "auth": { "type": "bearer", @@ -124,8 +193,240 @@ }, "response": [] }, + { + "name": "Admin: Auto-Generate Schedule (Even)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Response contains schedule object\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"shifts\");\r", + " pm.expect(json).to.have.property(\"weekStart\");\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"week_start\": \"2025-11-25\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/autoGenerateSchedule/even", + "host": [ + "{{host}}" + ], + "path": [ + "autoGenerateSchedule", + "even" + ] + } + }, + "response": [] + }, + { + "name": "Admin: Auto-Generate Schedule (Balance Day Night)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Response contains schedule object\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"shifts\");\r", + " pm.expect(json).to.have.property(\"weekStart\");\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"week_start\": \"2025-11-25\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/autoGenerateSchedule/balanceDayNight", + "host": [ + "{{host}}" + ], + "path": [ + "autoGenerateSchedule", + "balanceDayNight" + ] + } + }, + "response": [] + }, + { + "name": "Admin: Auto-Generate Schedule (Minimize Days)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Response contains schedule object\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"shifts\");\r", + " pm.expect(json).to.have.property(\"weekStart\");\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"week_start\": \"2025-11-25\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/autoGenerateSchedule/minimizeDays", + "host": [ + "{{host}}" + ], + "path": [ + "autoGenerateSchedule", + "minimizeDays" + ] + } + }, + "response": [] + }, { "name": "Admin: Shift_Report", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Response body is valid JSON\", function () {", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();", + "});", + "", + "pm.test(\"Report contains valid shift details\", function () {", + " const json = pm.response.json();", + "", + " if (Array.isArray(json) && json.length > 0) {", + " const first = json[0];", + "", + " pm.expect(first).to.be.an(\"object\");", + " pm.expect(first).to.have.property(\"clock_in\");", + " pm.expect(first).to.have.property(\"clock_out\");", + " pm.expect(first).to.have.property(\"end_time\");", + " pm.expect(first).to.have.property(\"id\");", + " pm.expect(first).to.have.property(\"schedule_id\");", + " pm.expect(first).to.have.property(\"staff_id\");", + " pm.expect(first).to.have.property(\"staff_name\");", + " pm.expect(first).to.have.property(\"start_time\");", + " }", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "protocolProfileBehavior": { "disableBodyPruning": true }, @@ -160,6 +461,27 @@ }, { "name": "Logout", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response message contains 'Logged Out!'\", function(){\r", + " const json = pm.response.json();\r", + " pm.expect(json.message).to.eql(\"Logged Out!\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "request": { "auth": { "type": "bearer", @@ -200,7 +522,15 @@ "exec": [ "// js code to save the access_token for user authentication\r", "let jsonData = JSON.parse(responseBody);\r", - "pm.environment.set(\"access_token\",jsonData.access_token) // creates access_token variable in postman and sets it to the value of the access_token in the response body." + "pm.environment.set(\"access_token\",jsonData.access_token) // creates access_token variable in postman and sets it to the value of the access_token in the response body.\r", + "\r", + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Access token has been set\", function(){\r", + " pm.expect(pm.environment.get(\"access_token\")).to.eql(jsonData.access_token);\r", + "});" ], "type": "text/javascript", "packages": {}, @@ -249,6 +579,43 @@ }, { "name": "Staff: Show_Roster", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Report contains valid roster details\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " if (Array.isArray(json) && json.length > 0) {\r", + " const first = json[0];\r", + "\r", + " pm.expect(first).to.be.an(\"object\");\r", + " pm.expect(first).to.have.property(\"clock_in\");\r", + " pm.expect(first).to.have.property(\"clock_out\");\r", + " pm.expect(first).to.have.property(\"end_time\");\r", + " pm.expect(first).to.have.property(\"id\");\r", + " pm.expect(first).to.have.property(\"schedule_id\");\r", + " pm.expect(first).to.have.property(\"staff_id\");\r", + " pm.expect(first).to.have.property(\"staff_name\");\r", + " pm.expect(first).to.have.property(\"start_time\");\r", + " }\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "request": { "auth": { "type": "bearer", @@ -277,6 +644,39 @@ }, { "name": "getShift", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Response contains shift object\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"clock_in\");\r", + " pm.expect(json).to.have.property(\"clock_out\");\r", + " pm.expect(json).to.have.property(\"end_time\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"schedule_id\");\r", + " pm.expect(json).to.have.property(\"staff_id\");\r", + " pm.expect(json).to.have.property(\"staff_name\");\r", + " pm.expect(json).to.have.property(\"start_time\");\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "protocolProfileBehavior": { "disableBodyPruning": true }, @@ -317,6 +717,44 @@ }, { "name": "Staff: Clock_In", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Response contains shift object\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"clock_in\");\r", + " pm.expect(json).to.have.property(\"clock_out\");\r", + " pm.expect(json).to.have.property(\"end_time\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"schedule_id\");\r", + " pm.expect(json).to.have.property(\"staff_id\");\r", + " pm.expect(json).to.have.property(\"staff_name\");\r", + " pm.expect(json).to.have.property(\"start_time\");\r", + "});\r", + "\r", + "pm.test(\"Clock in is not null\", function () {\r", + " const json = pm.response.json();\r", + " pm.expect(json.clock_in).to.not.be.null;\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "request": { "auth": { "type": "bearer", @@ -354,6 +792,44 @@ }, { "name": "Staff: Clock_Out", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + "});\r", + "\r", + "pm.test(\"Response contains shift object\", function () {\r", + " const json = pm.response.json();\r", + "\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"clock_in\");\r", + " pm.expect(json).to.have.property(\"clock_out\");\r", + " pm.expect(json).to.have.property(\"end_time\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"schedule_id\");\r", + " pm.expect(json).to.have.property(\"staff_id\");\r", + " pm.expect(json).to.have.property(\"staff_name\");\r", + " pm.expect(json).to.have.property(\"start_time\");\r", + "});\r", + "\r", + "pm.test(\"Clock out is not null\", function () {\r", + " const json = pm.response.json();\r", + " pm.expect(json.clock_out).to.not.be.null;\r", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "request": { "auth": { "type": "bearer", @@ -391,6 +867,27 @@ }, { "name": "Logout", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function(){\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test(\"Response message contains 'Logged Out!'\", function(){\r", + " const json = pm.response.json();\r", + " pm.expect(json.message).to.eql(\"Logged Out!\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "request": { "auth": { "type": "bearer", From 6578624f258c1540a3c35318aae1122207fbcd17 Mon Sep 17 00:00:00 2001 From: VisheshRampersad Date: Tue, 25 Nov 2025 21:37:36 +0000 Subject: [PATCH 5/7] Fixed admin.py and shift.py for testing --- App/controllers/admin.py | 23 +++++++++++++++++++---- App/models/shift.py | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/App/controllers/admin.py b/App/controllers/admin.py index aa2d9ca..5d2ff4b 100644 --- a/App/controllers/admin.py +++ b/App/controllers/admin.py @@ -1,7 +1,8 @@ -from App.models import Shift -from App.database import db -from datetime import datetime -from App.controllers.user import get_user +#from App.models import Shift +#from App.database import db +#from datetime import datetime +#from App.controllers.user import get_user +#all of the above were duplicated imports from App.models import Shift, Schedule from App.database import db @@ -24,6 +25,20 @@ def create_schedule(admin_id, scheduleName): #Not sure why this was missing return new_schedule +def create_unassigned_shift(start_time, end_time): + new_shift = Shift( + staff_id=None, + schedule_id=None, + start_time=start_time, + end_time=end_time + ) + + db.session.add(new_shift) + db.session.commit() + + return new_shift + + def schedule_shift(admin_id, staff_id, schedule_id, start_time, end_time): admin = get_user(admin_id) staff = get_user(staff_id) diff --git a/App/models/shift.py b/App/models/shift.py index 0467dee..1d93563 100644 --- a/App/models/shift.py +++ b/App/models/shift.py @@ -3,7 +3,7 @@ class Shift(db.Model): id = db.Column(db.Integer, primary_key=True) - staff_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + staff_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True) schedule_id = db.Column(db.Integer, db.ForeignKey("schedule.id"), nullable=True) start_time = db.Column(db.DateTime, nullable=False) end_time = db.Column(db.DateTime, nullable=False) @@ -16,7 +16,7 @@ def get_json(self): return { "id": self.id, "staff_id": self.staff_id, - "staff_name": self.staff.username if self.staff else None, + "staff_name": self.staff.username if self.staff else "Unassigned", "start_time": self.start_time.isoformat(), "schedule_id": self.schedule_id, "end_time": self.end_time.isoformat(), From ca705cd861ccfc44949d586caefe72a01c4c9a08 Mon Sep 17 00:00:00 2001 From: II-117 <143234129+II-117@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:48:02 -0400 Subject: [PATCH 6/7] Implement auto-schedule command in CLI Adds an auto-scheduling command to the CLI for managing shifts based on specified strategies. --- wsgi.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/wsgi.py b/wsgi.py index d3cedc3..430ad20 100644 --- a/wsgi.py +++ b/wsgi.py @@ -198,6 +198,65 @@ def view_schedule_command(schedule_id): print(f"✅ Viewing schedule {schedule_id}:") print(schedule.get_json()) +@schedule_cli.command("auto-schedule", help="Auto-schedule shifts using a strategy") +@click.argument("schedule_id", type=int) +@click.argument("strategy", type=str) +@click.argument("staff_ids", type=str) +def auto_schedule_command(schedule_id, strategy, staff_ids): + """Auto-schedule shifts for a schedule using a strategy (even, minimize, daynight)""" + from App.models import Schedule, Staff + from App.strategies.evendistribution import EvenDistributionStrategy + from App.strategies.minimizedays import MinimizeDaysStrategy + from App.strategies.balancedaynight import BalanceDayNightStrategy + from App.strategies.schedule_generator import ScheduleGenerator + + require_admin_login() + + raw_id_list = [x.strip() for x in staff_ids.split(',') if x.strip()] + + if not all(x.isdigit() for x in raw_id_list): + print("❌ Invalid staff IDs. Use format: 1,2,3 (numbers only)") + return + + staff_id_list = [int(x) for x in raw_id_list] + + schedule = db.session.get(Schedule, schedule_id) + if not schedule: + print(f"❌ Schedule {schedule_id} not found") + return + + staff_members = Staff.query.filter(Staff.id.in_(staff_id_list)).all() + if not staff_members: + print("❌ No staff members found") + return + + strategies = { + "even": EvenDistributionStrategy, + "minimize": MinimizeDaysStrategy, + "daynight": BalanceDayNightStrategy + } + + strategy_key = strategy.lower() + if strategy_key not in strategies: + print(f"❌ Unknown strategy: {strategy}") + print("Use: even, minimize, or daynight") + return + + chosen_strategy = strategies[strategy_key]() + + generator = ScheduleGenerator() + generator.setStrategy(chosen_strategy) + generator.setStaffList(staff_members) + new_schedule = generator.generateSchedule(week_start=schedule.weekStart) + + print(f"✅ Schedule created successfully!") + print(f"Strategy: {strategy}") + print(f"Staff count: {len(staff_members)}") + print(f"Shifts assigned: {len(new_schedule.shifts)}") + + + + app.cli.add_command(schedule_cli) ''' Test Commands From 5e05696fcc4db5712719a147cadff9640746bc1f Mon Sep 17 00:00:00 2001 From: Tyler-Baksh Date: Wed, 26 Nov 2025 02:04:09 -0400 Subject: [PATCH 7/7] Fix endpoints --- App/views/adminView.py | 33 +++++++ RosterAPI.postman_collection.json | 149 +++++++++++++++++++++++------- 2 files changed, 148 insertions(+), 34 deletions(-) diff --git a/App/views/adminView.py b/App/views/adminView.py index f823777..d450a78 100644 --- a/App/views/adminView.py +++ b/App/views/adminView.py @@ -4,6 +4,7 @@ from App.controllers import staff, auth, admin from App.controllers.user import get_user from App.controllers.scheduler import auto_generate_schedule +from App.controllers.admin import create_unassigned_shift from flask_jwt_extended import jwt_required, get_jwt_identity from sqlalchemy.exc import SQLAlchemyError @@ -67,6 +68,38 @@ def createShift(): return jsonify({"error": str(e)}), 403 except SQLAlchemyError: return jsonify({"error": "Database error"}), 500 + +@admin_view.route('/createUnassignedShift', methods=['POST']) +@jwt_required() +def createUnassignedShift(): + admin_id = get_jwt_identity() + admin = get_user(admin_id) + + if not admin: + return jsonify({"error": "User not found"}), 404 + if admin.role != "admin": + return jsonify({"error": "Only admins can create schedules"}), 403 + + try: + data = request.get_json() + startTime = data.get("start_time") # gets the startTime from the request body + endTime = data.get("end_time") # gets the endTime from the request body + + # Try ISO first, fallback to "YYYY-MM-DD HH:MM:SS" + try: + start_time = datetime.fromisoformat(startTime) + end_time = datetime.fromisoformat(endTime) + except ValueError: + start_time = datetime.strptime(startTime, "%Y-%m-%d %H:%M:%S") + end_time = datetime.strptime(endTime, "%Y-%m-%d %H:%M:%S") + + shift = create_unassigned_shift(start_time, end_time) # Call controller method + + return jsonify(shift.get_json()), 200 # Return the created shift as JSON + except (PermissionError, ValueError) as e: + return jsonify({"error": str(e)}), 403 + except SQLAlchemyError: + return jsonify({"error": "Database error"}), 500 @admin_view.route('/shiftReport', methods=['GET']) @jwt_required() diff --git a/RosterAPI.postman_collection.json b/RosterAPI.postman_collection.json index 85515ee..b95f199 100644 --- a/RosterAPI.postman_collection.json +++ b/RosterAPI.postman_collection.json @@ -194,7 +194,7 @@ "response": [] }, { - "name": "Admin: Auto-Generate Schedule (Even)", + "name": "Admin: Create Unassigned Shift", "event": [ { "listen": "test", @@ -208,13 +208,18 @@ " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", "});\r", "\r", - "pm.test(\"Response contains schedule object\", function () {\r", + "pm.test(\"Response contains shift object\", function () {\r", " const json = pm.response.json();\r", "\r", " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"clock_in\");\r", + " pm.expect(json).to.have.property(\"clock_out\");\r", + " pm.expect(json).to.have.property(\"end_time\");\r", " pm.expect(json).to.have.property(\"id\");\r", - " pm.expect(json).to.have.property(\"shifts\");\r", - " pm.expect(json).to.have.property(\"weekStart\");\r", + " pm.expect(json).to.have.property(\"schedule_id\");\r", + " pm.expect(json).to.have.property(\"staff_id\");\r", + " pm.expect(json).to.have.property(\"staff_name\");\r", + " pm.expect(json).to.have.property(\"start_time\");\r", "});" ], "type": "text/javascript", @@ -223,6 +228,74 @@ } } ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"start_time\": \"2025-11-26T09:00:00\",\r\n \"end_time\": \"2025-11-26T17:00:00\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/createUnassignedShift", + "host": [ + "{{host}}" + ], + "path": [ + "createUnassignedShift" + ] + } + }, + "response": [] + }, + { + "name": "Admin: Auto-Generate Schedule (Even)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.response.code === 200) {\r", + " pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + " });\r", + " \r", + " pm.test(\"Response contains schedule object\", function () {\r", + " const json = pm.response.json();\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"shifts\");\r", + " pm.expect(json).to.have.property(\"weekStart\");\r", + " });\r", + "}\r", + "\r", + "if (pm.response.code === 400) {\r", + " pm.test(\"Correct error message\", function(){\r", + " const json = pm.response.json();\r", + " pm.expect(json.error).to.eql(\"Invalid input: No unassigned shifts available for scheduling\");\r", + " });\r", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], "request": { "auth": { "type": "bearer", @@ -265,22 +338,26 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Status code is 200\", function(){\r", - " pm.response.to.have.status(200);\r", - "});\r", - "\r", - "pm.test(\"Response body is valid JSON\", function () {\r", - " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", - "});\r", - "\r", - "pm.test(\"Response contains schedule object\", function () {\r", - " const json = pm.response.json();\r", + "if (pm.response.code === 200) {\r", + " pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + " });\r", + " \r", + " pm.test(\"Response contains schedule object\", function () {\r", + " const json = pm.response.json();\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"shifts\");\r", + " pm.expect(json).to.have.property(\"weekStart\");\r", + " });\r", + "}\r", "\r", - " pm.expect(json).to.be.an(\"object\");\r", - " pm.expect(json).to.have.property(\"id\");\r", - " pm.expect(json).to.have.property(\"shifts\");\r", - " pm.expect(json).to.have.property(\"weekStart\");\r", - "});" + "if (pm.response.code === 400) {\r", + " pm.test(\"Correct error message\", function(){\r", + " const json = pm.response.json();\r", + " pm.expect(json.error).to.eql(\"Invalid input: No unassigned shifts available for scheduling\");\r", + " });\r", + "}" ], "type": "text/javascript", "packages": {}, @@ -330,22 +407,26 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Status code is 200\", function(){\r", - " pm.response.to.have.status(200);\r", - "});\r", - "\r", - "pm.test(\"Response body is valid JSON\", function () {\r", - " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", - "});\r", - "\r", - "pm.test(\"Response contains schedule object\", function () {\r", - " const json = pm.response.json();\r", + "if (pm.response.code === 200) {\r", + " pm.test(\"Response body is valid JSON\", function () {\r", + " pm.expect(() => JSON.parse(pm.response.text())).not.to.throw();\r", + " });\r", + " \r", + " pm.test(\"Response contains schedule object\", function () {\r", + " const json = pm.response.json();\r", + " pm.expect(json).to.be.an(\"object\");\r", + " pm.expect(json).to.have.property(\"id\");\r", + " pm.expect(json).to.have.property(\"shifts\");\r", + " pm.expect(json).to.have.property(\"weekStart\");\r", + " });\r", + "}\r", "\r", - " pm.expect(json).to.be.an(\"object\");\r", - " pm.expect(json).to.have.property(\"id\");\r", - " pm.expect(json).to.have.property(\"shifts\");\r", - " pm.expect(json).to.have.property(\"weekStart\");\r", - "});" + "if (pm.response.code === 400) {\r", + " pm.test(\"Correct error message\", function(){\r", + " const json = pm.response.json();\r", + " pm.expect(json.error).to.eql(\"Invalid input: No unassigned shifts available for scheduling\");\r", + " });\r", + "}" ], "type": "text/javascript", "packages": {},