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
1 change: 1 addition & 0 deletions App/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# App/__init__.py
from .models import *
from .views import *
from .controllers import *
Expand Down
1 change: 1 addition & 0 deletions App/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# App/config.py
import os

def load_config(app, overrides):
Expand Down
7 changes: 4 additions & 3 deletions App/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# App/controllers/_init_.py
from .user import *
from .auth import *
from .initialize import *
from .admin import *
from .staff import *
from .staff import *
from .auth import *
from .initialize import *
96 changes: 60 additions & 36 deletions App/controllers/admin.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,82 @@
from App.models import Shift
from App.database import db
from App.controllers.scheduling_logic import ScheduleStrategyFactory, AutoScheduler
from datetime import datetime
from App.controllers.user import get_user

from App.models import Shift, Schedule
from App.models import Admin, Staff, Shift
from App.database import db
from datetime import datetime
from App.controllers.user import get_user

def create_schedule(admin_id, scheduleName): #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()
)
def generate_auto_schedule(schedule_id: int, method_type: str):
"""
Executes the automatic scheduling logic using the Strategy pattern
by leveraging the StrategyFactory and AutoScheduler directly in the Controller.

db.session.add(new_schedule)
db.session.commit()
The logic now flows: Controller -> StrategyFactory (select strategy) -> AutoScheduler (execute strategy).

return new_schedule
Args:
schedule_id (str): The ID of the schedule to be processed.
method_type (str): The strategy to use (e.g., 'priority', 'random').

def schedule_shift(admin_id, staff_id, schedule_id, start_time, end_time):
admin = get_user(admin_id)
staff = get_user(staff_id)
Returns:
dict: The result of the scheduling operation.
"""
# collect staff and unassigned shift templates for the schedule
staff_list = Staff.query.all()
shift_templates = Shift.query.filter_by(schedule_id=schedule_id, staff_id=None).all()

schedule = db.session.get(Schedule, schedule_id)
try:
strategy = ScheduleStrategyFactory.create_strategy(method_type)
except ValueError as e:
return {"status": "error", "message": str(e)}

if not admin or admin.role != "admin":
scheduler = AutoScheduler(strategy, staff_list, [
{"start_time": s.start_time, "end_time": s.end_time} for s in shift_templates
], schedule_id)

try:
result = scheduler.generate_schedule()
return {"status": "success", "data": result}
except Exception as e:
return {"status": "error", "message": f"Auto-scheduling failed: {e}"}


def schedule_shift(admin_id: int, staff_id: int, schedule_id: int, start_time, end_time):
"""Create a shift under a schedule assigned to a staff member.

This is a thin controller wrapper that persists the Shift.
"""
# enforce admin permission
actor = get_user(admin_id)
if not actor or actor.role != "admin":
raise PermissionError("Only admins can schedule shifts")
if not staff or staff.role != "staff":
raise ValueError("Invalid staff member")
if not schedule:
raise ValueError("Invalid schedule ID")
if not staff_id or not schedule_id:
raise ValueError("staff_id and schedule_id are required")

# parse datetimes if strings
if isinstance(start_time, str):
start_time = datetime.fromisoformat(start_time)
if isinstance(end_time, str):
end_time = datetime.fromisoformat(end_time)

new_shift = Shift(
shift = Shift(
staff_id=staff_id,
schedule_id=schedule_id,
start_time=start_time,
end_time=end_time
end_time=end_time,
)

db.session.add(new_shift)
db.session.add(shift)
db.session.commit()
return shift.get_json()

return new_shift

def get_shift_report(admin_id: int):
"""Return all shifts (simple report) for an admin dashboard.

def get_shift_report(admin_id):
admin = get_user(admin_id)
if not admin or admin.role != "admin":
No strict admin checks are enforced here; controllers that call this
should ensure permissions.
"""
actor = get_user(admin_id)
if not actor or actor.role != "admin":
raise PermissionError("Only admins can view shift reports")

return [shift.get_json() for shift in Shift.query.order_by(Shift.start_time).all()]
shifts = Shift.query.order_by(Shift.start_time).all()
return [s.get_json() for s in shifts]
1 change: 1 addition & 0 deletions App/controllers/auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# app/controllers/auth.py
from flask_jwt_extended import (
create_access_token, jwt_required, JWTManager,
get_jwt_identity, verify_jwt_in_request
Expand Down
1 change: 1 addition & 0 deletions App/controllers/initialize.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#app/controllers/initialize.py
from .user import create_user
from App.database import db

Expand Down
176 changes: 176 additions & 0 deletions App/controllers/scheduling_logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# App/controllers/scheduling_logic.py
import abc
from datetime import datetime
from App.database import db
from App.models import Staff, Shift, Schedule # Required for type hinting and database interaction

# --- Utility Function ---
def calculate_duration_hours(start_time_str, end_time_str):
"""Calculates the duration in hours between two time strings."""
try:
# Assuming ISO format like 'YYYY-MM-DD HH:MM:SS'
# Note: You should enforce a strict date format in your API/route layer
start = datetime.strptime(start_time_str, '%Y-%m-%d %H:%M:%S')
end = datetime.strptime(end_time_str, '%Y-%m-%d %H:%M:%S')
duration = end - start
return duration.total_seconds() / 3600
except Exception:
return 0.0

# --- 1. ScheduleStrategy Abstract Interface/Class ---
class ScheduleStrategy(abc.ABC):
"""
Abstract Base Class for all scheduling algorithms (Strategies).
"""
@abc.abstractmethod
def generate(self, staff_list: list[Staff], schedule_templates: list[dict], schedule_id: int) -> list[Shift]:
"""
Generates a list of new Shift objects based on the strategy.
"""
pass

# --- 4. EvenDistribution Concrete Strategy ---
class EvenDistribution(ScheduleStrategy):
"""
Assigns shifts in a simple round-robin fashion.
"""
def generate(self, staff_list: list[Staff], schedule_templates: list[dict], schedule_id: int) -> list[Shift]:
new_shifts = []
if not staff_list or not schedule_templates:
return new_shifts

num_staff = len(staff_list)

# Round-robin assignment
for i, template in enumerate(schedule_templates):
staff_index = i % num_staff
staff_id = staff_list[staff_index].id

new_shifts.append(Shift(
staff_id=staff_id,
schedule_id=schedule_id,
start_time=template['start_time'],
end_time=template['end_time']
))

return new_shifts

# --- 5. MinimalDays Concrete Strategy ---
class MinimalDays(ScheduleStrategy):
"""
Minimizes the total count of shifts assigned to each staff member.
"""
def generate(self, staff_list: list[Staff], schedule_templates: list[dict], schedule_id: int) -> list[Shift]:
new_shifts = []
if not staff_list or not schedule_templates:
return new_shifts

# Initialize shift count tracker {staff_id: count}
staff_shift_counts = {staff.id: 0 for staff in staff_list}

for template in schedule_templates:
# Find the staff member with the minimum number of assigned shifts
staff_id_to_assign = min(staff_shift_counts, key=staff_shift_counts.get)

new_shifts.append(Shift(
staff_id=staff_id_to_assign,
schedule_id=schedule_id,
start_time=template['start_time'],
end_time=template['end_time']
))

# Update the count for the assigned staff member
staff_shift_counts[staff_id_to_assign] += 1

return new_shifts

# --- 6. BalancedShift Concrete Strategy ---
class BalancedShift(ScheduleStrategy):
"""
Balances the total duration (hours) assigned to each staff member.
"""
def generate(self, staff_list: list[Staff], schedule_templates: list[dict], schedule_id: int) -> list[Shift]:
new_shifts = []
if not staff_list or not schedule_templates:
return new_shifts

# Initialize total hours tracker {staff_id: total_hours}
staff_hour_totals = {staff.id: 0.0 for staff in staff_list}

for template in schedule_templates:
# Calculate the duration of the current template shift
duration = calculate_duration_hours(template['start_time'], template['end_time'])

# Find the staff member with the minimum total hours
staff_id_to_assign = min(staff_hour_totals, key=staff_hour_totals.get)

new_shifts.append(Shift(
staff_id=staff_id_to_assign,
schedule_id=schedule_id,
start_time=template['start_time'],
end_time=template['end_time']
))

# Update the total hours for the assigned staff member
staff_hour_totals[staff_id_to_assign] += duration

return new_shifts

# --- 3. ScheduleStrategyFactory ---
class ScheduleStrategyFactory:
""" Factory method to create the appropriate scheduling strategy. """
STRATEGY_MAP = {
'even': EvenDistribution,
'minimal': MinimalDays,
'balanced': BalancedShift
}

@staticmethod
def create_strategy(method_type: str) -> ScheduleStrategy:
""" Instantiates and returns a concrete ScheduleStrategy object. """
method_type = method_type.lower().strip()
strategy_class = ScheduleStrategyFactory.STRATEGY_MAP.get(method_type)

if not strategy_class:
raise ValueError(f"Invalid scheduling method type: {method_type}. Must be one of {list(ScheduleStrategyFactory.STRATEGY_MAP.keys())}")

return strategy_class()


# --- 2. AutoScheduler (Context) Class ---
class AutoScheduler:
"""
The Context class in the Strategy Pattern. Executes the schedule generation.
"""
def _init_(self, strategy: ScheduleStrategy, staff_list: list[Staff], schedule_templates: list[dict], schedule_id: int):
self._strategy = strategy
self._staff_list = staff_list
self._schedule_templates = schedule_templates
self._schedule_id = schedule_id

def generate_schedule(self) -> list[dict]:
"""
Delegates the schedule generation to the configured strategy and persists it.
"""
# Step 1: Generate the new shifts using the chosen strategy
new_shifts = self._strategy.generate(
self._staff_list,
self._schedule_templates,
self._schedule_id
)

# Step 2: Save the generated shifts to the database (Persistence)
self.save_schedule(new_shifts)

# Step 3: Return the JSON representation of the new shifts
return [s.get_json() for s in new_shifts]

def save_schedule(self, shifts_to_save: list[Shift]):
"""
Persists the list of generated Shift objects to the database.
"""
if shifts_to_save:
# Before adding new shifts, optionally clear existing shifts for this schedule
# Shift.query.filter_by(schedule_id=self._schedule_id).delete()
db.session.add_all(shifts_to_save)
db.session.commit()
1 change: 1 addition & 0 deletions App/controllers/staff.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# App/controllers/staff.py
from App.models import Shift
from App.database import db
from datetime import datetime
Expand Down
6 changes: 3 additions & 3 deletions App/controllers/user.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# App/controllers/user.py
from App.models import User, Admin, Staff, Shift
from App.database import db
from datetime import datetime
Expand All @@ -7,8 +8,7 @@
def create_user(username, password, role):
role = role.lower().strip()
if role not in VALID_ROLES:
print(f"⚠️ Invalid role '{role}'. Must be one of {VALID_ROLES}")
return None
raise ValueError(f"Invalid role '{role}'. Must be one of {VALID_ROLES}")
if role == "admin":
newuser = Admin(username=username, password=password)
elif role == "staff":
Expand Down Expand Up @@ -41,4 +41,4 @@ def update_user(id, username):
user.username = username
db.session.commit()
return user
return None
return None
1 change: 1 addition & 0 deletions App/database.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# database.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

Expand Down
1 change: 1 addition & 0 deletions App/default_config.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# App/default_config.py
SQLALCHEMY_DATABASE_URI="sqlite:///temp-database.db"
SECRET_KEY="secret key"
1 change: 1 addition & 0 deletions App/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# App/main.py
import os
from flask import Flask, render_template
from flask_uploads import DOCUMENTS, IMAGES, TEXT, UploadSet, configure_uploads
Expand Down
10 changes: 8 additions & 2 deletions App/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
from App.models.admin import Admin
from App.models.staff import Staff
from App.models.schedule import Schedule
from App.models.shift import Shift

from App.models.shift import Shift
from App.models.auto_scheduler import AutoScheduler
from App.models.strategy import (
ScheduleStrategy,
EvenDistributionStrategy,
MinimalDaysStrategy,
BalancedShiftStrategy
)
Loading