.
This commit is contained in:
@@ -2,6 +2,7 @@ sample.txt
|
||||
*.html
|
||||
*.log
|
||||
*.pyc
|
||||
*.bak
|
||||
current.log
|
||||
extended proc nights only.log
|
||||
no nights.log
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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(`<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)
|
||||
});
|
||||
});
|
||||
+55
-22
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user