diff --git a/gen_proc.py b/gen_proc.py index 378b22b..514f845 100644 --- a/gen_proc.py +++ b/gen_proc.py @@ -84,149 +84,149 @@ def main( #] Rota.add_shifts( - SingleShift( - sites=( - "exeter", - "exeter twilights and weekends", - "exeter no nights", - "exeter twilights", - ), - name="exeter_twilight", - length=12.5, - days=days[:5], - balance_offset=2, - constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)], - ), - SingleShift( - sites=( - "truro", - "truro no nights", - "truro twilights", - "truro twilights and weekends", - "truro twilights and weekend nights", - "truro twilights, weekends and weekend nights", - ), - name="truro_twilight", - length=12.5, - days=days[:4], - balance_offset=4, - constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)], - ), - SingleShift( - sites=("torbay", "torbay twilights", "torbay twilights and weekends"), - name="torbay_twilight", - length=12.5, - days=days[:4], - balance_offset=2, - assign_as_block=True, - ), - SingleShift( - sites=( - "plymouth", - "plymouth twilights", - "plymouth twilights and weekends", - "plymouth twilights, weekends and weekend nights", - ), - name="plymouth_twilight", - length=12.5, - days=days[:5], - balance_offset=2, - workers_required=1, - constraints=[MaxShiftsPerWeekConstraint(max_shifts=2)], - ), - SingleShift( - sites=( - "exeter", - "exeter twilights and weekends", - "exeter no nights", - "exeter weekends", - #"exeter twilights", - ), - name="weekend_exeter", - length=12.5, - days=days[5:], - #balance_offset=2, - rota_on_nwds=True, - force_as_block=True, - constraints=[PostShiftConstraint(days=2), PreShiftConstraint(days=2)], - # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},], - ), - SingleShift( - sites=( - "truro", - "truro no nights", - "truro twilights and weekends", - "truro weekends", - "truro twilights, weekends and weekend nights", - ), - name="weekend_truro", - length=12.5, - days=days[4:], - #balance_offset=3, - rota_on_nwds=True, - force_as_block=True, - #assign_as_block=True, - constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)], - # force_as_block_unless_nwd=True - ), - SingleShift( - sites=("torbay", "torbay twilights and weekends"), - name="weekend_torbay", - length=12.5, - days=days[4:], - #balance_offset=2, - rota_on_nwds=True, - force_as_block=True, - constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)], - # force_as_block_unless_nwd=True - ), - SingleShift( - sites=( - "plymouth", - "plymouth twilights and weekends", - "plymouth first on weekends only", - "plymouth twilights, weekends and weekend nights", - ), - name="weekend_plymouth1", - length=8, - days=days[5:], - #balance_offset=2.9, - workers_required=1, - rota_on_nwds=True, - force_as_block=True, - constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)], - ), - SingleShift( - sites=( - "plymouth", - "plymouth twilights and weekends", - "plymouth twilights, weekends and weekend nights", - ), - name="weekend_plymouth2", - length=8, - days=days[5:], - #balance_offset=2.9, - workers_required=1, - rota_on_nwds=True, - force_as_block=True, - constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)], - ), - SingleShift( - sites=( - "plymouth", - "plymouth twilights", - "plymouth twilights and weekends", - "plymouth twilights, weekends and weekend nights", - ), - name="plymouth_bank_holidays", - length=8, - days=days[5:], - balance_offset=1, - workers_required=1, - rota_on_nwds=True, - force_as_block=False, - bank_holidays_only=True, - ), + #SingleShift( + # sites=( + # "exeter", + # "exeter twilights and weekends", + # "exeter no nights", + # "exeter twilights", + # ), + # name="exeter_twilight", + # length=12.5, + # days=days[:5], + # balance_offset=2, + # constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)], + #), + #SingleShift( + # sites=( + # "truro", + # "truro no nights", + # "truro twilights", + # "truro twilights and weekends", + # "truro twilights and weekend nights", + # "truro twilights, weekends and weekend nights", + # ), + # name="truro_twilight", + # length=12.5, + # days=days[:4], + # balance_offset=4, + # constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)], + #), + #SingleShift( + # sites=("torbay", "torbay twilights", "torbay twilights and weekends"), + # name="torbay_twilight", + # length=12.5, + # days=days[:4], + # balance_offset=2, + # assign_as_block=True, + #), + #SingleShift( + # sites=( + # "plymouth", + # "plymouth twilights", + # "plymouth twilights and weekends", + # "plymouth twilights, weekends and weekend nights", + # ), + # name="plymouth_twilight", + # length=12.5, + # days=days[:5], + # balance_offset=2, + # workers_required=1, + # constraints=[MaxShiftsPerWeekConstraint(max_shifts=2)], + #), + #SingleShift( + # sites=( + # "exeter", + # "exeter twilights and weekends", + # "exeter no nights", + # "exeter weekends", + # #"exeter twilights", + # ), + # name="weekend_exeter", + # length=12.5, + # days=days[5:], + # #balance_offset=2, + # rota_on_nwds=True, + # force_as_block=True, + # constraints=[PostShiftConstraint(days=2), PreShiftConstraint(days=2)], + # # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},], + #), + #SingleShift( + # sites=( + # "truro", + # "truro no nights", + # "truro twilights and weekends", + # "truro weekends", + # "truro twilights, weekends and weekend nights", + # ), + # name="weekend_truro", + # length=12.5, + # days=days[4:], + # #balance_offset=3, + # rota_on_nwds=True, + # force_as_block=True, + # #assign_as_block=True, + # constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)], + # # force_as_block_unless_nwd=True + #), + #SingleShift( + # sites=("torbay", "torbay twilights and weekends"), + # name="weekend_torbay", + # length=12.5, + # days=days[4:], + # #balance_offset=2, + # rota_on_nwds=True, + # force_as_block=True, + # constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)], + # # force_as_block_unless_nwd=True + #), + #SingleShift( + # sites=( + # "plymouth", + # "plymouth twilights and weekends", + # "plymouth first on weekends only", + # "plymouth twilights, weekends and weekend nights", + # ), + # name="weekend_plymouth1", + # length=8, + # days=days[5:], + # #balance_offset=2.9, + # workers_required=1, + # rota_on_nwds=True, + # force_as_block=True, + # constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)], + #), + #SingleShift( + # sites=( + # "plymouth", + # "plymouth twilights and weekends", + # "plymouth twilights, weekends and weekend nights", + # ), + # name="weekend_plymouth2", + # length=8, + # days=days[5:], + # #balance_offset=2.9, + # workers_required=1, + # rota_on_nwds=True, + # force_as_block=True, + # constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)], + #), + #SingleShift( + # sites=( + # "plymouth", + # "plymouth twilights", + # "plymouth twilights and weekends", + # "plymouth twilights, weekends and weekend nights", + # ), + # name="plymouth_bank_holidays", + # length=8, + # days=days[5:], + # balance_offset=1, + # workers_required=1, + # rota_on_nwds=True, + # force_as_block=False, + # bank_holidays_only=True, + #), SingleShift( sites=sites, name="night_weekday", @@ -280,10 +280,11 @@ def main( # Prevent shifts for ST2s for 2 weeks #Rota.add_grade_constraint_by_week([2], [1, 2], [shift.name for shift in Rota.shifts]) - Rota.add_min_summed_grade_by_shifts_per_day_constraint(["weekend_plymouth1", "weekend_plymouth2"], 6) - Rota.add_min_summed_grade_by_shifts_per_day_constraint(["plymouth_twilight", "plymouth_bank_holidays"], 6) + #Rota.add_min_summed_grade_by_shifts_per_day_constraint(["weekend_plymouth1", "weekend_plymouth2"], 6) + #Rota.add_min_summed_grade_by_shifts_per_day_constraint(["plymouth_twilight", "plymouth_bank_holidays"], 6) Rota.add_min_summed_grade_by_shifts_per_day_constraint(["night_weekday"], 12) + Rota.add_min_summed_grade_by_shifts_per_day_constraint(["night_weekend"], 12) Rota.terminate_on_warning.remove("Worker/no valid shifts") diff --git a/output/timetable.js b/output/timetable.js index 02792ca..9437a36 100644 --- a/output/timetable.js +++ b/output/timetable.js @@ -921,6 +921,9 @@ function renderWorkerCheckboxes() { let workerFilterSet = new Set(); // Store the current setting let showShiftNames = false; +// Controls for rendering +let showWeekStart = false; +let showGrades = false; // Call this after the DOM is ready and after timetable is rendered renderWorkerCheckboxes(); @@ -938,8 +941,20 @@ $(document).on("change", "input[name='worker-filter-checkbox']", function() { ); renderShiftTimetable(); }); +// Add a checkbox to show/hide worker grades in the shift timetable +$("#shift-timetable-options").prepend(` +
+ +
+`); -let showWeekStart = false; +$(document).on("change", "#show-grades-checkbox", function() { + showGrades = $(this).is(":checked"); + renderShiftTimetable(); +}); // Add a checkbox to show/hide shift names in the shift timetable section $("#shift-timetable-options").prepend(`
@@ -1022,7 +1037,7 @@ function renderShiftTimetable() { row.append(`${day}`); let workerCells = []; - $(`table#main-table td[data-date='${date}']`).filter(function () { + $(`table#main-table td[data-date='${date}']`).filter(function () { let shifts = $(this).attr("data-shift"); if (!shifts) return false; let cell_shifts = shifts.split(",").map(s => s.trim()); @@ -1030,6 +1045,14 @@ function renderShiftTimetable() { }).each((n, td) => { let workerTd = $(td).closest("tr").children("td:first"); let workerName = workerTd.find("span.name").text().trim(); + // grade may be provided as a data attribute; if not, try to parse from visible text + let workerGrade = workerTd.attr('data-grade') || ''; + if (!workerGrade) { + // attempt to parse e.g. "Name (4) [100]" -> capture 4 + let txt = workerTd.text(); + let m = txt.match(/\(([^)]+)\)/); + if (m) workerGrade = m[1].trim(); + } let workerId = workerTd.attr("data-worker-id"); if (!workerMatchesFilter(workerId)) return; let cell_shifts = $(td).attr("data-shift").split(",").map(s => s.trim()); @@ -1043,7 +1066,8 @@ function renderShiftTimetable() { color = `linear-gradient(90deg, ${shiftColors[matchedShifts[0]]} 0 50%, ${shiftColors[matchedShifts[1]]} 50% 100%)`; if (showShiftNames) shiftLabel = ` (${matchedShifts.join(", ")})`; } - workerCells.push(`${workerName}${shiftLabel}`); + let gradeTextLinear = showGrades ? ` (${workerGrade})` : ''; + workerCells.push(`${workerName}${gradeTextLinear}${shiftLabel}`); }); if (workerCells.length === 0) { @@ -1084,10 +1108,12 @@ function renderShiftTimetable() { $("table#main-table td.worker").each(function () { let workerId = $(this).attr("data-worker-id"); let workerName = $(this).find("span.name").text().trim(); + let workerGrade = $(this).attr('data-grade') || ''; if (workerId && !workerIds.includes(workerId)) { if (workerMatchesFilter(workerId)) { workerIds.push(workerId); workerIdToName[workerId] = workerName; + workerIdToName[workerId + '_grade'] = workerGrade; } } }); @@ -1096,7 +1122,8 @@ function renderShiftTimetable() { $("table#main-table td.worker").each(function () { let workerId = $(this).attr("data-worker-id"); let workerName = $(this).find("span.name").text().trim(); - if (!cellMap[workerId]) cellMap[workerId] = { name: workerName, cells: {} }; + let workerGrade = $(this).attr('data-grade') || ''; + if (!cellMap[workerId]) cellMap[workerId] = { name: workerName, grade: workerGrade, cells: {} }; }); $("table#main-table td[data-week][data-day]").each(function () { let td = $(this); @@ -1141,7 +1168,8 @@ function renderShiftTimetable() { color = `linear-gradient(90deg, ${shiftColors[matchedShifts[0]]} 0 50%, ${shiftColors[matchedShifts[1]]} 50% 100%)`; if (showShiftNames) shiftLabel = ` (${matchedShifts.join(", ")})`; } - cellContent.push(`
${worker.name}${shiftLabel}
`); + let gradeText = showGrades ? ` (${worker.grade||''})` : ''; + cellContent.push(`
${worker.name}${gradeText}${shiftLabel}
`); } } row.append(`${cellContent.join(" / ")}`); diff --git a/rota_generator/shifts.py b/rota_generator/shifts.py index 586a299..ca13636 100644 --- a/rota_generator/shifts.py +++ b/rota_generator/shifts.py @@ -2439,13 +2439,37 @@ class RotaBuilder(object): # nothing to constrain continue + # Determine which of the requested shifts actually occur on the selected dates + present_shifts = set( + shift + for wk, d, shift in self.get_all_shiftname_combinations() + if (wk, d) in selected_weeks_days + ) + + requested_shifts = set(constraint_dict["shifts"]) + used_shifts = requested_shifts.intersection(present_shifts) + + missing_shifts = requested_shifts - present_shifts + if missing_shifts: + self.add_warning( + "avoid shifts by worker/dates", + ( + f"Requested avoid shifts {sorted(list(missing_shifts))} for worker '{worker.name}'" + " are not present in the rota dates selected and will be ignored" + ), + ) + + if not used_shifts: + # Nothing to constrain after filtering — skip adding the constraint + continue + # Add constraint that forbids the given shifts for this worker on the selected dates self.model.constraints.add( 0 == sum( self.model.works[worker.id, wk, d, shift] for wk, d, shift in self.get_all_shiftname_combinations() - if (wk, d) in selected_weeks_days and shift in constraint_dict["shifts"] + if (wk, d) in selected_weeks_days and shift in used_shifts ) ) @@ -5584,6 +5608,7 @@ class RotaBuilder(object): worker_td = """