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(`