Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| wiki:facility:software:scheduling_documentation [2025/04/16 11:48] – created Max Breitmeyer | wiki:facility:software:scheduling_documentation [2025/05/04 16:22] (current) – Shanmukha Doddipatla | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | START HERE | + | **Overview** |
| + | |||
| + | The Observatory Shift Scheduler is a Google Apps Script that automatically assigns staff to different shift types based on: | ||
| + | Staff availability submissions | ||
| + | Seasonal shift timeframes | ||
| + | Fair distribution of shifts among operators | ||
| + | Specific scheduling constraints | ||
| + | The script reads data from multiple sheets, performs the scheduling algorithm, and outputs the results to a " | ||
| + | |||
| + | **How It Works** | ||
| + | |||
| + | Data Sources | ||
| + | The script uses three key data sources: | ||
| + | Shift Timeframes sheet: Contains seasonal information about shift start times | ||
| + | Start Date: Date when this timeframe becomes active | ||
| + | Sunset Time: Time of sunset for this period | ||
| + | Sunrise Time: Time of sunrise for this period | ||
| + | Night Duration: Duration of night in hours | ||
| + | ES 1 Start Time: The start time for Evening Shift 1 in this period | ||
| + | Availability sheet: Contains staff availability submissions for different shifts | ||
| + | First column: Staff names | ||
| + | Second column: Staff email addresses | ||
| + | Subsequent columns: Boolean values (TRUE/ | ||
| + | Column headers format: "Day ShiftType" | ||
| + | Current date + 7 days: Used to determine which seasonal timeframe to use | ||
| + | The script adds 7 days to the current date to calculate shifts for the upcoming week | ||
| + | This allows scheduling to be done one week in advance | ||
| + | |||
| + | **Workflow** | ||
| + | |||
| + | The script determines the correct shift timeframe based on the current date + 7 days | ||
| + | It looks up the most recent applicable row in the Shift Timeframes sheet | ||
| + | The timeframe determines the ES1 start time which affects other shifts | ||
| + | It identifies which shift types are available (ES 1, ES 2, GS 1, GS 2, and sometimes MS 1 and MS 2) | ||
| + | ES and GS shifts are always included | ||
| + | MS shifts are only included when ES1 starts between 16:00 and 17:30 | ||
| + | This logic is based on observatory-specific requirements | ||
| + | It retrieves staff availability from the " | ||
| + | Maps column headers to full shift names | ||
| + | Creates a data structure tracking which employees are available for each shift | ||
| + | Excludes shifts that aren't applicable for the current timeframe | ||
| + | It assigns shifts based on availability, | ||
| + | First tries to give everyone at least one shift | ||
| + | Then fills remaining shifts prioritizing those with fewer assignments | ||
| + | Ensures employees who work GS shifts get the next day off | ||
| + | Tries to distribute different shift types among employees | ||
| + | Uses randomization to ensure fairness among equally-qualified staff | ||
| + | Limits employees to a maximum of 3 shifts each | ||
| + | It outputs the schedule to the " | ||
| + | Organized by day of the week | ||
| + | Shows which employee is assigned to each shift | ||
| + | Includes a summary of filled vs. unfilled shifts | ||
| + | Lists any shifts that couldn' | ||
| + | If not in TEST_MODE, it sends email notifications to staff | ||
| + | Emails contain personalized schedules for each employee | ||
| + | The email format is clean and professional | ||
| + | |||
| + | |||
| + | **Script Components** | ||
| + | |||
| + | Key Functions | ||
| + | |||
| + | // | ||
| + | |||
| + | This function determines the shift timeframe to use based on date. | ||
| + | Reads the "Shift Timeframes" | ||
| + | Uses current date + 7 days to find the appropriate timeframe | ||
| + | Iterates through rows in the Shift Timeframes sheet to find the latest applicable row | ||
| + | Returns start time for ES1 and other timeframe info (sunset, sunrise, etc.) | ||
| + | Returns null if no valid timeframe is found | ||
| + | // | ||
| + | |||
| + | This function determines which shift types to include based on the ES1 start time. | ||
| + | Calls getShiftDuration() to get the ES1 start time | ||
| + | Always includes ES 1, ES 2, GS 1, and GS 2 shifts | ||
| + | Calculates total minutes from ES1 start time (converts HH:MM to minutes) | ||
| + | Includes MS 1 and MS 2 shifts only if ES1 starts between 16:00 (960 minutes) and 17:30 (1050 minutes) | ||
| + | Returns a mapping of shift types to their start times | ||
| + | // | ||
| + | |||
| + | This is the main entry point function that: | ||
| + | Clears/ | ||
| + | Calls scheduleAllShifts() to get assignments | ||
| + | Groups shift assignments by day for readability | ||
| + | Formats the results for display with a specific output format | ||
| + | Creates a summary of filled/ | ||
| + | Handles email notifications if not in TEST_MODE | ||
| + | Autoformats the output sheet for readability | ||
| + | // | ||
| + | |||
| + | This is the core scheduling algorithm that: | ||
| + | Reads the " | ||
| + | Gets shift timings from getShiftTimings() | ||
| + | Creates a mapping between availability column headers and full shift names | ||
| + | Initializes tracking variables for shift assignments and employee shift counts | ||
| + | Gathers employee availability for each possible shift | ||
| + | Prioritizes employees with fewer shifts for fair distribution | ||
| + | Applies logic to ensure no one works more than the maximum allowed shifts | ||
| + | Implements special rules for different shift types (like rest days after GS shifts) | ||
| + | Returns a detailed object with assignment results and statistics | ||
| + | sendEmailNotifications(employeeShifts) | ||
| + | This function sends email notifications to staff about their assigned shifts. | ||
| + | Checks if TEST_MODE is active (skips emails if true) | ||
| + | Retrieves email addresses using getEmployeeEmail() | ||
| + | Formats a personalized email for each employee with their shift assignments | ||
| + | Uses MailApp.sendEmail to send the notifications | ||
| + | Logs confirmation of sent emails | ||
| + | // | ||
| + | |||
| + | Helper function that randomly shuffles arrays using the Fisher-Yates algorithm for fair distribution. | ||
| + | Takes an array as input | ||
| + | Randomly reorders the elements in place | ||
| + | Returns the shuffled array | ||
| + | |||
| + | // | ||
| + | |||
| + | Helper function that retrieves email addresses from the availability data. | ||
| + | Searches for the employee name in the first column of data values | ||
| + | Returns the corresponding email from the second column | ||
| + | Returns null if no match is found | ||
| + | **Configuration Options** | ||
| + | |||
| + | //Global Settings// | ||
| + | |||
| + | TEST_MODE (boolean): When true, email notifications are disabled | ||
| + | Future date offset (7 days): Added to the current date for timeframe selection | ||
| + | Shift Assignment Rules | ||
| + | Maximum shifts per employee: 3 (can be adjusted in the scheduleAllShifts() function) | ||
| + | Rest day rule: Employees who work a GS shift can't work the next day | ||
| + | Shift variety: The algorithm tries to ensure employees get different shift types | ||
| + | **Scheduling Logic** | ||
| + | |||
| + | The scheduling algorithm is implemented through multiple phases and constraints to ensure fair and effective shift distribution. | ||
| + | Prioritization Phases | ||
| + | The algorithm uses two prioritization phases defined in the assignmentPhases array: | ||
| + | Phase 1: Give everyone at least one shift | ||
| + | function(availableEmployees, | ||
| + | return availableEmployees[shiftKey].filter(emp => | ||
| + | employeesWithNoShift.indexOf(emp) >= 0 && | ||
| + | !employeeDayAssignments[emp][shiftKey.split(" | ||
| + | ); | ||
| + | } | ||
| + | Filters employees who have zero shifts assigned | ||
| + | Checks that the employee isn't already assigned to another shift on the same day | ||
| + | This ensures every operator gets at least one shift before anyone gets multiple | ||
| + | Phase 2: Fair distribution | ||
| + | function(availableEmployees, | ||
| + | var shiftDay = shiftKey.split(" | ||
| + | var currentDayIndex = fullDayNames.indexOf(shiftDay); | ||
| + | var isGSShift = shiftKey.includes(" | ||
| + | |||
| + | return availableEmployees[shiftKey].filter(emp => { | ||
| + | // Various filtering criteria... | ||
| + | }); | ||
| + | } | ||
| + | Applies more complex filtering based on several criteria | ||
| + | Prioritizes employees with fewer shifts | ||
| + | Enforces rest days after GS shifts | ||
| + | Promotes shift type variety | ||
| + | Prevents exceeding maximum shifts per employee | ||
| + | Specific Assignment Criteria | ||
| + | |||
| + | The algorithm considers these factors when assigning shifts: | ||
| + | Availability Check | ||
| + | |||
| + | Employee must have indicated availability for the specific shift | ||
| + | Only employees with TRUE in the corresponding availability column are considered | ||
| + | Maximum Shift Limit | ||
| + | if (employeeShiftCount[emp] >= maxShiftsPerEmployee) return false; | ||
| + | No employee gets more than 3 shifts (configurable via maxShiftsPerEmployee) | ||
| + | Once an employee reaches this limit, they' | ||
| + | Day Assignment Tracking | ||
| + | |||
| + | if (employeeDayAssignments[emp][shiftDay]) return false; | ||
| + | Prevents multiple shifts on the same day for an employee | ||
| + | Uses the employeeDayAssignments object to track which days each employee is assigned | ||
| + | GS Shift Rest Day Rule | ||
| + | |||
| + | // Check if employee worked a GS shift yesterday | ||
| + | var prevDayIndex = (currentDayIndex - 1 + 7) % 7; | ||
| + | var prevDay = fullDayNames[prevDayIndex]; | ||
| + | |||
| + | |||
| + | for (var i = 0; i < employeeShifts[emp].length; | ||
| + | var pastShift = employeeShifts[emp][i]; | ||
| + | if (pastShift.includes(prevDay) && (pastShift.includes(" | ||
| + | return false; // Needs rest day after GS shift | ||
| + | } | ||
| + | } | ||
| + | If an employee works a GS shift, they cannot work the next day | ||
| + | This is enforced by checking previous day assignments for GS shifts | ||
| + | Shift Type Variety Promotion | ||
| + | // For GS shifts, check if they already have one | ||
| + | if (isGSShift) { | ||
| + | var hasGSShift = employeeShifts[emp].some(shift => | ||
| + | shift.includes(" | ||
| + | ); | ||
| + | |||
| + | if (hasGSShift && employeeShiftCount[emp] > 0) { | ||
| + | return false; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | |||
| + | // For non-GS shifts, prioritize those without ES/MS shifts | ||
| + | if (!isGSShift && employeeShiftCount[emp] > 0) { | ||
| + | var hasESorMSShift = employeeShifts[emp].some(shift => | ||
| + | shift.includes(" | ||
| + | shift.includes(" | ||
| + | ); | ||
| + | |||
| + | if (!hasESorMSShift) { | ||
| + | return true; // Prioritize | ||
| + | } | ||
| + | } | ||
| + | Promotes shift type variety by avoiding assigning the same type repeatedly | ||
| + | For GS shifts: deprioritizes employees who already have a GS shift | ||
| + | For ES/MS shifts: prioritizes employees who don't yet have an ES or MS shift | ||
| + | Fairness Through Randomization | ||
| + | |||
| + | // Shuffle employees with the same shift count | ||
| + | if (i > startIndex) { | ||
| + | var group = eligibleEmployees.slice(startIndex, | ||
| + | group = shuffleArray(group); | ||
| + | for (var j = 0; j < group.length; | ||
| + | eligibleEmployees[startIndex + j] = group[j]; | ||
| + | } | ||
| + | } | ||
| + | After prioritizing by shift count and type, adds randomness within priority groups | ||
| + | Uses the shuffleArray() function to randomly reorder employees with the same priority | ||
| + | This prevents the same employees from always getting preferred shifts | ||
| + | The scheduling logic combines these rules to create a fair, balanced schedule that respects constraints while maximizing shift coverage. | ||
| + | Output Format | ||
| + | |||
| + | The script outputs the schedule to the " | ||
| + | Monday Evening --> Tuesday Morning | ||
| + | Shift: Prep + ES 1, Assigned to: Employee1 | ||
| + | Shift: ES 2, Assigned to: Employee2 | ||
| + | Shift: GS 1, Assigned to: Employee3 | ||
| + | Shift: GS 2, Assigned to: Employee4 | ||
| + | |||
| + | Tuesday Evening --> Wednesday Morning | ||
| + | ... | ||
| + | |||
| + | Shifts Summary: 20/28 filled | ||
| + | |||
| + | Unassigned Shifts: | ||
| + | Monday MS 1 | ||
| + | Monday MS 2 | ||
| + | ... | ||
| + | **Troubleshooting** | ||
| + | |||
| + | Common Issues | ||
| + | |||
| + | No Results from Scheduling | ||
| + | |||
| + | Cause: Could be missing data in the Shift Timeframes or Availability sheets | ||
| + | Solution: | ||
| + | |||
| + | Check that both sheets exist with exactly these names: "Shift Timeframes" | ||
| + | Verify the Shift Timeframes sheet has data in the expected format | ||
| + | Check if any dates in the Shift Timeframes sheet are applicable to current date + 7 days | ||
| + | Look for error messages in the script execution logs | ||
| + | Missing Shift Types (MS Shifts) | ||
| + | Cause: MS shifts only appear when ES1 start time is between 16: | ||
| + | |||
| + | Solution: | ||
| + | |||
| + | This is expected behavior based on the condition: if (totalMinutes >= 960 && totalMinutes <= 1050) | ||
| + | If MS shifts should be included regardless of time, modify the getShiftTimings() function | ||
| + | To debug, add a log statement: `Logger.log(" | ||
| + | |||
| + | |||
| + | Making Updates | ||
| + | |||
| + | This section provides detailed guidance on common modifications your team might need to make. | ||
| + | Modifying the Date Offset | ||
| + | Currently, the script adds 7 days to the current date to determine the shift timeframe. To change this: | ||
| + | // In getShiftDuration() function | ||
| + | |||
| + | var today = new Date(); | ||
| + | today.setDate(today.getDate() + 7); // Change 7 to your desired days(based on when we schedule) | ||
| + | If you want to make this configurable at the top of the script, add: | ||
| + | var DAYS_TO_ADD = 7; // Configure how many days to look ahead | ||
| + | // Then in getShiftDuration() | ||
| + | |||
| + | var today = new Date(); | ||
| + | today.setDate(today.getDate() + DAYS_TO_ADD); | ||
| + | Changing Maximum Shifts Per Employee | ||
| + | To change the maximum number of shifts per employee: | ||
| + | // In scheduleAllShifts() function | ||
| + | |||
| + | var maxShiftsPerEmployee = 3; // Change to desired number | ||
| + | Consider adding this as a global constant at the top of the script for easier configuration: | ||
| + | var MAX_SHIFTS_PER_EMPLOYEE = 3; // Configure maximum shifts | ||
| + | |||
| + | // Then update the reference in scheduleAllShifts() | ||
| + | var maxShiftsPerEmployee = MAX_SHIFTS_PER_EMPLOYEE; | ||
| + | |||
| + | |||
| + | Modifying Shift Types | ||
| + | |||
| + | If you need to add or remove shift types, update the checks in the getShiftTimings() function. | ||
| + | For example, to always include MS shifts regardless of ES1 start time: | ||
| + | // Always include these shift types | ||
| + | shiftTimings[" | ||
| + | shiftTimings[" | ||
| + | shiftTimings[" | ||
| + | shiftTimings[" | ||
| + | shiftTimings[" | ||
| + | shiftTimings[" | ||
| + | |||
| + | // Remove or comment out the conditional MS shift logic | ||
| + | // if (totalMinutes >= 960 && totalMinutes <= 1050) { ... } | ||
| + | To add a new shift type: | ||
| + | // Add new shift type | ||
| + | shiftTimings[" | ||
| + | |||
| + | // Don't forget to update the shifts array in scheduleShifts() | ||
| + | var shifts = ["Prep + ES 1", "ES 2", "GS 1", "GS 2", "MS 1", "MS 2", " | ||
| + | Changing MS Shift Inclusion Criteria | ||
| + | To modify when MS shifts are included: | ||
| + | // Change the time range for MS shifts | ||
| + | // Current: 16:00 (960 min) to 17:30 (1050 min) | ||
| + | if (totalMinutes >= 900 && totalMinutes <= 1080) { // Changed to 15: | ||
| + | Logger.log(" | ||
| + | shiftTimings[" | ||
| + | shiftTimings[" | ||
| + | } else { | ||
| + | Logger.log(" | ||
| + | } | ||
| + | Changing Email Notification Format | ||
| + | |||
| + | To modify the email content, update the sendEmailNotifications() function: | ||
| + | var subject = "Your Observatory Shift Assignment"; | ||
| + | |||
| + | // Change email body format | ||
| + | |||
| + | var body = "Hello " + employee + ", | ||
| + | " | ||
| + | | ||
| + | " | ||
| + | " | ||
| + | Adding Different Constraints | ||
| + | |||
| + | To add new scheduling constraints, | ||
| + | Example - Adding a constraint to prevent working more than 2 consecutive days: | ||
| + | // Check if employee is already working the previous two days | ||
| + | var prevDay1Index = (currentDayIndex - 1 + 7) % 7; | ||
| + | var prevDay2Index = (currentDayIndex - 2 + 7) % 7; | ||
| + | var prevDay1 = fullDayNames[prevDay1Index]; | ||
| + | var prevDay2 = fullDayNames[prevDay2Index]; | ||
| + | |||
| + | var workingPrevDay1 = employeeDayAssignments[emp][prevDay1]; | ||
| + | var workingPrevDay2 = employeeDayAssignments[emp][prevDay2]; | ||
| + | |||
| + | // If working both previous days, don't assign this day | ||
| + | if (workingPrevDay1 && workingPrevDay2) return false; | ||
| + | Manually Setting a Specific Date | ||
| + | |||
| + | If you need to run the script for a specific date instead of current date + 7 days, you can replace: | ||
| + | var today = new Date(); | ||
| + | today.setDate(today.getDate() + 7); | ||
| + | With: | ||
| + | var today = new Date(" | ||
| + | Implementing a Date Selection Dialog | ||
| + | For more flexibility, | ||
| + | |||
| + | function showDatePickerDialog() { | ||
| + | var ui = SpreadsheetApp.getUi(); | ||
| + | var response = ui.prompt( | ||
| + | ' | ||
| + | 'Enter the date to schedule for (YYYY-MM-DD):', | ||
| + | ui.ButtonSet.OK_CANCEL | ||
| + | ); | ||
| + | |||
| + | if (response.getSelectedButton() == ui.Button.OK) { | ||
| + | var dateString = response.getResponseText(); | ||
| + | // Validate date format | ||
| + | var dateRegex = / | ||
| + | if (dateRegex.test(dateString)) { | ||
| + | // Override the current date | ||
| + | var schedulingDate = new Date(dateString); | ||
| + | // Call scheduling function with this date | ||
| + | scheduleShiftsForDate(schedulingDate); | ||
| + | } else { | ||
| + | ui.alert(' | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // Modified scheduling function that accepts a date parameter | ||
| + | function scheduleShiftsForDate(targetDate) { | ||
| + | // Store the original date calculation | ||
| + | var originalGetShiftDuration = getShiftDuration; | ||
| + | |||
| + | // Override getShiftDuration temporarily | ||
| + | getShiftDuration = function() { | ||
| + | var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(" | ||
| + | if (!sheet) { | ||
| + | Logger.log(" | ||
| + | return null; | ||
| + | } | ||
| + | |||
| + | var data = sheet.getDataRange().getValues(); | ||
| + | var today = targetDate; // Use the provided date | ||
| + | // Rest of function remains the same... | ||
| + | }; | ||
| + | |||
| + | // Call the regular scheduling function | ||
| + | scheduleShifts(); | ||
| + | |||
| + | // Restore the original function | ||
| + | getShiftDuration = originalGetShiftDuration; | ||
| + | } | ||
| + | Adding Custom Menu Items | ||
| + | You can add menu items to make the script more user-friendly: | ||
| + | function onOpen() { | ||
| + | var ui = SpreadsheetApp.getUi(); | ||
| + | ui.createMenu(' | ||
| + | .addItem(' | ||
| + | .addItem(' | ||
| + | .addSeparator() | ||
| + | .addItem(' | ||
| + | .addToUi(); | ||
| + | } | ||
| + | |||
| + | function toggleEmailNotifications() { | ||
| + | TEST_MODE = !TEST_MODE; | ||
| + | var ui = SpreadsheetApp.getUi(); | ||
| + | ui.alert(' | ||
| + | | ||
| + | } | ||
| + | |||
| + | |||