This commit is contained in:
Ross
2022-07-16 19:50:18 +01:00
parent fecddb2db1
commit 5b31a01546
6 changed files with 231 additions and 814 deletions
+32 -22
View File
@@ -28,18 +28,19 @@ from rota.workers import (
suspend_on_finish = True
solve = True
time_to_run = 258500
time_to_run = 36000
time_to_run = 166000
# allow = 5000
ratio = 0.05
ratio = 0.01
#ratio = 0.9
start_date = datetime.date(2022, 9, 5)
#start_date = datetime.date(2022, 10, 10)
# start_date = datetime.date(2022, 10, 10)
Rota = RotaBuilder(start_date, weeks_to_rota=26)
Rota.constraint_options["balance_weekends"] = True
Rota.constraint_options["max_night_frequency"] = 4
Rota.constraint_options["max_weekend_frequency"] = 4
Rota.constraint_options["max_night_frequency"] = 3
Rota.constraint_options["max_weekend_frequency"] = 3
# Rota.constraint_options["avoid_st2_first_month"] = True
Rota.add_shifts(
@@ -48,22 +49,27 @@ Rota.add_shifts(
name="exeter_twilight",
length=12.5,
days=days[:5],
#balance_offset=3,
constraint=[{"name": "max_shifts_per_week", "options": 2, }],
balance_offset=2,
constraint=[
{
"name": "max_shifts_per_week",
"options": 2,
}
],
),
SingleShift(
sites=("truro", "truro twilights"),
name="truro_twilight",
length=12.5,
days=days[:4],
#balance_offset=3,
balance_offset=2,
),
SingleShift(
sites=("torbay", "torbay twilights"),
name="torbay_twilight",
length=12.5,
days=days[:4],
#balance_offset=4,
balance_offset=2,
assign_as_block=True,
),
SingleShift(
@@ -71,7 +77,7 @@ Rota.add_shifts(
name="plymouth_twilight",
length=12.5,
days=days[:5],
#balance_offset=4,
balance_offset=2,
workers_required=1,
constraint=[{"name": "max_shifts_per_week", "options": 2}],
),
@@ -80,18 +86,18 @@ Rota.add_shifts(
name="weekend_exeter",
length=12.5,
days=days[5:],
#balance_offset=4,
balance_offset=2,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 5}],
#constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
),
SingleShift(
sites=("truro",),
name="weekend_truro",
length=12.5,
days=days[4:],
#balance_offset=4,
balance_offset=3,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
@@ -102,7 +108,7 @@ Rota.add_shifts(
name="weekend_torbay",
length=12.5,
days=days[4:],
#balance_offset=4,
balance_offset=3,
rota_on_nwds=True,
force_as_block=True,
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
@@ -113,7 +119,7 @@ Rota.add_shifts(
name="weekend_plymouth1",
length=8,
days=days[5:],
#balance_offset=4,
balance_offset=2,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
@@ -124,7 +130,7 @@ Rota.add_shifts(
name="weekend_plymouth2",
length=8,
days=days[5:],
#balance_offset=4,
balance_offset=2,
workers_required=1,
rota_on_nwds=True,
force_as_block=True,
@@ -135,7 +141,7 @@ Rota.add_shifts(
name="plymouth_bank_holidays",
length=8,
days=days[5:],
#balance_offset=2,
balance_offset=2,
workers_required=1,
rota_on_nwds=True,
force_as_block=False,
@@ -146,7 +152,7 @@ Rota.add_shifts(
name="night_weekday",
length=12.25,
days=days[:4],
#balance_offset=4.9,
# balance_offset=4.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=3,
@@ -157,6 +163,8 @@ Rota.add_shifts(
{"name": "pre", "options": 5},
{"name": "post", "options": 5},
{"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
{"name": "limit_grade_number", "options": {2: 1}},
{"name": "minimum_grade_number", "options": (4, 1)},
],
),
SingleShift(
@@ -164,7 +172,7 @@ Rota.add_shifts(
name="night_weekend",
length=12.25,
days=days[4:],
#balance_offset=3.9,
# balance_offset=3.9,
balance_weighting=1,
# hard_constrain_shift=False,
workers_required=3,
@@ -175,6 +183,8 @@ Rota.add_shifts(
{"name": "pre", "options": 5},
{"name": "post", "options": 5},
{"name": "require_remote_site_presence_week", "options": ("plymouth", 1)},
{"name": "limit_grade_number", "options": {2: 1}},
{"name": "minimum_grade_number", "options": (4, 1)},
],
),
)
@@ -283,15 +293,15 @@ if load_leave:
)
)
#Rota.build_workers()
#Rota.build_model()
# Rota.build_workers()
# Rota.build_model()
solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
start_time = time.time()
Rota.build_and_solve(solver_options)
#Rota.solve_shifts_individually(solver_options)
#Rota.solve_model(options=solver_options)
# Rota.solve_model(options=solver_options)
end_time = time.time()
print(f"Time taken {end_time-start_time}")
-301
View File
@@ -1,301 +0,0 @@
html {
/* color: red; */
}
table {
/* margin-top: 50px; */
padding-top: 100px;
text-align: center;
/* table-layout : fixed; */
/* width:150px */
/* width: 200%; */
/* overflow-x: auto;
display: block;
position: relative; */
border-collapse: separate;
border-spacing: 0;
}
.shift-table {
padding-top: 10px;
}
.shift-table thead {
font-weight: bold;
}
.table-div {
/* overflow-x: auto; */
width: 80%;
overflow-x: scroll;
margin-left: 200px;
overflow-y: visible;
padding: 0;
}
.table-div th {
text-align: center;
white-space: nowrap;
transform: rotate(90deg) translateX(-100px);
position: sticky;
top: 0;
}
.table-div th p {
margin: 0 -100%;
display: inline-block;
}
.table-div th p:before {
content: '';
width: 0;
padding-top: 110%;
/* takes width as reference, + 10% for faking some extra padding */
display: inline-block;
vertical-align: middle;
}
.table-div td,
th {
max-width: 10px;
padding: 0px;
}
.table-div tr {
padding: 0px;
}
.table-div tr:hover {
background-color: lightblue;
}
.table-div tr:hover .unavailable {
background-color: lightgray;
}
.Sat {
/* opacity: 30%; */
/* background-color: lightsteelblue; */
border-left: solid 1px lightsteelblue;
}
.Sun {
border-right: solid 1px lightsteelblue;
}
.bank-holiday {
border-bottom: dotted 1px blue;
}
th.bank-holiday {
color: darkblue;
}
/* th.worker {
max-width: 100%;
width: 200px;
} */
.plymouth {
color: #9169FF;
}
.exeter {
color: #5F60E8;
}
.truro {
color: #0074D9;
}
.torquay, .torbay {
color: #5FA8E8;
}
.taunton {
color: #e85fdf;
}
.unavailable {
color: lightgray;
}
.shift-requested {
color: green;
}
.worker-summary {
position: absolute;
display: none;
overflow-x: visible;
width: 100%;
text-align: left;
}
.site-summary {
display: inline-flex;
}
.site-summary .site {
border: 1px solid black;
padding: 5px;
margin: 20px;
}
.site-name {
font-weight: bold;
}
.worker-summary span {
min-width: 160px;
display: inline-flex;
}
.training-days-lost {
padding-top: 5px;
}
.tdl-settings {
padding: 2px;
}
.table-div+pre {
display: none;
}
.shift-breakdown-button+pre {
display: none;
}
.shift-breakdown-button {
display: block;
}
.nwd {
color: lightcoral;
}
.th {}
.table-div th.worker,
td.worker {
position: absolute;
width: 200px;
max-width: 200px;
left: 0;
top: auto;
border-top-width: 1px;
/*only relevant for first row*/
margin-top: -1px;
/*compensate for top border*/
}
.hidden {
opacity: 0%;
}
.tsummary {
max-width: auto;
}
.header th {
max-width: 200px;
}
/* [data-night-at-derriford="0"] {
border: 1px dotted orange;
} */
th.site-title {
transform: unset;
}
.name {
max-width: 90px;
display: inline-flex;
white-space: nowrap;
overflow: hidden;
/*text-overflow: ellipsis;*/
}
.shift-highlight {
background-color: palegreen;
}
.pair-match {
background-color: red;
}
summary h2 {
display: inline;
}
.remote-site-match.shift-highlight {
background-color: yellow;
}
.large-shift-diff::before {
content: "*";
color: red;
}
/* table.transposed {
border-collapse: collapse;
}
table.transposed tr {
display: block;
float: left;
}
table.transposed th,
table.transposed td {
display: block;
border: 1px solid black;
max-width: 100px;
} */
table.transposed {
}
table.transposed tr {
}
table.transposed th,
table.transposed td {
width: fit-content;
max-width: unset;
position: relative;
border-top: solid 0.5px black;
border-right: solid 0.5px black;
}
table.transposed .Sat {
/* opacity: 30%; */
/* background-color: lightsteelblue; */
border-top: solid 2px lightsteelblue !important;
}
table.transposed .Sun {
border-bottom: solid 2px lightsteelblue !important;
}
table.transposed .bank-holiday {
border-bottom: dotted 2px blue !important;
}
table.transposed th.bank-holiday {
color: darkblue;
}
+1
View File
@@ -0,0 +1 @@
../timetable.css
-437
View File
@@ -1,437 +0,0 @@
Object.defineProperties(Array.prototype, {
count: {
value: function (query) {
/*
Counts number of occurrences of query in array, an integer >= 0
Uses the javascript == notion of equality.
*/
var count = 0;
for (let i = 0; i < this.length; i++)
if (this[i] == query)
count++;
return count;
}
}
});
const mean = arr => arr.reduce((p, c) => p + c, 0) / arr.length;
tables = $(".table-div");
function generateExtra() {
$(".auto-generated").remove();
global_shifts = new Set();
total_summed_shift_diffs = 0;
total_summed_shift_diffs_abs = 0;
tables.each((n, table) => {
rows = $(table).find(".worker-row");
workers = {};
rows.each((n, tr) => {
worker_td = $(tr).find("td:first");
worker = worker_td.attr("data-worker");
shift_tds = $(tr).find(".rota-day");
shifts = []
shift_tds.each((n, td) => {
shifts.push($(td).attr("data-shift"));
})
shifts_unique = new Set(shifts);
shifts_unique.delete("");
global_shifts = new Set([...global_shifts, ...shifts_unique]);
weekends_worked = 0;
weeks = shift_tds.length / 7;
whole_weekends_worked = 0;
for (var i = 1; i <= weeks; i++) {
if ($(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift") != "" || $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift") != "") {
weekends_worked = weekends_worked + 1;
}
if ($(tr).find(`[data-week='${i}'][data-day='Sat']`).attr("data-shift") != "" && $(tr).find(`[data-week='${i}'][data-day='Sun']`).attr("data-shift") != "") {
whole_weekends_worked = whole_weekends_worked + 1;
}
}
shift_counts = {};
total_shifts = 0;
days_lost = 0;
shifts_unique.forEach(el => {
shift_counts[el] = shifts.count(el);
total_shifts = total_shifts + shifts.count(el);
// why like this?
if (el == "night_weekend") {
whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
}
if ($("#global-settings").length == 1) {
n = $(`#${el}-tdl`).val();
if (!isNaN(n)) {
days_lost = days_lost + shifts.count(el) * n;
}
}
// // 5 days for weekday nights
// if(el == "night_weekday") { days_lost = days_lost + shifts.count(el) / 4 * 5 }
// // 4 days for weekend nights
// if(el == "night_weekend") {
// days_lost = days_lost + shifts.count(el) / 3 * 4
// //whole_weekends_worked = whole_weekends_worked - shifts.count(el) / 3
// }
// // 0.5 for twilights
// if(el.includes("twilight")) {
// twilight_days_lost = shifts.count(el) / 2
// days_lost = days_lost + twilight_days_lost
// }
});
// Also 1 day for a whole weekend
days_lost = days_lost + whole_weekends_worked
// Highlight non working days
nwds = JSON.parse(worker_td.attr("data-nwds"));
nwds.forEach(element => {
// Restrict to date ranges
start_date = new Date(Date.parse(element[1]))
end_date = new Date(Date.parse(element[2]))
$(shift_tds).filter(`.${element[0]}`).each(
(n, td_el) => {
td_date = new Date(Date.parse(td_el.dataset.date));
if (td_date >= start_date && td_date < end_date) {
$(td_el).addClass("nwd");
}
}
)
});
workers[worker] = {
"site": worker_td.attr("data-site"),
"nwds": worker_td.attr("data-nwds"),
"fte": worker_td.attr("data-fte_adj"),
"end_date": worker_td.attr("data-end_date"),
"shift_diff": worker_td.attr("data-shift-diff"),
"shifts": shifts,
"shift_types": shifts_unique,
"weekends_worked": weekends_worked,
"shift_counts": shift_counts,
"days_lost": days_lost,
//"shifts"
}
sc = JSON.stringify(shift_counts);
bank_holidays = shift_tds.filter(".bank-holiday").filter(function (i) {
return $(this).attr('data-shift') != "";
});
console.log("BH", bank_holidays.length)
bank_holidays_worked = bank_holidays.length;
shift_diff = JSON.parse(worker_td.attr("data-shift-diff"));
summed_shift_diff = Object.values(shift_diff).reduce((a, b) => a + b);
total_summed_shift_diffs = total_summed_shift_diffs + summed_shift_diff;
total_summed_shift_diffs_abs = total_summed_shift_diffs_abs + Math.abs(summed_shift_diff);
worker_td.after(`<div class='worker-summary auto-generated'>
<span>Total shifts: ${total_shifts}, </span>
<span>Weekends: ${weekends_worked}, </span>
<span>Bank holidays: ${bank_holidays_worked}, </span>
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
</div>`)
if (Math.abs(summed_shift_diff) >= 1) {
worker_td.addClass("large-shift-diff")
$(tr).find(".shift-diff-span").addClass("large-shift-diff")
}
})
sites = {};
sites_days_lost = {}
for (const worker in workers) {
//console.log(worker, workers[worker]);
w = workers[worker];
if (w.fte == 100) {
//console.log(w.shift_types);
for (let shift of w.shift_types) {
//console.log(shift);
if (!(w.site in sites)) {
sites[w.site] = {};
}
if (!(shift in sites[w.site])) {
sites[w.site][shift] = [w.shift_counts[shift]];
} else {
sites[w.site][shift].push(w.shift_counts[shift]);
}
}
if (!(w.site in sites_days_lost)) {
sites_days_lost[w.site] = [w.days_lost];
} else {
sites_days_lost[w.site].push(w.days_lost);
}
}
}
// Calculate site shift summaries
// $(table).append("<div class='site-summary auto-generated'>Average shifts for a 100% FTE trainee</div>");
// for(const site in sites) {
// $(table).find(".site-summary").append(`<div class='site site-${site}'><span class='site-name'>${site}</span></div>`);
// for(const shift in sites[site]) {
// average = mean(sites[site][shift])
// $(table).find(`.site-${site}`).append(`<div>${shift}: ${average.toFixed(2)}</div>`);
// }
// days_lost_avg = mean(sites_days_lost[site]);
// $(table).find(`.site-${site}`).append(`<div class='training-days-lost'>Training days lost: ${days_lost_avg.toFixed(2)}</div>`);
// }
summary_button = $("<button class='auto-generated'>Toggle Summary</button>").click(() => {
$(table).find(".rota-day").toggleClass("hidden");
$(table).find(".worker-summary").toggle();
});
$(table).before($(`<span>Total summed shift diff: ${total_summed_shift_diffs}</span><br/>`));
$(table).before($(`<span>Total summed shift diff: ${total_summed_shift_diffs_abs}</span><br/>`));
$(table).before(summary_button);
});
return global_shifts
}
shifts = generateExtra();
console.log(shifts)
if (shifts.size < 1) {
shifts = $("#shifts-container").data("shifts");
}
console.log(shifts)
oshifts = Array.from(shifts).sort()
$("body").append("<div><table class='tsummary'></table></div>")
$("table.tsummary").append("<tr class='header'></tr>");
h = $("table.tsummary tr.header");
h.append(`<th>Worker</th>`);
h.append(`<th>Site</th>`);
h.append(`<th>FTE</th>`);
h.append(`<th>FTE (adj)</th>`);
oshifts.forEach((s) => {
h.append(`<th>${s}</th>`);
});
$(".table-div .worker-row .worker").each((n, tr) => {
//worker_td = $(tr).find("td").first();
jtr = $(tr);
row = $("<tr></tr>");
row.append(`<td>${jtr.data("worker")}</td>`)
row.append(`<td>${jtr.data("site")}</td>`)
row.append(`<td>${jtr.data("fte")}</td>`)
row.append(`<td>${parseFloat(jtr.data("fte_adj")).toFixed(2)}</td>`)
shift_counts = jtr.data("shift-counts")
shift_targets = jtr.data("worker-targets")
oshifts.forEach((s) => {
if (s in shift_targets && shift_targets[s] > 0) {
if (s in shift_counts) {
c = shift_counts[s];
} else {
c = 0;
}
row.append(`<td>${c} (${shift_targets[s].toFixed(2)})</td>`)
} else {
row.append(`<td>-</td>`)
}
})
$("table.tsummary").append(row)
})
//$("table.summary")
// $("body").prepend("<div id='global-settings'>Settings:<br />Customise training days lost per shift (you need to press recalculate for changes to take effect)<form></form></div>")
// shifts.forEach((s) => {
// disable = false;
// if(s.includes("twilight")) {
// value = 0.5;
// } else if(s == "night_weekend") {
// value = 4 / 3;
// } else if(s == "night_weekday") {
// value = 5 / 4;
// } else {
// disable = true;
// value = 0;
// }
// if(!disable) {
// $("#global-settings form").append(`<span class='tdl-settings'><label for="${s}-tdl">${s}</label><input type="number" id="${s}-tdl" name="${s}-tdl" value='${value}'></span>`);
// }
// })
// $("#global-settings").append($("<button >Recalculate</button>").click(() => {
// generateExtra();
// }))
// generateExtra();
$(".table-div + pre").each((n, pre) => {
$(pre).before($("<button class='shift-breakdown-button'>Show shift breakdown</button>").click(() => {
$(pre).toggle();
}));
})
$("td").hover((e) => {
//start hover
worker_td = $(e.target).closest('tr').find('td:first')[0]
pair = worker_td.dataset.pair;
index = $(e.target).index() - 1;
if (pair > 0) {
$(`td[data-pair=${pair}]`).each((n, el) => {
$($(el).closest('tr').find('td').get(index)).addClass("pair-match")
})
}
if (e.target.dataset.shift != undefined && e.target.dataset.shift.length > 0) {
$(`td[data-shift=${e.target.dataset.shift}]`).addClass("shift-highlight")
console.log(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`)
console.log($(e.target).closest('tr').find(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`))
$(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`).closest('tr').find(`td[data-shift=${e.target.dataset.shift}]`).addClass("remote-site-match")
// console.log(e.target.dataset.shiftRemoteSite , worker_td.dataset.remoteSite)
// if (e.target.dataset.shiftRemoteSite == worker_td.dataset.remoteSite) {
// $(`td[data-shift-remote-site=${e.target.dataset.shiftRemoteSite}]`).addClass("remote-site-match")
// }
}
}, (e) => {
//end hover
$("td.pair-match").removeClass("pair-match")
$("td.shift-highlight").removeClass("shift-highlight")
$("td.remote-site-match").removeClass("remote-site-match")
})
$("#rota-table tr td:first-child").each((n, td) => {
$(td).click(() => {
viewWorker(td);
})
})
function viewWorker(worker) {
ds = worker.dataset;
dlg = $(`<div class='dialog' title='${ds.worker}'>
Site: ${ds.site}</br>
FTE: ${ds.fte} (${ds.fte_adj})</br>
Start date: ${ds.start_date}</br>
End date: ${ds.end_date}</br>
Non working days: ${ds.nwds}</br>
Remote site: ${ds.remoteSite}
</div>`)
$("body").append(dlg)
console.log(ds.shiftCounts)
shifts = JSON.parse(ds.shiftCounts)
shift_targets = JSON.parse(ds.workerTargets)
shift_diffs = JSON.parse(ds.shiftDiff)
let shift_count_table = document.createElement("table")
shift_count_table.classList.add("shift-table")
let header_row = shift_count_table.createTHead().insertRow();
header_row.insertCell().appendChild(document.createTextNode("Shift"))
header_row.insertCell().appendChild(document.createTextNode("Count"))
header_row.insertCell().appendChild(document.createTextNode("Target"))
header_row.insertCell().appendChild(document.createTextNode("Diffs"))
table_body = shift_count_table.createTBody()
for (const shift in shifts) {
let row = table_body.insertRow();
row.insertCell().appendChild(document.createTextNode(shift))
row.insertCell().appendChild(document.createTextNode(shifts[shift]))
row.insertCell().appendChild(document.createTextNode(shift_targets[shift].toPrecision(2)))
row.insertCell().appendChild(document.createTextNode(shift_diffs[shift].toPrecision(2)))
//console.log(shift, shifts[shift]);
}
dlg.get(0).appendChild(shift_count_table)
dlg.dialog()
console.log(worker);
}
$("#export-div").append($("#main-table").clone().attr("id", "gen-table").addClass("transposed"))
$("#gen-table").each(function() {
var $this = $(this);
var newrows = [];
//$this.find("th.site-title").parent().remove();
$this.find("tr").each(function(rowToColIndex){
$(this).find("td, th").each(function(colToRowIndex){
if(newrows[colToRowIndex] === undefined) { newrows[colToRowIndex] = $("<tr></tr>"); }
while(newrows[colToRowIndex].find("td, th").length < rowToColIndex){
newrows[colToRowIndex].append($("<td></td>"));
}
newrows[colToRowIndex].append($(this));
});
});
$this.find("tr").remove();
$.each(newrows, function(){
$this.append(this);
});
$this.find("td.rota-day").each((n, el) => {
console.log(el)
$(el).text(el.dataset.shift)
});
});
+1
View File
@@ -0,0 +1 @@
../timetable.js
+22 -3
View File
@@ -5,7 +5,6 @@ html {
table {
/* margin-top: 50px; */
padding-top: 100px;
text-align: center;
/* table-layout : fixed; */
/* width:150px */
@@ -18,6 +17,10 @@ table {
}
#main-table {
padding-top: 100px;
}
.shift-table {
padding-top: 10px;
}
@@ -186,7 +189,7 @@ th.bank-holiday {
.th {}
.table-div th.worker,
td.worker {
.table-div td.worker {
position: absolute;
width: 200px;
max-width: 200px;
@@ -243,10 +246,22 @@ summary h2 {
background-color: yellow;
}
.large-shift-diff::before {
td.large-shift-diff-pos::before {
content: "*";
color: red;
}
td.large-shift-diff-neg::before {
content: "*";
color: green;
}
tr.large-shift-diff-pos td {
border-bottom: solid 1px red;
border-top: solid 1px red;
}
tr.large-shift-diff-neg td {
border-bottom: solid 1px green;
border-top: solid 1px green;
}
/* table.transposed {
@@ -298,4 +313,8 @@ table.transposed .bank-holiday {
table.transposed th.bank-holiday {
color: darkblue;
}
.target-assigned.no-colour {
background-color: white !important;
}
+151 -39
View File
@@ -146,7 +146,6 @@ function generateExtra() {
bank_holidays = shift_tds.filter(".bank-holiday").filter(function (i) {
return $(this).attr('data-shift') != "";
});
console.log("BH", bank_holidays.length)
bank_holidays_worked = bank_holidays.length;
@@ -156,6 +155,8 @@ function generateExtra() {
total_summed_shift_diffs = total_summed_shift_diffs + summed_shift_diff;
total_summed_shift_diffs_abs = total_summed_shift_diffs_abs + Math.abs(summed_shift_diff);
worker_td.get(0).dataset.shiftDiffSummed = summed_shift_diff.toPrecision(2);
worker_td.after(`<div class='worker-summary auto-generated'>
<span>Total shifts: ${total_shifts}, </span>
@@ -165,9 +166,13 @@ function generateExtra() {
</div>`)
if (Math.abs(summed_shift_diff) >= 1) {
worker_td.addClass("large-shift-diff")
$(tr).find(".shift-diff-span").addClass("large-shift-diff")
if (summed_shift_diff >= 1) {
worker_td.addClass("large-shift-diff-pos")
$(tr).find(".shift-diff-span").addClass("large-shift-diff-pos")
} else if (summed_shift_diff <= -1) {
worker_td.addClass("large-shift-diff-neg")
$(tr).find(".shift-diff-span").addClass("large-shift-diff-neg")
}
})
@@ -176,12 +181,9 @@ function generateExtra() {
sites = {};
sites_days_lost = {}
for (const worker in workers) {
//console.log(worker, workers[worker]);
w = workers[worker];
if (w.fte == 100) {
//console.log(w.shift_types);
for (let shift of w.shift_types) {
//console.log(shift);
if (!(w.site in sites)) {
sites[w.site] = {};
@@ -234,16 +236,17 @@ function generateExtra() {
}
shifts = generateExtra();
console.log(shifts)
if (shifts.size < 1) {
shifts = $("#shifts-container").data("shifts");
}
console.log(shifts)
oshifts = Array.from(shifts).sort()
$("body").append("<div><table class='tsummary'></table></div>")
$("body").append("<div><button id='show-colour-diff'>Show colour diff</button><table class='tsummary'></table></div>")
$("#show-colour-diff").on("click", (evt) => {
$("table.tsummary").find(`td.target-assigned`).toggleClass("no-colour");
});
$("table.tsummary").append("<tr class='header'></tr>");
h = $("table.tsummary tr.header");
@@ -269,6 +272,13 @@ $(".table-div .worker-row .worker").each((n, tr) => {
row.append(`<td>${jtr.data("fte")}</td>`)
row.append(`<td>${parseFloat(jtr.data("fte_adj")).toFixed(2)}</td>`)
if (jtr.data("shift-diff-summed") >= 1) {
row.addClass("large-shift-diff-pos")
} else if (jtr.data("shift-diff-summed") <= -1) {
row.addClass("large-shift-diff-neg")
}
shift_counts = jtr.data("shift-counts")
shift_targets = jtr.data("worker-targets")
@@ -279,7 +289,7 @@ $(".table-div .worker-row .worker").each((n, tr) => {
} else {
c = 0;
}
row.append(`<td>${c} (${shift_targets[s].toFixed(2)})</td>`)
row.append(`<td class="target-assigned ${s}" data-assigned=${c} data-target=${shift_targets[s].toFixed(2)} data-diff=${shift_targets[s].toFixed(2)-c}>${c} (${shift_targets[s].toFixed(2)})</td>`)
} else {
row.append(`<td>-</td>`)
@@ -289,6 +299,77 @@ $(".table-div .worker-row .worker").each((n, tr) => {
$("table.tsummary").append(row)
})
shift_max_diff_map = {};
shift_min_diff_map = {};
oshifts.forEach((s) => {
shift_max_diff_map[s] = 0;
shift_min_diff_map[s] = 0;
$("table.tsummary").find(`td.target-assigned.${s}`).each((n, td) => {
shift_max_diff_map[s] = Math.max(shift_max_diff_map[s], td.dataset.diff)
shift_min_diff_map[s] = Math.min(shift_min_diff_map[s], td.dataset.diff)
})
});
oshifts.forEach((shift) => {
$("table.tsummary").find(`td.target-assigned.${shift}`).each((n, td) => {
diff = td.dataset.diff;
if (diff > 0) {
max_diff = shift_max_diff_map[shift];
var h = 120 - Math.floor((max_diff - diff) * 120 / max_diff);
var s = diff / max_diff;
//var s = 0.5;
var v = 1;
$(td).css({
backgroundColor: hsv2rgb(h, s, 1)
})
} else {
min_diff = shift_min_diff_map[shift]
var h = Math.floor((min_diff - diff) * 120 / min_diff);
var s = diff / min_diff;
//var s = 0.5;
var v = 1;
$(td).css({
backgroundColor: hsv2rgb(h, s, 1)
})
}
$(td).addClass("no-colour")
//s = Math.max(shift_max_diff_map[s], td.dataset.diff)
//shift_min_diff_map[s] = Math.min(shift_max_diff_map[s], td.dataset.diff)
})
});
oshifts.forEach((shift) => {
button = $(`<button data-shift='${shift}'>${shift}</button>`)
button.on("click", (evt) => {
console.log(evt.target.dataset.shift)
$("#shift-timetable-div").empty();
table = $("<table>");
$("table#main-table th.date").each((n, th) => {
row = $("<tr>")
row.append(`<td>${th.dataset.date}</td>`);
$(`table#main-table td[data-date='${th.dataset.date}'][data-shift='${evt.target.dataset.shift}']`).each((n, td) => {
//row.append(`<td>${}</td>`)
row.append($(td).closest("tr").children("td:first").clone())
});
table.append(row)
});
$("#shift-timetable-div").append(table);
})
$("#shift-timetable-options").append(button)
});
//$("table.summary")
// $("body").prepend("<div id='global-settings'>Settings:<br />Customise training days lost per shift (you need to press recalculate for changes to take effect)<form></form></div>")
@@ -340,11 +421,8 @@ $("td").hover((e) => {
if (e.target.dataset.shift != undefined && e.target.dataset.shift.length > 0) {
$(`td[data-shift=${e.target.dataset.shift}]`).addClass("shift-highlight")
console.log(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`)
console.log($(e.target).closest('tr').find(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`))
$(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`).closest('tr').find(`td[data-shift=${e.target.dataset.shift}]`).addClass("remote-site-match")
// console.log(e.target.dataset.shiftRemoteSite , worker_td.dataset.remoteSite)
// if (e.target.dataset.shiftRemoteSite == worker_td.dataset.remoteSite) {
// $(`td[data-shift-remote-site=${e.target.dataset.shiftRemoteSite}]`).addClass("remote-site-match")
// }
@@ -373,11 +451,11 @@ function viewWorker(worker) {
Start date: ${ds.start_date}</br>
End date: ${ds.end_date}</br>
Non working days: ${ds.nwds}</br>
Remote site: ${ds.remoteSite}
Remote site: ${ds.remoteSite}</br>
Summed shift diff: ${ds.shiftDiffSummed}
</div>`)
$("body").append(dlg)
console.log(ds.shiftCounts)
shifts = JSON.parse(ds.shiftCounts)
shift_targets = JSON.parse(ds.workerTargets)
shift_diffs = JSON.parse(ds.shiftDiff)
@@ -399,39 +477,73 @@ function viewWorker(worker) {
row.insertCell().appendChild(document.createTextNode(shifts[shift]))
row.insertCell().appendChild(document.createTextNode(shift_targets[shift].toPrecision(2)))
row.insertCell().appendChild(document.createTextNode(shift_diffs[shift].toPrecision(2)))
//console.log(shift, shifts[shift]);
}
dlg.get(0).appendChild(shift_count_table)
dlg.dialog()
console.log(worker);
}
$("#export-div").append($("#main-table").clone().attr("id", "gen-table").addClass("transposed"))
$("#gen-table").each(function() {
var $this = $(this);
var newrows = [];
//$this.find("th.site-title").parent().remove();
$this.find("tr").each(function(rowToColIndex){
$(this).find("td, th").each(function(colToRowIndex){
if(newrows[colToRowIndex] === undefined) { newrows[colToRowIndex] = $("<tr></tr>"); }
while(newrows[colToRowIndex].find("td, th").length < rowToColIndex){
newrows[colToRowIndex].append($("<td></td>"));
}
newrows[colToRowIndex].append($(this));
});
});
$this.find("tr").remove();
$.each(newrows, function(){
$this.append(this);
$("#gen-table").each(function () {
var $this = $(this);
var newrows = [];
//$this.find("th.site-title").parent().remove();
$this.find("tr").each(function (rowToColIndex) {
$(this).find("td, th").each(function (colToRowIndex) {
if (newrows[colToRowIndex] === undefined) {
newrows[colToRowIndex] = $("<tr></tr>");
}
while (newrows[colToRowIndex].find("td, th").length < rowToColIndex) {
newrows[colToRowIndex].append($("<td></td>"));
}
newrows[colToRowIndex].append($(this));
});
});
$this.find("tr").remove();
$.each(newrows, function () {
$this.append(this);
});
$this.find("td.rota-day").each((n, el) => {
console.log(el)
$(el).text(el.dataset.shift)
});
});
$this.find("td.rota-day").each((n, el) => {
$(el).text(el.dataset.shift)
});
});
function hsv2rgb(h, s, v) {
// adapted from http://schinckel.net/2012/01/10/hsv-to-rgb-in-javascript/
var rgb, i, data = [];
if (s === 0) {
rgb = [v, v, v];
} else {
h = h / 60;
i = Math.floor(h);
data = [v * (1 - s), v * (1 - s * (h - i)), v * (1 - s * (1 - (h - i)))];
switch (i) {
case 0:
rgb = [v, data[2], data[0]];
break;
case 1:
rgb = [data[1], v, data[0]];
break;
case 2:
rgb = [data[0], v, data[2]];
break;
case 3:
rgb = [data[0], data[1], v];
break;
case 4:
rgb = [data[2], data[0], v];
break;
default:
rgb = [v, data[0], data[1]];
break;
}
}
return '#' + rgb.map(function (x) {
return ("0" + Math.round(x * 255).toString(16)).slice(-2);
}).join('');
};
+24 -12
View File
@@ -26,6 +26,8 @@ from io import StringIO
import sys
from rich.pretty import pprint
from rich.progress import track
from rich.panel import Panel
import logging
@@ -177,9 +179,9 @@ class RotaBuilder(object):
SHIFT_BOUNDS=SHIFT_BOUNDS,
):
print(f"Start time: {datetime.datetime.now()}")
print(f"Use previous shifts {use_previous_shifts}")
print(f"Use previous shifts (balance_extra) {use_shift_balance_extra}")
console.print(Panel(f"""[white]
{locals()}
""", style="green", title="Generating Rota"), justify="left")
self.SHIFT_BOUNDS = SHIFT_BOUNDS
self.shifts: List[SingleShift] = [] # type List[SingleShift]
@@ -203,7 +205,7 @@ class RotaBuilder(object):
self.rota_days_length = len(self.weeks) * 7
self.rota_end_date = self.start_date + datetime.timedelta(self.rota_days_length)
print(
console.print(
f"Weeks to rota: {weeks_to_rota} ({self.start_date} - {self.rota_end_date})"
)
@@ -961,7 +963,7 @@ class RotaBuilder(object):
# - self.model.night_per_site_t2[week, block, site]
# == self.model.night_per_site[week, block, site] - 1
# )
for site in self.sites:
for site in track(self.sites, description="Generating site balance constraints..."):
for shift in self.get_shifts_with_constraint("balance_across_groups"):
if site in shift.sites:
block = shift.name
@@ -985,7 +987,7 @@ class RotaBuilder(object):
weeks_days = self.get_week_day_combinations()
for week in self.weeks:
for week in track(self.weeks, description="Generating week constraints..."):
for shift_name in self.shifts_to_assign_or_force_as_blocks():
shift = self.get_shift_by_name(shift_name)
for worker in self.get_workers_for_shift(shift):
@@ -1073,6 +1075,8 @@ class RotaBuilder(object):
# Most of our constraints apply per worker
for worker in self.workers:
console.rule(f"Generate worker constraints: [bold blue]{worker.name}[/bold blue]")
logging.debug(f"Generate worker constraints: {worker.name}")
if self.constraint_options["minimise_shift_diffs"]:
self.model.constraints.add(
@@ -1097,6 +1101,7 @@ class RotaBuilder(object):
except ValueError:
# Occurs if there are no shifts within a defined block
# TODO: test if this breaks (and we should check rathar than except)
logging.debug(f"Failed to constrain max_shifts_per_month for worker: {worker.name}")
pass
for constraint_dict in self.constraint_options["avoid_shifts_by_grades"]:
@@ -1161,7 +1166,7 @@ class RotaBuilder(object):
# Balance shifts within required limits (set by balance_offset)
# If balance_offset is too restrictive (for the rota length)
# no solution will be possible
for shift in self.get_shifts():
for shift in track(self.get_shifts(), description="Generate shift balance constraints"):
if (
worker.site in shift.sites
): # Each site specfies which sites self.workers can fullfill it
@@ -1777,7 +1782,7 @@ class RotaBuilder(object):
# def get_pre_may(week_days, max_pre)
for n in range(len(weeks_days)):
for n in track(range(len(weeks_days)), description="Generate week/day constraints"):
week, day = weeks_days[n]
@@ -2358,7 +2363,7 @@ class RotaBuilder(object):
self.workers_id_map = {}
self.workers_name_map = {}
for worker in self.workers:
for worker in track(self.workers, description="Building workers"):
worker.load_rota(self)
wid = worker.id
if wid in self.workers_id_map:
@@ -2377,7 +2382,7 @@ class RotaBuilder(object):
self.full_time_equivalent_sites = {}
self.workers_at_sites = {}
for site in self.sites:
for site in track(self.sites, description="Generating site workers"):
self.workers_at_sites[site] = [w for w in self.workers if w.site == site]
self.full_time_equivalent_sites[site] = sum(
w.fte_adj for w in self.workers if w.site == site
@@ -2920,9 +2925,9 @@ class RotaBuilder(object):
for week, day in self.get_week_day_combinations():
d = self.start_date + datetime.timedelta(n)
th_class = ""
th_class = "date"
if d in bank_holiday_map:
th_class = "bank-holiday"
th_class = th_class + " bank-holiday"
date_row.append(
f"<th title='{d}' class='{th_class}' data-date='{d}'>Week {week}: {day}</th>"
@@ -3140,6 +3145,13 @@ class RotaBuilder(object):
<div id="export-div">
<div>
</details>
<details>
<summary><h2>Shift timetables</h2></summary>
<div id="shift-timetable-options">
</div>
<div id="shift-timetable-div">
<div>
</details>
</div
</body>
"""