.
This commit is contained in:
@@ -11,7 +11,7 @@ sites = (
|
|||||||
"torbay",
|
"torbay",
|
||||||
"barnstaple",
|
"barnstaple",
|
||||||
"plymouth",
|
"plymouth",
|
||||||
"taunton",
|
#"taunton",
|
||||||
"proc",
|
"proc",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,26 +25,28 @@ from rota.workers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
suspend_on_finish = True
|
suspend_on_finish = False
|
||||||
solve = True
|
solve = True
|
||||||
time_to_run = 458500
|
time_to_run = 508500
|
||||||
#time_to_run = 2660000
|
#time_to_run = 2660000
|
||||||
#time_to_run = 458500
|
time_to_run = 400
|
||||||
# allow = 5000
|
# allow = 5000
|
||||||
ratio = 0.05
|
ratio = 0.01
|
||||||
#ratio = 0.9
|
#ratio = 0.9
|
||||||
|
|
||||||
|
|
||||||
start_date = datetime.date(2023, 3, 6)
|
start_date = datetime.date(2023, 3, 6)
|
||||||
|
#start_date = datetime.date(2023, 5, 1)
|
||||||
#start_date = start_date + datetime.timedelta(weeks=13)
|
#start_date = start_date + datetime.timedelta(weeks=13)
|
||||||
# start_date = datetime.date(2022, 10, 10)
|
# start_date = datetime.date(2022, 10, 10)
|
||||||
|
|
||||||
Rota = RotaBuilder(start_date, weeks_to_rota=26, balance_offset_modifier=1)
|
Rota = RotaBuilder(start_date, weeks_to_rota=6, balance_offset_modifier=1)
|
||||||
|
#Rota = RotaBuilder(start_date, weeks_to_rota=20, balance_offset_modifier=1)
|
||||||
Rota.constraint_options["balance_weekends"] = True
|
Rota.constraint_options["balance_weekends"] = True
|
||||||
Rota.constraint_options["max_night_frequency"] = 3 # 3
|
Rota.constraint_options["max_night_frequency"] = 3 # 3
|
||||||
Rota.constraint_options["max_night_frequency_week_exclusions"] = []
|
Rota.constraint_options["max_night_frequency_week_exclusions"] = []
|
||||||
Rota.constraint_options["max_weekend_frequency"] = 2
|
Rota.constraint_options["max_weekend_frequency"] = 2
|
||||||
Rota.constraint_options["hard_constrain_pair_separation"] = False
|
Rota.constraint_options["hard_constrain_pair_separation"] = True
|
||||||
# Rota.constraint_options["avoid_st2_first_month"] = True
|
# Rota.constraint_options["avoid_st2_first_month"] = True
|
||||||
|
|
||||||
Rota.add_shifts(
|
Rota.add_shifts(
|
||||||
@@ -57,7 +59,7 @@ Rota.add_shifts(
|
|||||||
constraint=[
|
constraint=[
|
||||||
{
|
{
|
||||||
"name": "max_shifts_per_week",
|
"name": "max_shifts_per_week",
|
||||||
"options": 1,
|
"options": 2,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -77,7 +79,7 @@ Rota.add_shifts(
|
|||||||
assign_as_block=True,
|
assign_as_block=True,
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("plymouth", "plymouth twilights"),
|
sites=("plymouth", "plymouth twilights", "plymouth twilights and weekends"),
|
||||||
name="plymouth_twilight",
|
name="plymouth_twilight",
|
||||||
length=12.5,
|
length=12.5,
|
||||||
days=days[:5],
|
days=days[:5],
|
||||||
@@ -93,7 +95,7 @@ Rota.add_shifts(
|
|||||||
balance_offset=3,
|
balance_offset=3,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 5}],
|
constraint=[{"name": "post", "options": 2}, {"name": "pre", "options": 2}],
|
||||||
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
|
# constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2},],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
@@ -119,33 +121,33 @@ Rota.add_shifts(
|
|||||||
# force_as_block_unless_nwd=True
|
# force_as_block_unless_nwd=True
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("plymouth",),
|
sites=("plymouth", "plymouth twilights and weekends"),
|
||||||
name="weekend_plymouth1",
|
name="weekend_plymouth1",
|
||||||
length=8,
|
length=8,
|
||||||
days=days[5:],
|
days=days[5:],
|
||||||
balance_offset=3,
|
balance_offset=2.9,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("plymouth",),
|
sites=("plymouth","plymouth twilights and weekends"),
|
||||||
name="weekend_plymouth2",
|
name="weekend_plymouth2",
|
||||||
length=8,
|
length=8,
|
||||||
days=days[5:],
|
days=days[5:],
|
||||||
balance_offset=3,
|
balance_offset=2.9,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=True,
|
force_as_block=True,
|
||||||
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
constraint=[{"name": "pre", "options": 2}, {"name": "post", "options": 2}],
|
||||||
),
|
),
|
||||||
SingleShift(
|
SingleShift(
|
||||||
sites=("plymouth", "plymouth twilights"),
|
sites=("plymouth", "plymouth twilights","plymouth twilights and weekends"),
|
||||||
name="plymouth_bank_holidays",
|
name="plymouth_bank_holidays",
|
||||||
length=8,
|
length=8,
|
||||||
days=days[5:],
|
days=days[5:],
|
||||||
balance_offset=3,
|
balance_offset=1,
|
||||||
workers_required=1,
|
workers_required=1,
|
||||||
rota_on_nwds=True,
|
rota_on_nwds=True,
|
||||||
force_as_block=False,
|
force_as_block=False,
|
||||||
@@ -156,7 +158,7 @@ Rota.add_shifts(
|
|||||||
name="night_weekday",
|
name="night_weekday",
|
||||||
length=12.25,
|
length=12.25,
|
||||||
days=days[:4],
|
days=days[:4],
|
||||||
balance_offset=4.9,
|
balance_offset=3.9,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
# hard_constrain_shift=False,
|
# hard_constrain_shift=False,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
@@ -176,7 +178,7 @@ Rota.add_shifts(
|
|||||||
name="night_weekend",
|
name="night_weekend",
|
||||||
length=12.25,
|
length=12.25,
|
||||||
days=days[4:],
|
days=days[4:],
|
||||||
balance_offset=3.9,
|
balance_offset=2.9,
|
||||||
balance_weighting=1,
|
balance_weighting=1,
|
||||||
# hard_constrain_shift=False,
|
# hard_constrain_shift=False,
|
||||||
workers_required=3,
|
workers_required=3,
|
||||||
@@ -236,7 +238,9 @@ if load_leave:
|
|||||||
nwd_end_date = Rota.rota_end_date
|
nwd_end_date = Rota.rota_end_date
|
||||||
|
|
||||||
if "[" in i:
|
if "[" in i:
|
||||||
|
print("Split nwd", worker_name, i)
|
||||||
a, b = i.split("[")[1][:-1].split("-")
|
a, b = i.split("[")[1][:-1].split("-")
|
||||||
|
print(f"{a=} {b=}")
|
||||||
nwd_start_date = datetime.datetime.strptime(a, "%d/%m/%y").date()
|
nwd_start_date = datetime.datetime.strptime(a, "%d/%m/%y").date()
|
||||||
nwd_end_date = datetime.datetime.strptime(b, "%d/%m/%y").date()
|
nwd_end_date = datetime.datetime.strptime(b, "%d/%m/%y").date()
|
||||||
|
|
||||||
@@ -326,6 +330,7 @@ solver_options = {"ratio": ratio, "seconds": time_to_run, "threads": 10}
|
|||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
Rota.build_and_solve(solver_options)
|
Rota.build_and_solve(solver_options)
|
||||||
|
#Rota.solve_shifts_by_block(solver_options, block_length=10)
|
||||||
#Rota.solve_shifts_individually(solver_options)
|
#Rota.solve_shifts_individually(solver_options)
|
||||||
# Rota.solve_model(options=solver_options)
|
# Rota.solve_model(options=solver_options)
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ def load_leave(Rota):
|
|||||||
|
|
||||||
workers = {}
|
workers = {}
|
||||||
|
|
||||||
download = s.get("https://docs.google.com/spreadsheets/d/e/2PACX-1vR18crGluN4wrL9n1sBdZ2u-JsvaLtQ6hr-R5CPUBOaKpL7_hBbhemyPSrFOhDuWxKlrjh-QUM0HrcD/pub?gid=0&single=true&output=csv")
|
headers = {
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Pragma": "no-cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
download = s.get("https://docs.google.com/spreadsheets/d/e/2PACX-1vR18crGluN4wrL9n1sBdZ2u-JsvaLtQ6hr-R5CPUBOaKpL7_hBbhemyPSrFOhDuWxKlrjh-QUM0HrcD/pub?gid=0&single=true&output=csv", headers=headers)
|
||||||
decoded_content = download.content.decode('utf-8')
|
decoded_content = download.content.decode('utf-8')
|
||||||
|
|
||||||
reader = csv.reader(decoded_content.splitlines(), delimiter=',')
|
reader = csv.reader(decoded_content.splitlines(), delimiter=',')
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
html {
|
||||||
|
|
||||||
|
/* color: red; */
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
/* margin-top: 50px; */
|
||||||
|
text-align: center;
|
||||||
|
/* table-layout : fixed; */
|
||||||
|
/* width:150px */
|
||||||
|
/* width: 200%; */
|
||||||
|
/* overflow-x: auto;
|
||||||
|
display: block;
|
||||||
|
position: relative; */
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-table {
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,
|
||||||
|
.table-div 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-assigned.no-colour {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,549 @@
|
|||||||
|
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') != "";
|
||||||
|
});
|
||||||
|
|
||||||
|
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.get(0).dataset.shiftDiffSummed = summed_shift_diff.toPrecision(2);
|
||||||
|
|
||||||
|
|
||||||
|
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 (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")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
sites = {};
|
||||||
|
sites_days_lost = {}
|
||||||
|
for (const worker in workers) {
|
||||||
|
w = workers[worker];
|
||||||
|
if (w.fte == 100) {
|
||||||
|
for (let shift of w.shift_types) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (shifts.size < 1) {
|
||||||
|
shifts = $("#shifts-container").data("shifts");
|
||||||
|
}
|
||||||
|
|
||||||
|
oshifts = Array.from(shifts).sort()
|
||||||
|
|
||||||
|
$("body").append("<div><button id='show-colour-diff'>Show colour diff</button><table class='tsummary'></table></div>")
|
||||||
|
$("#show-colour-diff").on("click", (evt) => {
|
||||||
|
$("table.tsummary").find(`td.target-assigned`).toggleClass("no-colour");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("table.tsummary").append("<tr class='header'></tr>");
|
||||||
|
h = $("table.tsummary tr.header");
|
||||||
|
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>`)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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 class="target-assigned ${s}" data-assigned=${c} data-target=${shift_targets[s].toFixed(2)} data-diff=${shift_targets[s].toFixed(2)-c}>${c} (${shift_targets[s].toFixed(2)})</td>`)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
row.append(`<td>-</td>`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$("table.tsummary").append(row)
|
||||||
|
})
|
||||||
|
|
||||||
|
shift_max_diff_map = {};
|
||||||
|
shift_min_diff_map = {};
|
||||||
|
|
||||||
|
oshifts.forEach((s) => {
|
||||||
|
shift_max_diff_map[s] = 0;
|
||||||
|
shift_min_diff_map[s] = 0;
|
||||||
|
$("table.tsummary").find(`td.target-assigned.${s}`).each((n, td) => {
|
||||||
|
shift_max_diff_map[s] = Math.max(shift_max_diff_map[s], td.dataset.diff)
|
||||||
|
shift_min_diff_map[s] = Math.min(shift_min_diff_map[s], td.dataset.diff)
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
oshifts.forEach((shift) => {
|
||||||
|
$("table.tsummary").find(`td.target-assigned.${shift}`).each((n, td) => {
|
||||||
|
diff = td.dataset.diff;
|
||||||
|
|
||||||
|
if (diff > 0) {
|
||||||
|
max_diff = shift_max_diff_map[shift];
|
||||||
|
var h = 120 - Math.floor((max_diff - diff) * 120 / max_diff);
|
||||||
|
var s = diff / max_diff;
|
||||||
|
//var s = 0.5;
|
||||||
|
var v = 1;
|
||||||
|
$(td).css({
|
||||||
|
backgroundColor: hsv2rgb(h, s, 1)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
min_diff = shift_min_diff_map[shift]
|
||||||
|
|
||||||
|
var h = Math.floor((min_diff - diff) * 120 / min_diff);
|
||||||
|
var s = diff / min_diff;
|
||||||
|
//var s = 0.5;
|
||||||
|
var v = 1;
|
||||||
|
$(td).css({
|
||||||
|
backgroundColor: hsv2rgb(h, s, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
$(td).addClass("no-colour")
|
||||||
|
//s = Math.max(shift_max_diff_map[s], td.dataset.diff)
|
||||||
|
//shift_min_diff_map[s] = Math.min(shift_max_diff_map[s], td.dataset.diff)
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
oshifts.forEach((shift) => {
|
||||||
|
button = $(`<button data-shift='${shift}'>${shift}</button>`)
|
||||||
|
|
||||||
|
button.on("click", (evt) => {
|
||||||
|
console.log(evt.target.dataset.shift)
|
||||||
|
$("#shift-timetable-div").empty();
|
||||||
|
table = $("<table>");
|
||||||
|
$("table#main-table th.date").each((n, th) => {
|
||||||
|
row = $("<tr>")
|
||||||
|
row.append(`<td>${th.dataset.date}</td>`);
|
||||||
|
$(`table#main-table td[data-date='${th.dataset.date}'][data-shift='${evt.target.dataset.shift}']`).each((n, td) => {
|
||||||
|
|
||||||
|
//row.append(`<td>${}</td>`)
|
||||||
|
row.append($(td).closest("tr").children("td:first").clone())
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
table.append(row)
|
||||||
|
});
|
||||||
|
$("#shift-timetable-div").append(table);
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#shift-timetable-options").append(button)
|
||||||
|
});
|
||||||
|
|
||||||
|
//$("table.summary")
|
||||||
|
|
||||||
|
// $("body").prepend("<div id='global-settings'>Settings:<br />Customise training days lost per shift (you need to press recalculate for changes to take effect)<form></form></div>")
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
|
||||||
|
|
||||||
|
$(`td[data-remote-site='${e.target.dataset.shiftRemoteSite}']`).closest('tr').find(`td[data-shift=${e.target.dataset.shift}]`).addClass("remote-site-match")
|
||||||
|
// 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}</br>
|
||||||
|
Summed shift diff: ${ds.shiftDiffSummed}
|
||||||
|
</div>`)
|
||||||
|
$("body").append(dlg)
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
dlg.get(0).appendChild(shift_count_table)
|
||||||
|
dlg.dialog()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#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) => {
|
||||||
|
$(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('');
|
||||||
|
};
|
||||||
+50
-10
@@ -3,6 +3,7 @@ import datetime
|
|||||||
from distutils.log import debug
|
from distutils.log import debug
|
||||||
import itertools
|
import itertools
|
||||||
from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type
|
from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type
|
||||||
|
import time
|
||||||
|
|
||||||
from pydantic import BaseModel, Extra, constr
|
from pydantic import BaseModel, Extra, constr
|
||||||
|
|
||||||
@@ -190,12 +191,6 @@ class RotaBuilder(object):
|
|||||||
self.use_shift_balance_extra = use_shift_balance_extra
|
self.use_shift_balance_extra = use_shift_balance_extra
|
||||||
self.use_bank_holiday_extra = use_bank_holiday_extra
|
self.use_bank_holiday_extra = use_bank_holiday_extra
|
||||||
|
|
||||||
self.unavailable_to_work = set()
|
|
||||||
self.unavailable_to_work_reason = {}
|
|
||||||
self.pref_not_to_work = {}
|
|
||||||
self.pref_not_to_work_reason = {}
|
|
||||||
self.work_requests: set[Tuple[str, WeekInt, DayStr, ShiftName]] = set()
|
|
||||||
self.work_requests_map = {}
|
|
||||||
|
|
||||||
self.workers: list[Worker] = []
|
self.workers: list[Worker] = []
|
||||||
|
|
||||||
@@ -243,7 +238,9 @@ class RotaBuilder(object):
|
|||||||
|
|
||||||
self.set_rota_dates(start_date, weeks_to_rota)
|
self.set_rota_dates(start_date, weeks_to_rota)
|
||||||
|
|
||||||
def set_rota_dates(self, start_date, weeks_to_rota):
|
def set_rota_dates(self, start_date: datetime.datetime, weeks_to_rota: int):
|
||||||
|
|
||||||
|
self.weeks_to_rota = weeks_to_rota
|
||||||
|
|
||||||
self.days = days
|
self.days = days
|
||||||
|
|
||||||
@@ -273,6 +270,12 @@ class RotaBuilder(object):
|
|||||||
self.week_day_date_map[(week, day)] = d
|
self.week_day_date_map[(week, day)] = d
|
||||||
n = n + 1
|
n = n + 1
|
||||||
|
|
||||||
|
self.unavailable_to_work = set()
|
||||||
|
self.unavailable_to_work_reason = {}
|
||||||
|
self.pref_not_to_work = {}
|
||||||
|
self.pref_not_to_work_reason = {}
|
||||||
|
self.work_requests: set[Tuple[str, WeekInt, DayStr, ShiftName]] = set()
|
||||||
|
self.work_requests_map = {}
|
||||||
|
|
||||||
def solve_model(
|
def solve_model(
|
||||||
self,
|
self,
|
||||||
@@ -282,7 +285,15 @@ class RotaBuilder(object):
|
|||||||
debug_if_fail: bool = False,
|
debug_if_fail: bool = False,
|
||||||
):
|
):
|
||||||
console.print("Setting up solver")
|
console.print("Setting up solver")
|
||||||
self.opt = SolverFactory(solver)
|
|
||||||
|
solver = "scip"
|
||||||
|
|
||||||
|
if solver == "scip":
|
||||||
|
self.opt = SolverFactory(solver, executable="scip")
|
||||||
|
else:
|
||||||
|
self.opt = SolverFactory(solver)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.print("Solving")
|
console.print("Solving")
|
||||||
if use_neos:
|
if use_neos:
|
||||||
@@ -313,6 +324,35 @@ class RotaBuilder(object):
|
|||||||
if not results.solver.status:
|
if not results.solver.status:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
def solve_shifts_by_block(self, options, block_length: int, folder="block_shifts"):
|
||||||
|
shifts = self.shifts
|
||||||
|
|
||||||
|
outcomes = {}
|
||||||
|
|
||||||
|
no_blocks = self.weeks_to_rota - block_length
|
||||||
|
|
||||||
|
original_start_date = self.start_date
|
||||||
|
|
||||||
|
for block in range(0, no_blocks):
|
||||||
|
start_time = time.time()
|
||||||
|
start_date = original_start_date + datetime.timedelta(weeks=block)
|
||||||
|
self.set_rota_dates(start_date = start_date,weeks_to_rota=block_length)
|
||||||
|
console.print(f"Testing block: start_date - {self.start_date} , length {self.weeks_to_rota} weeks")
|
||||||
|
self.clear_shifts()
|
||||||
|
self.add_shifts(*shifts)
|
||||||
|
self.build_and_solve(options)
|
||||||
|
self.export_rota_to_html(filename=f"{start_date}_{self.weeks_to_rota}", folder=folder)
|
||||||
|
end_time = time.time()
|
||||||
|
time_taken = end_time - start_time
|
||||||
|
|
||||||
|
outcomes[f"{start_date}_{self.weeks}"] = (
|
||||||
|
self.results.solver.status,
|
||||||
|
self.results.solver.termination_condition,
|
||||||
|
time_taken
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print(outcomes)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
def solve_shifts_individually(self, options, folder="individual_shifts"):
|
def solve_shifts_individually(self, options, folder="individual_shifts"):
|
||||||
shifts = self.shifts
|
shifts = self.shifts
|
||||||
@@ -2424,7 +2464,7 @@ class RotaBuilder(object):
|
|||||||
"""
|
"""
|
||||||
self.shifts.append(shift)
|
self.shifts.append(shift)
|
||||||
|
|
||||||
def add_shifts(self, *shifts: SingleShift) -> None:
|
def add_shifts(self, *shifts: SingleShift ) -> None:
|
||||||
"""Add multiple shifts
|
"""Add multiple shifts
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -2659,7 +2699,7 @@ class RotaBuilder(object):
|
|||||||
""" """ """
|
""" """ """
|
||||||
return self.shift_names
|
return self.shift_names
|
||||||
|
|
||||||
def get_week_block_iterator(self, block_length):
|
def get_week_block_iterator(self, block_length: int):
|
||||||
"""Gets a two dimensional list of week blocks in specified length
|
"""Gets a two dimensional list of week blocks in specified length
|
||||||
|
|
||||||
e.g. block_length = 4
|
e.g. block_length = 4
|
||||||
|
|||||||
+5
-1
@@ -128,7 +128,10 @@ class Worker(BaseModel):
|
|||||||
self.calculated_start_date = Rota.start_date
|
self.calculated_start_date = Rota.start_date
|
||||||
else:
|
else:
|
||||||
# ? test if start date is valid
|
# ? test if start date is valid
|
||||||
self.calculated_start_date = self.start_date
|
if self.start_date < Rota.start_date:
|
||||||
|
self.calculated_start_date = self.start_date
|
||||||
|
else:
|
||||||
|
self.calculated_start_date = self.start_date
|
||||||
|
|
||||||
if self.end_date is None:
|
if self.end_date is None:
|
||||||
self.calculated_end_date = Rota.rota_end_date
|
self.calculated_end_date = Rota.rota_end_date
|
||||||
@@ -271,6 +274,7 @@ class Worker(BaseModel):
|
|||||||
self.fte_adj = 0
|
self.fte_adj = 0
|
||||||
|
|
||||||
if days_to_work > 0 and (days_to_work - len(unavailable_set)) / days_to_work < 0.3:
|
if days_to_work > 0 and (days_to_work - len(unavailable_set)) / days_to_work < 0.3:
|
||||||
|
console.print(f"{ self.name }: {days_to_work}, {Rota.rota_days_length}")
|
||||||
console.print(f"Warning {self.name} has excessive unavailability [{len(unavailable_set)}/{days_to_work}, adjusted fte={self.fte_adj}] (this may be infeasible)", style="red")
|
console.print(f"Warning {self.name} has excessive unavailability [{len(unavailable_set)}/{days_to_work}, adjusted fte={self.fte_adj}] (this may be infeasible)", style="red")
|
||||||
|
|
||||||
def __lt__(self, other) -> bool:
|
def __lt__(self, other) -> bool:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
parallel/minnthreads = 5
|
||||||
|
parallel/maxnthreads = 10
|
||||||
|
|
||||||
|
limits/solutions = -1
|
||||||
|
|
||||||
|
limits/time = 345600
|
||||||
+2
-2
@@ -353,7 +353,7 @@ class TestDemoRotaClear:
|
|||||||
self.Rota.build_workers()
|
self.Rota.build_workers()
|
||||||
self.Rota.build_model()
|
self.Rota.build_model()
|
||||||
|
|
||||||
solver_options = {"ratio": 0.1, "seconds": 1000, "threads": 10}
|
solver_options = {"seconds": 1000, "threads": 10}
|
||||||
|
|
||||||
self.Rota.solve_model(options=solver_options)
|
self.Rota.solve_model(options=solver_options)
|
||||||
self.Rota.export_rota_to_html("test4")
|
self.Rota.export_rota_to_html("test4")
|
||||||
@@ -682,7 +682,7 @@ class TestLimitConstraints:
|
|||||||
# Rota.constraint_options["balance_nights_across_sites"] = False
|
# Rota.constraint_options["balance_nights_across_sites"] = False
|
||||||
# Rota.constraint_options["balance_shifts_over_workers"] = False
|
# Rota.constraint_options["balance_shifts_over_workers"] = False
|
||||||
#Rota.constraint_options["balance_shifts_quadratic"] = True
|
#Rota.constraint_options["balance_shifts_quadratic"] = True
|
||||||
Rota.build_and_solve(options={"ratio": 0.1})
|
Rota.build_and_solve(options={})
|
||||||
|
|
||||||
Rota.export_rota_to_html("constraint_limit_grades")
|
Rota.export_rota_to_html("constraint_limit_grades")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user