diff --git a/.gitignore b/.gitignore index 84a4637..ab16c17 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ sample.txt *.html *.log *.pyc +*.bak current.log extended proc nights only.log no nights.log diff --git a/gen.py b/gen.py index b78194f..de09de4 100644 --- a/gen.py +++ b/gen.py @@ -25,11 +25,10 @@ from rota.workers import ( ) -suspend_on_finish = False +suspend_on_finish = True solve = True time_to_run = 258500 time_to_run = 36000 -time_to_run = 200 # allow = 5000 ratio = 0.05 @@ -39,145 +38,145 @@ start_date = datetime.date(2022, 9, 5) Rota = RotaBuilder(start_date, weeks_to_rota=26) Rota.constraint_options["balance_weekends"] = True -Rota.constraint_options["max_night_frequency"] = 3 -Rota.constraint_options["max_weekend_frequency"] = 3 +Rota.constraint_options["max_night_frequency"] = 4 +Rota.constraint_options["max_weekend_frequency"] = 4 # Rota.constraint_options["avoid_st2_first_month"] = True Rota.add_shifts( - #SingleShift( - # sites=("exeter", "exeter twilights and weekends"), - # name="exeter_twilight", - # length=12.5, - # days=days[:5], - # balance_offset=3, - # 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, - #), - #SingleShift( - # sites=("torbay", "torbay twilights"), - # name="torbay_twilight", - # length=12.5, - # days=days[:4], - # balance_offset=5, - # assign_as_block=True, - #), + SingleShift( + sites=("exeter", "exeter twilights and weekends"), + name="exeter_twilight", + length=12.5, + days=days[:5], + #balance_offset=3, + 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, + ), + SingleShift( + sites=("torbay", "torbay twilights"), + name="torbay_twilight", + length=12.5, + days=days[:4], + #balance_offset=4, + assign_as_block=True, + ), SingleShift( sites=("plymouth", "plymouth twilights"), name="plymouth_twilight", length=12.5, days=days[:5], - balance_offset=2, + #balance_offset=4, workers_required=1, constraint=[{"name": "max_shifts_per_week", "options": 2}], ), - #SingleShift( - # sites=("exeter", "exeter twilights and weekends"), - # name="weekend_exeter", - # length=12.5, - # days=days[5:], - # balance_offset=4, - # 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},], - #), - #SingleShift( - # sites=("truro",), - # name="weekend_truro", - # length=12.5, - # days=days[4:], - # balance_offset=4, - # rota_on_nwds=True, - # force_as_block=True, - # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], - # # force_as_block_unless_nwd=True - #), - #SingleShift( - # sites=("torbay",), - # name="weekend_torbay", - # length=12.5, - # days=days[4:], - # balance_offset=4, - # rota_on_nwds=True, - # force_as_block=True, - # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], - # # force_as_block_unless_nwd=True - #), - #SingleShift( - # sites=("plymouth",), - # name="weekend_plymouth1", - # length=8, - # days=days[5:], - # balance_offset=4, - # workers_required=1, - # rota_on_nwds=True, - # force_as_block=True, - # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], - #), - #SingleShift( - # sites=("plymouth",), - # name="weekend_plymouth2", - # length=8, - # days=days[5:], - # balance_offset=4, - # workers_required=1, - # rota_on_nwds=True, - # force_as_block=True, - # constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], - #), - #SingleShift( - # sites=("plymouth", "plymouth twilights"), - # name="plymouth_bank_holidays", - # length=8, - # days=days[5:], - # balance_offset=2, - # workers_required=1, - # rota_on_nwds=True, - # force_as_block=False, - # bank_holidays_only=True, - #), - #SingleShift( - # sites=(sites), - # name="night_weekday", - # length=12.25, - # days=days[:4], - # balance_offset=4.9, - # balance_weighting=1, - # # hard_constrain_shift=False, - # workers_required=3, - # force_as_block=True, - # rota_on_nwds=True, - # constraint=[ - # {"name": "night"}, - # {"name": "pre", "options": 5}, - # {"name": "post", "options": 5}, - # {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, - # ], - #), - #SingleShift( - # sites=(sites), - # name="night_weekend", - # length=12.25, - # days=days[4:], - # balance_offset=3.9, - # balance_weighting=1, - # # hard_constrain_shift=False, - # workers_required=3, - # force_as_block=True, - # rota_on_nwds=True, - # constraint=[ - # {"name": "night"}, - # {"name": "pre", "options": 5}, - # {"name": "post", "options": 5}, - # {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, - # ], - #), + SingleShift( + sites=("exeter", "exeter twilights and weekends"), + name="weekend_exeter", + length=12.5, + days=days[5:], + #balance_offset=4, + 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},], + ), + SingleShift( + sites=("truro",), + name="weekend_truro", + length=12.5, + days=days[4:], + #balance_offset=4, + rota_on_nwds=True, + force_as_block=True, + constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], + # force_as_block_unless_nwd=True + ), + SingleShift( + sites=("torbay",), + name="weekend_torbay", + length=12.5, + days=days[4:], + #balance_offset=4, + rota_on_nwds=True, + force_as_block=True, + constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], + # force_as_block_unless_nwd=True + ), + SingleShift( + sites=("plymouth",), + name="weekend_plymouth1", + length=8, + days=days[5:], + #balance_offset=4, + workers_required=1, + rota_on_nwds=True, + force_as_block=True, + constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], + ), + SingleShift( + sites=("plymouth",), + name="weekend_plymouth2", + length=8, + days=days[5:], + #balance_offset=4, + workers_required=1, + rota_on_nwds=True, + force_as_block=True, + constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}], + ), + SingleShift( + sites=("plymouth", "plymouth twilights"), + name="plymouth_bank_holidays", + length=8, + days=days[5:], + #balance_offset=2, + workers_required=1, + rota_on_nwds=True, + force_as_block=False, + bank_holidays_only=True, + ), + SingleShift( + sites=(sites), + name="night_weekday", + length=12.25, + days=days[:4], + #balance_offset=4.9, + balance_weighting=1, + # hard_constrain_shift=False, + workers_required=3, + force_as_block=True, + rota_on_nwds=True, + constraint=[ + {"name": "night"}, + {"name": "pre", "options": 5}, + {"name": "post", "options": 5}, + {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, + ], + ), + SingleShift( + sites=(sites), + name="night_weekend", + length=12.25, + days=days[4:], + #balance_offset=3.9, + balance_weighting=1, + # hard_constrain_shift=False, + workers_required=3, + force_as_block=True, + rota_on_nwds=True, + constraint=[ + {"name": "night"}, + {"name": "pre", "options": 5}, + {"name": "post", "options": 5}, + {"name": "require_remote_site_presence_week", "options": ("plymouth", 1)}, + ], + ), ) Rota.add_grade_constraint_by_week([2], [1, 2, 3, 4], ["night_weekday", "night_weekend"]) @@ -290,7 +289,8 @@ if load_leave: solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10} start_time = time.time() -Rota.solve_shifts_individually(solver_options) +Rota.build_and_solve(solver_options) +#Rota.solve_shifts_individually(solver_options) #Rota.solve_model(options=solver_options) end_time = time.time() diff --git a/output/individual_shifts/timetable.css b/output/individual_shifts/timetable.css new file mode 100644 index 0000000..dac3438 --- /dev/null +++ b/output/individual_shifts/timetable.css @@ -0,0 +1,301 @@ +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.js b/output/individual_shifts/timetable.js new file mode 100644 index 0000000..82ca10a --- /dev/null +++ b/output/individual_shifts/timetable.js @@ -0,0 +1,437 @@ +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/rota/shifts.py b/rota/shifts.py index c082c3f..d0a42fa 100644 --- a/rota/shifts.py +++ b/rota/shifts.py @@ -1,5 +1,6 @@ from calendar import week import datetime +from distutils.log import debug import itertools from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type @@ -13,6 +14,7 @@ from pyomo.environ import * from pyomo.opt import SolverFactory from rota.workers import Worker +from rota.console import console import json @@ -25,6 +27,15 @@ import sys from rich.pretty import pprint +import logging + +logging.basicConfig( + filename="rota.log", + filemode="w", + format="%(name)s - %(levelname)s - %(message)s", + level=logging.DEBUG, +) + ShiftName = str DayStr = str WeekInt = int @@ -38,9 +49,12 @@ from govuk_bank_holidays.bank_holidays import BankHolidays bank_holidays = BankHolidays() bank_holiday_map = {} +logging.debug("Creating bank holiday map") for bank_holiday in bank_holidays.get_holidays(division="england-and-wales"): bank_holiday_map[bank_holiday["date"]] = bank_holiday["title"] +logging.debug(bank_holiday_map) + SHIFT_BOUNDS = { "bank_holiday": (0, 9), "shift_count": (0, 400), @@ -253,12 +267,16 @@ class RotaBuilder(object): self.ignore_valid_shifts = False def solve_model( - self, solver: str = "cbc", use_neos: bool = False, options: dict = {} + self, + solver: str = "cbc", + use_neos: bool = False, + options: dict = {}, + debug_if_fail: bool = False, ): - print("Setting up solver") + console.print("Setting up solver") self.opt = SolverFactory(solver) - print("Solving") + console.print("Solving") if use_neos: solver_manager = SolverManagerFactory("neos") # Solve using neos server # results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log") @@ -278,39 +296,47 @@ class RotaBuilder(object): self.results = results - print(results) - results.solver.status + console.print(f"Complete - outcome: {results.solver.status}") + + if results.solver.status != "ok" and debug_if_fail: + console.print(f"Attempting each shift individually") + self.solve_shifts_individually(options) + if not results.solver.status: sys.exit(0) def solve_shifts_individually(self, options, folder="individual_shifts"): shifts = self.shifts - + outcomes = {} for shift in shifts: + console.print(f"Testing shifts: {shift.name}") self.ignore_valid_shifts = True self.clear_shifts() self.add_shift(shift) self.build_and_solve(options) self.export_rota_to_html(filename=shift.name, folder=folder) - outcomes[shift.name] = (self.results.solver.status, self.results.solver.termination_condition) - pprint(outcomes) - - - pprint(outcomes) + outcomes[shift.name] = ( + self.results.solver.status, + self.results.solver.termination_condition, + ) + console.print(outcomes) def build_and_solve( - self, options={"ratio": 0.1, "seconds": 1000, "threads": 10}, solve=True + self, + options={"ratio": 0.1, "seconds": 1000, "threads": 10}, + solve=True, + debug_if_fail: bool = False, ): self.build_shifts() self.build_workers() self.build_model() if solve: - self.solve_model(options=options) + self.solve_model(options=options, debug_if_fail=debug_if_fail) def build_model(self): # Initialize model @@ -1078,7 +1104,7 @@ class RotaBuilder(object): set(self.get_shift_names()) ): if self.ignore_valid_shifts: - continue # Skip if we don't want to raise an error + continue # Skip if we don't want to raise an error else: raise InvalidShift( f"avoid shifts by grade constraint contains a non existent shift: {invalid_shifts}" @@ -1478,7 +1504,9 @@ class RotaBuilder(object): shift.name, week, worker.id ] for week in week_blocks - for shift in self.get_shifts_with_constraint("night") + for shift in self.get_shifts_with_constraint( + "night" + ) ) ) @@ -1747,7 +1775,7 @@ class RotaBuilder(object): # - #def get_pre_may(week_days, max_pre) + # def get_pre_may(week_days, max_pre) for n in range(len(weeks_days)): @@ -1955,7 +1983,7 @@ class RotaBuilder(object): ) for constraint_shift in self.get_shifts_with_constraints( - "pre", + "pre", ): for n in range(0, constraint_shift.constraint_options["pre"]): if day in constraint_shift.days: @@ -1965,7 +1993,10 @@ class RotaBuilder(object): worker.id, week, day, constraint_shift.name ] + sum( - pre_map[n][0] * self.model.works[w.id, pre_map[n][1], pre_map[n][2], shiftname] + pre_map[n][0] + * self.model.works[ + w.id, pre_map[n][1], pre_map[n][2], shiftname + ] for shiftname in self.get_shift_names_by_week_day( pre_map[n][1], pre_map[n][2] ) @@ -1973,7 +2004,7 @@ class RotaBuilder(object): for w in workers ) ) - #for constraint_shift in self.get_shifts_with_constraint( "pre"): + # for constraint_shift in self.get_shifts_with_constraint( "pre"): # to_sum = [] # for n in range( # 0, constraint_shift.constraint_options["pre"] @@ -1988,7 +2019,6 @@ class RotaBuilder(object): # w.id, wk, dy, shiftname # ]) - # if day in constraint_shift.days: # self.model.constraints.add( # 1 @@ -2403,7 +2433,10 @@ class RotaBuilder(object): set: Set of ShiftName """ - return set((week, day, shift_name) for shift_name in self.week_day_shifts_dict[(week, day)]) + return set( + (week, day, shift_name) + for shift_name in self.week_day_shifts_dict[(week, day)] + ) def get_shift_by_name(self, name: str) -> SingleShift: """ @@ -2738,7 +2771,7 @@ class RotaBuilder(object): return self.start_date + datetime.timedelta(weeks=week) # RESULTS - def export_rota_to_html(self, filename: str = "rota", folder = None): + def export_rota_to_html(self, filename: str = "rota", folder=None): if folder is not None: output_file = Path("output", folder, f"{filename}.html") else: