Add functionality to handle missing shifts in RotaBuilder and include worker grades in the timetable
This commit is contained in:
+146
-145
@@ -84,149 +84,149 @@ def main(
|
|||||||
#]
|
#]
|
||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=(
|
# sites=(
|
||||||
"exeter",
|
# "exeter",
|
||||||
"exeter twilights and weekends",
|
# "exeter twilights and weekends",
|
||||||
"exeter no nights",
|
# "exeter no nights",
|
||||||
"exeter twilights",
|
# "exeter twilights",
|
||||||
),
|
# ),
|
||||||
name="exeter_twilight",
|
# name="exeter_twilight",
|
||||||
length=12.5,
|
# length=12.5,
|
||||||
days=days[:5],
|
# days=days[:5],
|
||||||
balance_offset=2,
|
# balance_offset=2,
|
||||||
constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)],
|
# constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)],
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=(
|
# sites=(
|
||||||
"truro",
|
# "truro",
|
||||||
"truro no nights",
|
# "truro no nights",
|
||||||
"truro twilights",
|
# "truro twilights",
|
||||||
"truro twilights and weekends",
|
# "truro twilights and weekends",
|
||||||
"truro twilights and weekend nights",
|
# "truro twilights and weekend nights",
|
||||||
"truro twilights, weekends and weekend nights",
|
# "truro twilights, weekends and weekend nights",
|
||||||
),
|
# ),
|
||||||
name="truro_twilight",
|
# name="truro_twilight",
|
||||||
length=12.5,
|
# length=12.5,
|
||||||
days=days[:4],
|
# days=days[:4],
|
||||||
balance_offset=4,
|
# balance_offset=4,
|
||||||
constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)],
|
# constraints=[MaxShiftsPerWeekConstraint(max_shifts=1)],
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=("torbay", "torbay twilights", "torbay twilights and weekends"),
|
# sites=("torbay", "torbay twilights", "torbay twilights and weekends"),
|
||||||
name="torbay_twilight",
|
# name="torbay_twilight",
|
||||||
length=12.5,
|
# length=12.5,
|
||||||
days=days[:4],
|
# days=days[:4],
|
||||||
balance_offset=2,
|
# balance_offset=2,
|
||||||
assign_as_block=True,
|
# assign_as_block=True,
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=(
|
# sites=(
|
||||||
"plymouth",
|
# "plymouth",
|
||||||
"plymouth twilights",
|
# "plymouth twilights",
|
||||||
"plymouth twilights and weekends",
|
# "plymouth twilights and weekends",
|
||||||
"plymouth twilights, weekends and weekend nights",
|
# "plymouth twilights, weekends and weekend nights",
|
||||||
),
|
# ),
|
||||||
name="plymouth_twilight",
|
# name="plymouth_twilight",
|
||||||
length=12.5,
|
# length=12.5,
|
||||||
days=days[:5],
|
# days=days[:5],
|
||||||
balance_offset=2,
|
# balance_offset=2,
|
||||||
workers_required=1,
|
# workers_required=1,
|
||||||
constraints=[MaxShiftsPerWeekConstraint(max_shifts=2)],
|
# constraints=[MaxShiftsPerWeekConstraint(max_shifts=2)],
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=(
|
# sites=(
|
||||||
"exeter",
|
# "exeter",
|
||||||
"exeter twilights and weekends",
|
# "exeter twilights and weekends",
|
||||||
"exeter no nights",
|
# "exeter no nights",
|
||||||
"exeter weekends",
|
# "exeter weekends",
|
||||||
#"exeter twilights",
|
# #"exeter twilights",
|
||||||
),
|
# ),
|
||||||
name="weekend_exeter",
|
# name="weekend_exeter",
|
||||||
length=12.5,
|
# length=12.5,
|
||||||
days=days[5:],
|
# days=days[5:],
|
||||||
#balance_offset=2,
|
# #balance_offset=2,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
force_as_block=True,
|
# force_as_block=True,
|
||||||
constraints=[PostShiftConstraint(days=2), PreShiftConstraint(days=2)],
|
# constraints=[PostShiftConstraint(days=2), PreShiftConstraint(days=2)],
|
||||||
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
|
# # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=(
|
# sites=(
|
||||||
"truro",
|
# "truro",
|
||||||
"truro no nights",
|
# "truro no nights",
|
||||||
"truro twilights and weekends",
|
# "truro twilights and weekends",
|
||||||
"truro weekends",
|
# "truro weekends",
|
||||||
"truro twilights, weekends and weekend nights",
|
# "truro twilights, weekends and weekend nights",
|
||||||
),
|
# ),
|
||||||
name="weekend_truro",
|
# name="weekend_truro",
|
||||||
length=12.5,
|
# length=12.5,
|
||||||
days=days[4:],
|
# days=days[4:],
|
||||||
#balance_offset=3,
|
# #balance_offset=3,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
force_as_block=True,
|
# force_as_block=True,
|
||||||
#assign_as_block=True,
|
# #assign_as_block=True,
|
||||||
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
# constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
||||||
# force_as_block_unless_nwd=True
|
# # force_as_block_unless_nwd=True
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=("torbay", "torbay twilights and weekends"),
|
# sites=("torbay", "torbay twilights and weekends"),
|
||||||
name="weekend_torbay",
|
# name="weekend_torbay",
|
||||||
length=12.5,
|
# length=12.5,
|
||||||
days=days[4:],
|
# days=days[4:],
|
||||||
#balance_offset=2,
|
# #balance_offset=2,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
force_as_block=True,
|
# force_as_block=True,
|
||||||
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
# constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
||||||
# force_as_block_unless_nwd=True
|
# # force_as_block_unless_nwd=True
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=(
|
# sites=(
|
||||||
"plymouth",
|
# "plymouth",
|
||||||
"plymouth twilights and weekends",
|
# "plymouth twilights and weekends",
|
||||||
"plymouth first on weekends only",
|
# "plymouth first on weekends only",
|
||||||
"plymouth twilights, weekends and weekend nights",
|
# "plymouth twilights, weekends and weekend nights",
|
||||||
),
|
# ),
|
||||||
name="weekend_plymouth1",
|
# name="weekend_plymouth1",
|
||||||
length=8,
|
# length=8,
|
||||||
days=days[5:],
|
# days=days[5:],
|
||||||
#balance_offset=2.9,
|
# #balance_offset=2.9,
|
||||||
workers_required=1,
|
# workers_required=1,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
force_as_block=True,
|
# force_as_block=True,
|
||||||
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
# constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=(
|
# sites=(
|
||||||
"plymouth",
|
# "plymouth",
|
||||||
"plymouth twilights and weekends",
|
# "plymouth twilights and weekends",
|
||||||
"plymouth twilights, weekends and weekend nights",
|
# "plymouth twilights, weekends and weekend nights",
|
||||||
),
|
# ),
|
||||||
name="weekend_plymouth2",
|
# name="weekend_plymouth2",
|
||||||
length=8,
|
# length=8,
|
||||||
days=days[5:],
|
# days=days[5:],
|
||||||
#balance_offset=2.9,
|
# #balance_offset=2.9,
|
||||||
workers_required=1,
|
# workers_required=1,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
force_as_block=True,
|
# force_as_block=True,
|
||||||
constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
# constraints=[PreShiftConstraint(days=2), PostShiftConstraint(days=2)],
|
||||||
),
|
#),
|
||||||
SingleShift(
|
#SingleShift(
|
||||||
sites=(
|
# sites=(
|
||||||
"plymouth",
|
# "plymouth",
|
||||||
"plymouth twilights",
|
# "plymouth twilights",
|
||||||
"plymouth twilights and weekends",
|
# "plymouth twilights and weekends",
|
||||||
"plymouth twilights, weekends and weekend nights",
|
# "plymouth twilights, weekends and weekend nights",
|
||||||
),
|
# ),
|
||||||
name="plymouth_bank_holidays",
|
# name="plymouth_bank_holidays",
|
||||||
length=8,
|
# length=8,
|
||||||
days=days[5:],
|
# days=days[5:],
|
||||||
balance_offset=1,
|
# balance_offset=1,
|
||||||
workers_required=1,
|
# workers_required=1,
|
||||||
rota_on_nwds=True,
|
# rota_on_nwds=True,
|
||||||
force_as_block=False,
|
# force_as_block=False,
|
||||||
bank_holidays_only=True,
|
# bank_holidays_only=True,
|
||||||
),
|
#),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=sites,
|
sites=sites,
|
||||||
name="night_weekday",
|
name="night_weekday",
|
||||||
@@ -280,10 +280,11 @@ def main(
|
|||||||
# Prevent shifts for ST2s for 2 weeks
|
# 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_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(["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(["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_weekday"], 12)
|
||||||
|
Rota.add_min_summed_grade_by_shifts_per_day_constraint(["night_weekend"], 12)
|
||||||
|
|
||||||
Rota.terminate_on_warning.remove("Worker/no valid shifts")
|
Rota.terminate_on_warning.remove("Worker/no valid shifts")
|
||||||
|
|
||||||
|
|||||||
+33
-5
@@ -921,6 +921,9 @@ function renderWorkerCheckboxes() {
|
|||||||
let workerFilterSet = new Set();
|
let workerFilterSet = new Set();
|
||||||
// Store the current setting
|
// Store the current setting
|
||||||
let showShiftNames = false;
|
let showShiftNames = false;
|
||||||
|
// Controls for rendering
|
||||||
|
let showWeekStart = false;
|
||||||
|
let showGrades = false;
|
||||||
|
|
||||||
// Call this after the DOM is ready and after timetable is rendered
|
// Call this after the DOM is ready and after timetable is rendered
|
||||||
renderWorkerCheckboxes();
|
renderWorkerCheckboxes();
|
||||||
@@ -938,8 +941,20 @@ $(document).on("change", "input[name='worker-filter-checkbox']", function() {
|
|||||||
);
|
);
|
||||||
renderShiftTimetable();
|
renderShiftTimetable();
|
||||||
});
|
});
|
||||||
|
// Add a checkbox to show/hide worker grades in the shift timetable
|
||||||
|
$("#shift-timetable-options").prepend(`
|
||||||
|
<div id="shift-timetable-show-grades" style="margin-bottom:8px;">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="show-grades-checkbox">
|
||||||
|
Show worker grades in timetable
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
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
|
// Add a checkbox to show/hide shift names in the shift timetable section
|
||||||
$("#shift-timetable-options").prepend(`
|
$("#shift-timetable-options").prepend(`
|
||||||
<div id="shift-timetable-show-shift-names" style="margin-bottom:8px;">
|
<div id="shift-timetable-show-shift-names" style="margin-bottom:8px;">
|
||||||
@@ -1022,7 +1037,7 @@ function renderShiftTimetable() {
|
|||||||
row.append(`<td>${day}</td>`);
|
row.append(`<td>${day}</td>`);
|
||||||
|
|
||||||
let workerCells = [];
|
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");
|
let shifts = $(this).attr("data-shift");
|
||||||
if (!shifts) return false;
|
if (!shifts) return false;
|
||||||
let cell_shifts = shifts.split(",").map(s => s.trim());
|
let cell_shifts = shifts.split(",").map(s => s.trim());
|
||||||
@@ -1030,6 +1045,14 @@ function renderShiftTimetable() {
|
|||||||
}).each((n, td) => {
|
}).each((n, td) => {
|
||||||
let workerTd = $(td).closest("tr").children("td:first");
|
let workerTd = $(td).closest("tr").children("td:first");
|
||||||
let workerName = workerTd.find("span.name").text().trim();
|
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");
|
let workerId = workerTd.attr("data-worker-id");
|
||||||
if (!workerMatchesFilter(workerId)) return;
|
if (!workerMatchesFilter(workerId)) return;
|
||||||
let cell_shifts = $(td).attr("data-shift").split(",").map(s => s.trim());
|
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%)`;
|
color = `linear-gradient(90deg, ${shiftColors[matchedShifts[0]]} 0 50%, ${shiftColors[matchedShifts[1]]} 50% 100%)`;
|
||||||
if (showShiftNames) shiftLabel = `<span style="font-size:0.9em; color:#333;"> (${matchedShifts.join(", ")})</span>`;
|
if (showShiftNames) shiftLabel = `<span style="font-size:0.9em; color:#333;"> (${matchedShifts.join(", ")})</span>`;
|
||||||
}
|
}
|
||||||
workerCells.push(`<td style="background:${color}">${workerName}${shiftLabel}</td>`);
|
let gradeTextLinear = showGrades ? ` <span style="font-size:0.85em;color:#444;">(${workerGrade})</span>` : '';
|
||||||
|
workerCells.push(`<td style="background:${color}">${workerName}${gradeTextLinear}${shiftLabel}</td>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workerCells.length === 0) {
|
if (workerCells.length === 0) {
|
||||||
@@ -1084,10 +1108,12 @@ function renderShiftTimetable() {
|
|||||||
$("table#main-table td.worker").each(function () {
|
$("table#main-table td.worker").each(function () {
|
||||||
let workerId = $(this).attr("data-worker-id");
|
let workerId = $(this).attr("data-worker-id");
|
||||||
let workerName = $(this).find("span.name").text().trim();
|
let workerName = $(this).find("span.name").text().trim();
|
||||||
|
let workerGrade = $(this).attr('data-grade') || '';
|
||||||
if (workerId && !workerIds.includes(workerId)) {
|
if (workerId && !workerIds.includes(workerId)) {
|
||||||
if (workerMatchesFilter(workerId)) {
|
if (workerMatchesFilter(workerId)) {
|
||||||
workerIds.push(workerId);
|
workerIds.push(workerId);
|
||||||
workerIdToName[workerId] = workerName;
|
workerIdToName[workerId] = workerName;
|
||||||
|
workerIdToName[workerId + '_grade'] = workerGrade;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1096,7 +1122,8 @@ function renderShiftTimetable() {
|
|||||||
$("table#main-table td.worker").each(function () {
|
$("table#main-table td.worker").each(function () {
|
||||||
let workerId = $(this).attr("data-worker-id");
|
let workerId = $(this).attr("data-worker-id");
|
||||||
let workerName = $(this).find("span.name").text().trim();
|
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 () {
|
$("table#main-table td[data-week][data-day]").each(function () {
|
||||||
let td = $(this);
|
let td = $(this);
|
||||||
@@ -1141,7 +1168,8 @@ function renderShiftTimetable() {
|
|||||||
color = `linear-gradient(90deg, ${shiftColors[matchedShifts[0]]} 0 50%, ${shiftColors[matchedShifts[1]]} 50% 100%)`;
|
color = `linear-gradient(90deg, ${shiftColors[matchedShifts[0]]} 0 50%, ${shiftColors[matchedShifts[1]]} 50% 100%)`;
|
||||||
if (showShiftNames) shiftLabel = `<span style="font-size:0.9em; color:#333;"> (${matchedShifts.join(", ")})</span>`;
|
if (showShiftNames) shiftLabel = `<span style="font-size:0.9em; color:#333;"> (${matchedShifts.join(", ")})</span>`;
|
||||||
}
|
}
|
||||||
cellContent.push(`<div style="background:${color};margin:1px 0;padding:0 2px;border-radius:3px;display:inline-block;">${worker.name}${shiftLabel}</div>`);
|
let gradeText = showGrades ? ` <span style="font-size:0.85em;color:#444;">(${worker.grade||''})</span>` : '';
|
||||||
|
cellContent.push(`<div style="background:${color};margin:1px 0;padding:0 2px;border-radius:3px;display:inline-block;">${worker.name}${gradeText}${shiftLabel}</div>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
row.append(`<td style="border:1px solid #888; min-width:40px; text-align:center;">${cellContent.join(" / ")}</td>`);
|
row.append(`<td style="border:1px solid #888; min-width:40px; text-align:center;">${cellContent.join(" / ")}</td>`);
|
||||||
|
|||||||
@@ -2439,13 +2439,37 @@ class RotaBuilder(object):
|
|||||||
# nothing to constrain
|
# nothing to constrain
|
||||||
continue
|
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
|
# Add constraint that forbids the given shifts for this worker on the selected dates
|
||||||
self.model.constraints.add(
|
self.model.constraints.add(
|
||||||
0
|
0
|
||||||
== sum(
|
== sum(
|
||||||
self.model.works[worker.id, wk, d, shift]
|
self.model.works[worker.id, wk, d, shift]
|
||||||
for wk, d, shift in self.get_all_shiftname_combinations()
|
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 = """<td title='Site: {site}' class='worker {site}'
|
worker_td = """<td title='Site: {site}' class='worker {site}'
|
||||||
data-worker-id='{worker_id}'
|
data-worker-id='{worker_id}'
|
||||||
|
data-grade='{grade}'
|
||||||
data-nwds='{nwds}' data-site='{site}' data-worker='{name}'
|
data-nwds='{nwds}' data-site='{site}' data-worker='{name}'
|
||||||
data-fte='{fte}' data-fte_adj='{fte_adj}'
|
data-fte='{fte}' data-fte_adj='{fte_adj}'
|
||||||
data-start_date='{start_date}'
|
data-start_date='{start_date}'
|
||||||
|
|||||||
Reference in New Issue
Block a user