Scenario One: Basic Rostering
In this section, we will start expanding upon the existing example, which was a roster for a week, that has 2 shifts, and 6 employees. The adjustments will make sure there are limits on weekly rest/weekly hours and rest between shifts.
Before we do any of that, the roster will be expanded for a full month of work (not shown, but otherwise most constraints will not make sense).
Business and legislation requirements
In our scenario, we want to model the following requirements are a must for the roster:
- Weekly Maximum: No employee should work more than 40 hours per week.
- Between Shifts 1: Employees must have 24 hours of rest between shifts.
- Between Shifts 2: When shift1 has been worked, a cooldown period of 48 hours must be applied before shift1 can be worked again.
- Periodic Rest: Employees must have at least one full 48-hour consecutive rest period per week.
To begin with handling the weekly maximum, we have to change the schedule to even make it possible to have more than 40 hours per week, so we will put demand for shift 1 and shift 2 on all days of the week.
To handle the requirement of weekly rest, we can specify a periodic rest constraint that accomplishes this:
"periodicRestConstraints": [
{
"id": "PeriodicRestConstraint1",
"minimumContinuousRestDuration": {
"hours": 48
},
"periods": {
"recurrentDefinition": {
"daysPerPeriod": 7
}
}
}
],
Unless otherwise specified, a constraint will automatically apply to all employees.
We can see that the schedule is now having all employees scheduled, and now complies with the requirement of at most 40 hours per week and includes a 48-hour continuous rest period.(the gaps in there are always 48 hours).
Defining Rest Rule Interdependencies Between Shifts
We can define the following rest periods between shifts:
"restBetweenShiftsConstraints": [
{
"id": "RestBetweenShiftsConstraint1",
"weight": 100,
"minRestDuration": {
"hours": 24
}
},
{
"id": "RestBetweenShiftsConstraint2",
"weight": 100,
"minRestDuration": {
"hours": 48
},
"filters": {
"shiftIds": [
"shift1"
]
}
}
]
This configuration ensures that after working shift1, an employee cannot work shift1 again for 48 hours, and both shift1 and shift2 require a 24-hour break after being worked.
Watching the results, we're not completely satisfied with our roster, so to fix it, we will encourage more frequent scheduling of shift 2, as it does not have the same cooldown restrictions as shift 1.
Adjusting the Schedule to Encourage More Shift 2 Scheduling
What we can do is adjust the weight of the restBetweenShiftsConstraint, so it isn't a hard requirement with the rest for shift2, and we can encouraget shift2 by using the shift utilization constraint.
To encourage more frequent scheduling of shift 2, we first adjust the weight of the restBetweenShiftsConstraint to 50, which will make it less important to adhere to.
"restBetweenShiftsConstraints": [
{
"id": "RestBetweenShiftsConstraint1",
"weight": 50,
"minRestDuration": {
"hours": 24
},
"filters": {
"shiftIds": [
"shift2"
]
}
},
{
"id": "RestBetweenShiftsConstraint2",
"weight": 100,
"minRestDuration": {
"hours": 48
},
"filters": {
"shiftIds": [
"shift1"
]
}
}
]
Continuing with the employee utilization constraint, we can add the following configuration:
"employeeUtilizationConstraints": [
{
"id": "MIN_FAIR_SHIFT_1",
"weight": 10,
"fairness": {
"fairnessWeight": 90
},
"targets": {
"maxShifts": 0
},
"shiftIds": ["shift1"]
},
{
"id": "MAX_FAIR_SHIFT_2",
"weight": 50,
"fairness": {
"fairnessWeight": 90
},
"targets": {
"minShifts": 100
},
"filters": {
"shiftIds": ["shift2"]
}
}
]
Check out the results below - we see that shift2 demand is covered on all days, while also ensuring 48 hours of rest for all employees - excellent!
The final payload that was built during this part of the tutorial is listed below:
{
"jobInfo": {
"id": "PeriodId",
"organisationId": "ParentId",
"scheduleType": "RECURRING",
"demandType": "TIME_DEMAND",
"planningHorizon": {
"startDate": "2021-11-29",
"nrOfWeeks": 4,
"fteStartDay": {
"dayIndex": 0
},
"fteEndDay": {
"dayIndex": 27
}
}
},
"shifts": [
{
"id": "shift1",
"intervals": [
{
"startTime": "07:00",
"endTime": "16:00",
"dayIndicator": 0,
"workTypeId": "1",
"breakMinutes": 60
}
]
},
{
"id": "shift2",
"intervals": [
{
"startTime": "08:00",
"endTime": "17:00",
"dayIndicator": 0,
"workTypeId": "1",
"breakMinutes": 60
}
]
}
],
"employees": [
{
"id": "1",
"workTime": {
"weekHoursRules": {
"maxWeekHours": 40
}
}
},
{
"id": "2",
"workTime": {
"weekHoursRules": {
"maxWeekHours": 40
}
}
},
{
"id": "3",
"workTime": {
"weekHoursRules": {
"maxWeekHours": 40
}
}
},
{
"id": "4",
"workTime": {
"weekHoursRules": {
"maxWeekHours": 40
}
}
},
{
"id": "5",
"workTime": {
"weekHoursRules": {
"maxWeekHours": 40
}
}
},
{
"id": "6",
"workTime": {
"weekHoursRules": {
"maxWeekHours": 40
}
}
}
],
"timeSlots": [
{
"id": "slot-shift1",
"startTime": "07:00",
"endTime": "16:00",
"workTypeId": "1"
},
{
"id": "slot-shift2",
"startTime": "08:00",
"endTime": "17:00",
"workTypeId": "1"
}
],
"demands": [
{
"days": {
"dayIndexes": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27
]
},
"timeDemands": [
{
"timeSlotId": "slot-shift1",
"ideal": 4
},
{
"timeSlotId": "slot-shift2",
"ideal": 2
}
]
}
],
"constraints": {
"periodicRestConstraints": [
{
"id": "PeriodicRestConstraint1",
"minimumContinuousRestDuration": {
"hours": 48
},
"periods": {
"recurrentDefinition": {
"daysPerPeriod": 7
}
}
}
],
"employeeUtilizationConstraints": [
{
"id": "MIN_FAIR_SHIFT_1",
"weight": 10,
"fairness": {
"fairnessWeight": 90
},
"targets": {
"maxShifts": 0
},
"shiftIds": ["shift1"]
},
{
"id": "MAX_FAIR_SHIFT_2",
"weight": 50,
"fairness": {
"fairnessWeight": 90
},
"targets": {
"minShifts": 100
},
"filters": {
"shiftIds": ["shift2"]
}
}
],
"restBetweenShiftsConstraints": [
{
"id": "RestBetweenShiftsConstraint1",
"weight": 50,
"minRestDuration": {
"hours": 24
}
},
{
"id": "RestBetweenShiftsConstraint2",
"weight": 100,
"minRestDuration": {
"hours": 48
},
"filters": {
"shiftIds": ["shift1"]
}
}
]
},
"configuration": {
"workTypes": [
{
"id": "1",
"name": "WORK",
"factor": 1.0
}
]
}
}