diff --git a/gen.py b/gen.py index de09de4..8b8215e 100644 --- a/gen.py +++ b/gen.py @@ -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}") diff --git a/output/individual_shifts/timetable.css b/output/individual_shifts/timetable.css deleted file mode 100644 index dac3438..0000000 --- a/output/individual_shifts/timetable.css +++ /dev/null @@ -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; - -} \ No newline at end of file diff --git a/output/individual_shifts/timetable.css b/output/individual_shifts/timetable.css new file mode 120000 index 0000000..8c357d2 --- /dev/null +++ b/output/individual_shifts/timetable.css @@ -0,0 +1 @@ +../timetable.css \ No newline at end of file diff --git a/output/individual_shifts/timetable.js b/output/individual_shifts/timetable.js deleted file mode 100644 index 82ca10a..0000000 --- a/output/individual_shifts/timetable.js +++ /dev/null @@ -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(`
- Total shifts: ${total_shifts}, - Weekends: ${weekends_worked}, - Bank holidays: ${bank_holidays_worked}, - Summed shift diff: ${summed_shift_diff.toPrecision(2)}, - -
`) - - 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("
Average shifts for a 100% FTE trainee
"); - - // for(const site in sites) { - // $(table).find(".site-summary").append(`
${site}
`); - - // for(const shift in sites[site]) { - // average = mean(sites[site][shift]) - // $(table).find(`.site-${site}`).append(`
${shift}: ${average.toFixed(2)}
`); - // } - - // days_lost_avg = mean(sites_days_lost[site]); - // $(table).find(`.site-${site}`).append(`
Training days lost: ${days_lost_avg.toFixed(2)}
`); - // } - - summary_button = $("").click(() => { - $(table).find(".rota-day").toggleClass("hidden"); - $(table).find(".worker-summary").toggle(); - - }); - - - $(table).before($(`Total summed shift diff: ${total_summed_shift_diffs}
`)); - $(table).before($(`Total summed shift diff: ${total_summed_shift_diffs_abs}
`)); - $(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("
") - -$("table.tsummary").append(""); -h = $("table.tsummary tr.header"); -h.append(`Worker`); -h.append(`Site`); -h.append(`FTE`); -h.append(`FTE (adj)`); - - -oshifts.forEach((s) => { - h.append(`${s}`); - -}); - - - -$(".table-div .worker-row .worker").each((n, tr) => { - //worker_td = $(tr).find("td").first(); - jtr = $(tr); - row = $(""); - row.append(`${jtr.data("worker")}`) - row.append(`${jtr.data("site")}`) - row.append(`${jtr.data("fte")}`) - row.append(`${parseFloat(jtr.data("fte_adj")).toFixed(2)}`) - - 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(`${c} (${shift_targets[s].toFixed(2)})`) - - } else { - row.append(`-`) - } - }) - - $("table.tsummary").append(row) -}) - -//$("table.summary") - -// $("body").prepend("
Settings:
Customise training days lost per shift (you need to press recalculate for changes to take effect)
") - -// 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(``); -// } - - -// }) -// $("#global-settings").append($("").click(() => { -// generateExtra(); -// })) - -// generateExtra(); - -$(".table-div + pre").each((n, pre) => { - - $(pre).before($("").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 = $(`
- Site: ${ds.site}
- FTE: ${ds.fte} (${ds.fte_adj})
- Start date: ${ds.start_date}
- End date: ${ds.end_date}
- Non working days: ${ds.nwds}
- Remote site: ${ds.remoteSite} -
`) - $("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] = $(""); } - while(newrows[colToRowIndex].find("td, th").length < rowToColIndex){ - newrows[colToRowIndex].append($("")); - } - 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) - }); - }); \ No newline at end of file diff --git a/output/individual_shifts/timetable.js b/output/individual_shifts/timetable.js new file mode 120000 index 0000000..78957ab --- /dev/null +++ b/output/individual_shifts/timetable.js @@ -0,0 +1 @@ +../timetable.js \ No newline at end of file diff --git a/output/timetable.css b/output/timetable.css index dac3438..8fda8b5 100644 --- a/output/timetable.css +++ b/output/timetable.css @@ -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; } \ No newline at end of file diff --git a/output/timetable.js b/output/timetable.js index 82ca10a..a980ff9 100644 --- a/output/timetable.js +++ b/output/timetable.js @@ -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(`
Total shifts: ${total_shifts}, @@ -165,9 +166,13 @@ function generateExtra() {
`) - 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("
") +$("body").append("
") +$("#show-colour-diff").on("click", (evt) => { + $("table.tsummary").find(`td.target-assigned`).toggleClass("no-colour"); +}); $("table.tsummary").append(""); h = $("table.tsummary tr.header"); @@ -269,6 +272,13 @@ $(".table-div .worker-row .worker").each((n, tr) => { row.append(`${jtr.data("fte")}`) row.append(`${parseFloat(jtr.data("fte_adj")).toFixed(2)}`) + 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(`${c} (${shift_targets[s].toFixed(2)})`) + row.append(`${c} (${shift_targets[s].toFixed(2)})`) } else { row.append(`-`) @@ -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.on("click", (evt) => { + console.log(evt.target.dataset.shift) + $("#shift-timetable-div").empty(); + table = $(""); + $("table#main-table th.date").each((n, th) => { + row = $("") + row.append(``); + $(`table#main-table td[data-date='${th.dataset.date}'][data-shift='${evt.target.dataset.shift}']`).each((n, td) => { + + //row.append(``) + 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("
Settings:
Customise training days lost per shift (you need to press recalculate for changes to take effect)
") @@ -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}
End date: ${ds.end_date}
Non working days: ${ds.nwds}
- Remote site: ${ds.remoteSite} + Remote site: ${ds.remoteSite}
+ Summed shift diff: ${ds.shiftDiffSummed} `) $("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] = $(""); } - while(newrows[colToRowIndex].find("td, th").length < rowToColIndex){ - newrows[colToRowIndex].append($("")); - } - 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] = $(""); + } + while (newrows[colToRowIndex].find("td, th").length < rowToColIndex) { + newrows[colToRowIndex].append($("")); + } + 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) - }); - }); \ No newline at end of file + $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(''); +}; \ No newline at end of file diff --git a/rota/shifts.py b/rota/shifts.py index d0a42fa..3de709c 100644 --- a/rota/shifts.py +++ b/rota/shifts.py @@ -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"" @@ -3140,6 +3145,13 @@ class RotaBuilder(object):
+
+

Shift timetables

+
+
+
+
+
"""
${th.dataset.date}${}
Week {week}: {day}