Refactor RotaBuilder export functionality and enhance display of run times with improved layout
This commit is contained in:
+2
-2
@@ -318,7 +318,7 @@ def main(
|
||||
rota_start_date,
|
||||
weeks_to_rota=weeks,
|
||||
balance_offset_modifier=bom,
|
||||
name="cons rota",
|
||||
name="cons rota test",
|
||||
)
|
||||
# Rota = RotaBuilder(start_date, weeks_to_rota=20, balance_offset_modifier=1)
|
||||
Rota.constraint_options["balance_weekends"] = True
|
||||
@@ -486,7 +486,7 @@ def main(
|
||||
# solver_options = {"seconds": time_to_run, "threads": 10}
|
||||
|
||||
# start_time = time.time()
|
||||
Rota.build_and_solve(solver_options, export=True, solve=solve, solver="appsi_highs")
|
||||
Rota.build_and_solve(solver_options, export=True, export_with_timestamp=False, solve=solve, solver="appsi_highs")
|
||||
|
||||
# Rota.solve_shifts_by_block(solver_options, block_length=13)
|
||||
# Rota.solve_shifts_individually(solver_options)
|
||||
|
||||
+38
-11
@@ -31,7 +31,7 @@ table {
|
||||
|
||||
.table-div {
|
||||
/* overflow-x: auto; */
|
||||
width: 80%;
|
||||
/* width: 80%; */
|
||||
overflow-x: scroll;
|
||||
margin-left: 200px;
|
||||
overflow-y: visible;
|
||||
@@ -118,7 +118,8 @@ th.bank-holiday {
|
||||
color: #0074D9;
|
||||
}
|
||||
|
||||
.torquay, .torbay {
|
||||
.torquay,
|
||||
.torbay {
|
||||
color: #5FA8E8;
|
||||
}
|
||||
|
||||
@@ -250,6 +251,7 @@ td.large-shift-diff-pos::before {
|
||||
content: "*";
|
||||
color: red;
|
||||
}
|
||||
|
||||
td.large-shift-diff-neg::before {
|
||||
content: "*";
|
||||
color: green;
|
||||
@@ -259,6 +261,7 @@ 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;
|
||||
@@ -279,11 +282,9 @@ table.transposed td {
|
||||
border: 1px solid black;
|
||||
max-width: 100px;
|
||||
} */
|
||||
table.transposed {
|
||||
}
|
||||
table.transposed {}
|
||||
|
||||
table.transposed tr {
|
||||
}
|
||||
table.transposed tr {}
|
||||
|
||||
table.transposed th,
|
||||
table.transposed td {
|
||||
@@ -327,12 +328,38 @@ table.transposed th.bank-holiday {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.force-assigned {
|
||||
background-color: #ffe066 !important;
|
||||
border: 2px solid #ff8800 !important;
|
||||
}
|
||||
|
||||
button.selected {
|
||||
background: #0074d9;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#main-table th.col-hover {
|
||||
background: #ffe066 !important;
|
||||
color: #222 !important;
|
||||
}
|
||||
|
||||
.highlight-day-btn.highlighted {
|
||||
background: #ffe066 !important;
|
||||
color: #222 !important;
|
||||
border: 2px solid #bfa600 !important;
|
||||
}
|
||||
|
||||
.highlight-day-btn.highlighted {
|
||||
background: #ffe066 !important;
|
||||
color: #222 !important;
|
||||
border: 2px solid #bfa600 !important;
|
||||
}
|
||||
|
||||
#main-table td.day-highlighted,
|
||||
#main-table th.day-highlighted {
|
||||
/* background is set inline for custom color */
|
||||
}
|
||||
|
||||
#display-settings-modal {
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
#main-table td.force-assigned.force-assigned-highlight {
|
||||
background-color: #ffe066 !important;
|
||||
border: 2px solid #ff8800 !important;
|
||||
}
|
||||
+361
-36
@@ -18,6 +18,8 @@ const mean = arr => arr.reduce((p, c) => p + c, 0) / arr.length;
|
||||
|
||||
tables = $(".table-div");
|
||||
|
||||
$("#extra-div").append("<div id='shift-diffs'></div>");
|
||||
|
||||
|
||||
function generateExtra() {
|
||||
$(".auto-generated").remove();
|
||||
@@ -158,7 +160,7 @@ function generateExtra() {
|
||||
|
||||
bank_holidays_worked = bank_holidays.length;
|
||||
|
||||
nights_worked= $(tr).find(".night-shift").length;
|
||||
nights_worked = $(tr).find(".night-shift").length;
|
||||
|
||||
shift_diff = JSON.parse(worker_td.attr("data-shift-diff"));
|
||||
summed_shift_diff = Object.values(shift_diff).reduce((a, b) => a + b);
|
||||
@@ -166,6 +168,8 @@ function generateExtra() {
|
||||
total_summed_shift_diffs = total_summed_shift_diffs + summed_shift_diff;
|
||||
total_summed_shift_diffs_abs = total_summed_shift_diffs_abs + Math.abs(summed_shift_diff);
|
||||
|
||||
|
||||
|
||||
worker_td.get(0).dataset.shiftDiffSummed = summed_shift_diff.toPrecision(2);
|
||||
|
||||
|
||||
@@ -237,8 +241,10 @@ function generateExtra() {
|
||||
});
|
||||
|
||||
|
||||
$(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/>`));
|
||||
$("#shift-diffs").before($(`<span>Total summed shift diff: ${total_summed_shift_diffs}</span><br/>`));
|
||||
$("#shift-diffs").before($(`<span>Total summed shift diff (abs): ${total_summed_shift_diffs_abs}</span><br/>`));
|
||||
|
||||
|
||||
$(table).before(summary_button);
|
||||
|
||||
|
||||
@@ -295,10 +301,8 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
locum_shift_counts = jtr.data("locum-shift-counts")
|
||||
shift_targets = jtr.data("worker-targets")
|
||||
|
||||
console.log(shift_counts, locum_shift_counts, shift_targets)
|
||||
|
||||
oshifts.forEach((s) => {
|
||||
console.log(s, shift_counts, shift_targets, locum_shift_counts)
|
||||
if (s in shift_targets && shift_targets[s] > 0) {
|
||||
if (s in shift_counts) {
|
||||
c = shift_counts[s];
|
||||
@@ -309,7 +313,7 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
if (s in locum_shift_counts) {
|
||||
locum_shifts = `+${locum_shift_counts[s]}`
|
||||
}
|
||||
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}${locum_shifts} (${shift_targets[s].toFixed(2)})</td>`)
|
||||
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}${locum_shifts} (${shift_targets[s].toFixed(2)})</td>`)
|
||||
|
||||
} else {
|
||||
row.append(`<td>-</td>`)
|
||||
@@ -373,34 +377,77 @@ oshifts.forEach((shift, i) => {
|
||||
shiftColors[shift] = `hsl(${(i * 360 / oshifts.length)}, 70%, 80%)`;
|
||||
});
|
||||
|
||||
oshifts.forEach((shift) => {
|
||||
let button = $(`<button data-shift='${shift}'>${shift}</button>`);
|
||||
// Set the button background to default (no color) initially
|
||||
button.css({
|
||||
background: "",
|
||||
border: "1px solid #888",
|
||||
margin: "2px",
|
||||
color: "#222"
|
||||
// Add a button to toggle grid view
|
||||
$("#shift-timetable-options").append(
|
||||
`<button id="toggle-grid-view" style="margin-left:10px;">Toggle Linear/Grid View</button><br/>`
|
||||
);
|
||||
|
||||
let gridViewEnabled = false;
|
||||
|
||||
$("#toggle-grid-view").on("click", function () {
|
||||
gridViewEnabled = !gridViewEnabled;
|
||||
renderShiftTimetable();
|
||||
});
|
||||
|
||||
|
||||
const shiftColourControls = $(`
|
||||
<details><summary>Shift Timetable Colours</summary>
|
||||
<div id="shift-colour-controls" style="margin:16px 0;">
|
||||
<b>Shift timetable colours:</b>
|
||||
<span id="shift-colour-buttons"></span>
|
||||
</div>
|
||||
</details>
|
||||
`);
|
||||
$("#shift-timetable-options").append(shiftColourControls);
|
||||
|
||||
// Add colour pickers for each shift
|
||||
oshifts.forEach(shift => {
|
||||
// Colour picker for the shift
|
||||
let colorInput = $(`<input type="color" class="shift-colour-picker" data-shift="${shift}" value="${rgb2hex(shiftColors[shift])}" style="margin-left:2px; vertical-align:middle;" title="Pick colour for ${shift}">`);
|
||||
// Label for the shift
|
||||
let label = $(`<label style="margin-right:10px;">${shift}</label>`);
|
||||
// On change, update the shiftColors map, button color, and re-render the timetable
|
||||
colorInput.on("input", function () {
|
||||
shiftColors[shift] = $(this).val();
|
||||
// Update the corresponding shift button color
|
||||
$(`#shift-timetable-options button[data-shift='${shift}']`).css("background", selectedShifts.has(shift) ? shiftColors[shift] : "");
|
||||
renderShiftTimetable();
|
||||
});
|
||||
$("#shift-colour-buttons").append(label.append(colorInput));
|
||||
});
|
||||
|
||||
button.on("click", function (evt) {
|
||||
let s = evt.target.dataset.shift;
|
||||
if (selectedShifts.has(s)) {
|
||||
selectedShifts.delete(s);
|
||||
$(this).removeClass("selected");
|
||||
// Remove color when not selected
|
||||
$(this).css("background", "");
|
||||
} else {
|
||||
selectedShifts.add(s);
|
||||
$(this).addClass("selected");
|
||||
// Set color when selected
|
||||
$(this).css("background", shiftColors[shift]);
|
||||
// Utility to convert hsl to hex for initial value
|
||||
function rgb2hex(rgb) {
|
||||
// Accepts hsl() or rgb() or hex
|
||||
if (rgb.startsWith("#")) return rgb;
|
||||
if (rgb.startsWith("hsl")) {
|
||||
// Convert hsl to rgb
|
||||
let hsl = rgb.match(/hsl\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%\)/);
|
||||
if (!hsl) return "#ffe066";
|
||||
let h = Number(hsl[1]), s = Number(hsl[2]) / 100, l = Number(hsl[3]) / 100;
|
||||
let a = s * Math.min(l, 1 - l);
|
||||
function f(n) {
|
||||
let k = (n + h / 30) % 12;
|
||||
let color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
return Math.round(255 * color);
|
||||
}
|
||||
return "#" + [f(0), f(8), f(4)].map(x => x.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
if (rgb.startsWith("rgb")) {
|
||||
let rgbArr = rgb.match(/\d+/g).map(Number);
|
||||
return "#" + rgbArr.map(x => x.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
return "#ffe066";
|
||||
}
|
||||
|
||||
// Update timetable
|
||||
$("#shift-timetable-div").empty();
|
||||
if (selectedShifts.size === 0) return;
|
||||
// Wrap timetable rendering in a function so we can call it for both views
|
||||
function renderShiftTimetable() {
|
||||
$("#shift-timetable-div").empty();
|
||||
if (selectedShifts.size === 0) return;
|
||||
|
||||
if (!gridViewEnabled) {
|
||||
|
||||
// Original (list) view
|
||||
let table = $("<table>");
|
||||
$("table#main-table th.date").each((n, th) => {
|
||||
let row = $("<tr>");
|
||||
@@ -430,11 +477,111 @@ oshifts.forEach((shift) => {
|
||||
table.append(row);
|
||||
});
|
||||
$("#shift-timetable-div").append(table);
|
||||
return;
|
||||
}
|
||||
|
||||
//// Get all weeks and days
|
||||
let weeks = [];
|
||||
let daysOfWeek = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
$("table#main-table td[data-week]").each(function () {
|
||||
let week = $(this).attr("data-week");
|
||||
if (week && !weeks.includes(week)) weeks.push(week);
|
||||
});
|
||||
weeks = weeks.sort((a, b) => parseInt(a) - parseInt(b));
|
||||
|
||||
|
||||
// Get all workers who have at least one selected shift
|
||||
let workerIds = [];
|
||||
let workerIdToName = {};
|
||||
$("table#main-table td.worker").each(function () {
|
||||
let workerId = $(this).attr("data-worker-id");
|
||||
if (workerId && !workerIds.includes(workerId)) {
|
||||
workerIds.push(workerId);
|
||||
let workerName = $(this).find("span.name").text().trim();
|
||||
workerIdToName[workerId] = workerName;
|
||||
}
|
||||
});
|
||||
|
||||
// workerId -> week -> day -> {shifts: [...], name: ...}
|
||||
let cellMap = {};
|
||||
$("table#main-table td.worker").each(function () {
|
||||
let workerId = $(this).attr("data-worker-id");
|
||||
let workerName = $(this).find("span.name").text().trim();
|
||||
if (!cellMap[workerId]) cellMap[workerId] = { name: workerName, cells: {} };
|
||||
});
|
||||
$("table#main-table td[data-week][data-day]").each(function () {
|
||||
let td = $(this);
|
||||
let tr = td.closest("tr");
|
||||
let workerTd = tr.find("td.worker");
|
||||
let workerId = workerTd.attr("data-worker-id");
|
||||
let week = td.attr("data-week");
|
||||
let day = td.attr("data-day");
|
||||
let shifts = (td.attr("data-shift") || "").split(",").map(s => s.trim()).filter(Boolean);
|
||||
if (workerId && week && day) {
|
||||
if (!cellMap[workerId].cells[week]) cellMap[workerId].cells[week] = {};
|
||||
cellMap[workerId].cells[week][day] = shifts;
|
||||
}
|
||||
});
|
||||
|
||||
let gridTable = $(`<table class="shift-grid-table" style="margin-bottom:20px; border-collapse:collapse; width:auto;"><caption>Shift Grid</caption></table>`);
|
||||
let header = $("<tr><th style='border:1px solid #888; min-width:60px; width:80px;'>Week</th></tr>");
|
||||
daysOfWeek.forEach(day => header.append(`<th style='border:1px solid #888;'>${day}</th>`));
|
||||
gridTable.append(header);
|
||||
|
||||
weeks.forEach(week => {
|
||||
let row = $(`<tr></tr>`);
|
||||
row.append(`<td style="border:1px solid #888; min-width:60px; width:80px; font-weight:bold;">${week}</td>`);
|
||||
daysOfWeek.forEach(day => {
|
||||
let cellContent = "";
|
||||
for (const workerId of workerIds) {
|
||||
let worker = cellMap[workerId];
|
||||
let shifts = (worker.cells[week] && worker.cells[week][day]) || [];
|
||||
let matchedShifts = Array.from(selectedShifts).filter(s => shifts.includes(s));
|
||||
if (matchedShifts.length > 0) {
|
||||
let color = "#fff";
|
||||
if (matchedShifts.length === 1) {
|
||||
color = shiftColors[matchedShifts[0]];
|
||||
} else if (matchedShifts.length > 1) {
|
||||
color = `linear-gradient(90deg, ${shiftColors[matchedShifts[0]]} 0 50%, ${shiftColors[matchedShifts[1]]} 50% 100%)`;
|
||||
}
|
||||
cellContent += `<div style="background:${color};margin:1px 0;padding:0 2px;border-radius:3px;display:inline-block;">${worker.name}</div> `;
|
||||
}
|
||||
}
|
||||
row.append(`<td style="border:1px solid #888; min-width:40px; text-align:center;">${cellContent}</td>`);
|
||||
});
|
||||
gridTable.append(row);
|
||||
});
|
||||
$("#shift-timetable-div").append(gridTable);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Patch the timetable button logic to use renderShiftTimetable
|
||||
oshifts.forEach((shift) => {
|
||||
let button = $(`<button data-shift='${shift}'>${shift}</button>`);
|
||||
button.css({
|
||||
background: "",
|
||||
border: "1px solid #888",
|
||||
margin: "2px",
|
||||
color: "#222"
|
||||
});
|
||||
|
||||
button.on("click", function (evt) {
|
||||
let s = evt.target.dataset.shift;
|
||||
if (selectedShifts.has(s)) {
|
||||
selectedShifts.delete(s);
|
||||
$(this).removeClass("selected");
|
||||
$(this).css("background", "");
|
||||
} else {
|
||||
selectedShifts.add(s);
|
||||
$(this).addClass("selected");
|
||||
$(this).css("background", shiftColors[shift]);
|
||||
}
|
||||
renderShiftTimetable();
|
||||
});
|
||||
|
||||
$("#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>")
|
||||
@@ -486,19 +633,19 @@ $("td").hover((e) => {
|
||||
}
|
||||
|
||||
// Find the span under the mouse, if any
|
||||
let targetSpan = $(e.target).closest("span.multi-shift-shift").get(0) ||
|
||||
($(e.target).hasClass("multi-shift-shift") ? e.target : null);
|
||||
let targetSpan = $(e.target).closest("span.multi-shift-shift").get(0) ||
|
||||
($(e.target).hasClass("multi-shift-shift") ? e.target : null);
|
||||
|
||||
if (targetSpan) {
|
||||
// Only highlight spans with the same text
|
||||
let shiftDisplay = $(targetSpan).text().trim();
|
||||
$("span.multi-shift-shift").filter(function() {
|
||||
$("span.multi-shift-shift").filter(function () {
|
||||
return $(this).text().trim() === shiftDisplay;
|
||||
}).addClass("shift-highlight");
|
||||
} else if (e.target.dataset.shift != undefined && e.target.dataset.shift.length > 0) {
|
||||
// Fallback: highlight all cells containing ANY of the hovered shifts (legacy)
|
||||
let hovered_shifts = e.target.dataset.shift.split(",").map(s => s.trim());
|
||||
$("td").filter(function() {
|
||||
$("td").filter(function () {
|
||||
let shifts = $(this).attr("data-shift");
|
||||
if (!shifts) return false;
|
||||
let cell_shifts = shifts.split(",").map(s => s.trim());
|
||||
@@ -630,7 +777,7 @@ function hsv2rgb(h, s, v) {
|
||||
|
||||
function syntaxHighlight(json) {
|
||||
if (typeof json != 'string') {
|
||||
json = JSON.stringify(json, undefined, 2);
|
||||
json = JSON.stringify(json, undefined, 2);
|
||||
}
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
|
||||
@@ -648,4 +795,182 @@ function syntaxHighlight(json) {
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(function() {
|
||||
const mainTable = document.getElementById("main-table");
|
||||
if (!mainTable) return;
|
||||
|
||||
// Helper to remove highlight from all headers
|
||||
function clearColHover() {
|
||||
const ths = mainTable.querySelectorAll("th.col-hover");
|
||||
ths.forEach(th => th.classList.remove("col-hover"));
|
||||
}
|
||||
|
||||
// Mouseover handler for all cells
|
||||
mainTable.addEventListener("mouseover", function(e) {
|
||||
let cell = e.target.closest("td,th");
|
||||
if (!cell) return;
|
||||
// Find the column index
|
||||
let colIdx = cell.cellIndex;
|
||||
if (colIdx === undefined) return;
|
||||
clearColHover();
|
||||
// Highlight the header in this column (first row)
|
||||
let headerRow = mainTable.tHead ? mainTable.tHead.rows[0] : mainTable.rows[0];
|
||||
if (headerRow && headerRow.cells[colIdx]) {
|
||||
headerRow.cells[colIdx].classList.add("col-hover");
|
||||
}
|
||||
});
|
||||
|
||||
// Mouseout handler to clear highlight
|
||||
mainTable.addEventListener("mouseout", function(e) {
|
||||
clearColHover();
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
|
||||
// --- Add a button to show the Display Settings modal ---
|
||||
$("#main-table").before(
|
||||
`<button id="show-display-settings" style="margin:8px 0 8px 0; float:right;">Display Settings</button>`
|
||||
);
|
||||
|
||||
// --- Modal show/hide logic ---
|
||||
$("#show-display-settings").on("click", function() {
|
||||
$("#display-settings-modal").show();
|
||||
});
|
||||
// Attach the event handler after the modal is added to the DOM
|
||||
$(document).on("click", "#close-display-settings", function() {
|
||||
$("#display-settings-modal").hide();
|
||||
});
|
||||
|
||||
// --- Modal HTML (replace the highlight-days-controls div with this) ---
|
||||
const displaySettingsModal = $(`
|
||||
<div id="display-settings-modal" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); z-index:10000; background:#fff; border:2px solid #888; border-radius:8px; box-shadow:0 4px 24px #0002; padding:24px; min-width:320px;">
|
||||
<h3 style="margin-top:0;">Display Settings</h3>
|
||||
<div id="highlight-days-controls" style="margin:8px 0;">
|
||||
<b>Highlight days of week:</b>
|
||||
<span id="highlight-days-buttons"></span>
|
||||
</div>
|
||||
<div style="text-align:right; margin-top:16px;">
|
||||
<button id="close-display-settings">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
$("body").append(displaySettingsModal);
|
||||
|
||||
// --- Highlight day buttons in modal with color pickers ---
|
||||
const daysOfWeek = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
let highlightedDays = new Set();
|
||||
let highlightDayColors = {}; // day -> color
|
||||
|
||||
// Default highlight color
|
||||
const defaultHighlightColor = "#ffe066";
|
||||
|
||||
// --- Add force assigned highlight controls to the Display Settings modal ---
|
||||
const forceAssignedControls = $(`
|
||||
<div id="force-assigned-controls" style="margin:16px 0;">
|
||||
<label>
|
||||
<input type="checkbox" id="highlight-force-assigned" checked>
|
||||
Highlight force assigned shifts
|
||||
</label>
|
||||
</div>
|
||||
`);
|
||||
$("#display-settings-modal #highlight-days-controls").after(forceAssignedControls);
|
||||
|
||||
// --- Highlighting logic ---
|
||||
function updateDayHighlights() {
|
||||
// Remove previous highlights and reset borders/backgrounds
|
||||
$("#main-table td, #main-table th").removeClass("day-highlighted")
|
||||
.css("border-left", "").css("border-right", "").css("background", "");
|
||||
|
||||
// For each selected day, highlight all cells in that column
|
||||
highlightedDays.forEach(day => {
|
||||
let color = highlightDayColors[day] || defaultHighlightColor;
|
||||
// Highlight th: only change border color, not background
|
||||
$(`#main-table th[data-day='${day}']`)
|
||||
.addClass("day-highlighted")
|
||||
.css("border-left", `3px solid ${color}`)
|
||||
.css("border-right", `3px solid ${color}`)
|
||||
.css("background", "");
|
||||
// Highlight td: only change border color, not background
|
||||
$(`#main-table td[data-day='${day}']`)
|
||||
.addClass("day-highlighted")
|
||||
.css("border-left", `3px solid ${color}`)
|
||||
.css("border-right", `3px solid ${color}`)
|
||||
.css("background", "");
|
||||
});
|
||||
// Update modal button states
|
||||
$("#highlight-days-buttons .highlight-day-btn").each(function() {
|
||||
let day = $(this).data("day");
|
||||
if (highlightedDays.has(day)) {
|
||||
$(this).addClass("highlighted");
|
||||
} else {
|
||||
$(this).removeClass("highlighted");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Highlighting logic for force assigned shifts ---
|
||||
function updateForceAssignedHighlights() {
|
||||
if ($("#highlight-force-assigned").prop("checked")) {
|
||||
// Add highlight to all force assigned cells
|
||||
$("#main-table td.force-assigned").addClass("force-assigned-highlight");
|
||||
} else {
|
||||
// Remove highlight
|
||||
$("#main-table td.force-assigned").removeClass("force-assigned-highlight");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update both day and force assigned highlights together ---
|
||||
function updateAllHighlights() {
|
||||
updateDayHighlights();
|
||||
updateForceAssignedHighlights();
|
||||
}
|
||||
|
||||
// --- Update force assigned highlights when checkbox changes ---
|
||||
$("#display-settings-modal").on("change", "#highlight-force-assigned", function() {
|
||||
updateForceAssignedHighlights();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// --- Also call updateAllHighlights on modal open ---
|
||||
$("#show-display-settings").on("click", function() {
|
||||
$("#display-settings-modal").show();
|
||||
updateAllHighlights();
|
||||
});
|
||||
|
||||
// --- Highlight day buttons in modal with color pickers ---
|
||||
daysOfWeek.forEach(day => {
|
||||
// Button to toggle highlight
|
||||
let btn = $(`<button type="button" class="highlight-day-btn" data-day="${day}" style="margin:0 2px;">${day}</button>`);
|
||||
// Color picker
|
||||
let colorInput = $(`<input type="color" class="highlight-day-color" data-day="${day}" value="${defaultHighlightColor}" style="margin-left:2px; vertical-align:middle;" title="Pick highlight color for ${day}">`);
|
||||
highlightDayColors[day] = defaultHighlightColor;
|
||||
|
||||
btn.on("click", function () {
|
||||
if (highlightedDays.has(day)) {
|
||||
highlightedDays.delete(day);
|
||||
$(this).removeClass("highlighted");
|
||||
} else {
|
||||
highlightedDays.add(day);
|
||||
$(this).addClass("highlighted");
|
||||
}
|
||||
updateAllHighlights();
|
||||
});
|
||||
|
||||
colorInput.on("input", function () {
|
||||
highlightDayColors[day] = $(this).val();
|
||||
updateAllHighlights();
|
||||
});
|
||||
|
||||
$("#highlight-days-buttons").append(btn).append(colorInput);
|
||||
});
|
||||
|
||||
|
||||
// --- Initial highlight on page load ---
|
||||
$(function() {
|
||||
updateAllHighlights();
|
||||
});
|
||||
+10
-5
@@ -609,6 +609,7 @@ class RotaBuilder(object):
|
||||
solve=True,
|
||||
debug_if_fail: bool = False,
|
||||
export=False,
|
||||
export_with_timestamp=False,
|
||||
solver="appsi_highs",
|
||||
):
|
||||
self.run_start_time = datetime.datetime.now()
|
||||
@@ -628,7 +629,7 @@ class RotaBuilder(object):
|
||||
self.run_end_time = datetime.datetime.now()
|
||||
|
||||
if export:
|
||||
self.export_rota_to_html(self.name)
|
||||
self.export_rota_to_html(self.name, timestamp_filename=export_with_timestamp)
|
||||
|
||||
def build_model(self):
|
||||
# Initialize model
|
||||
@@ -4234,9 +4235,11 @@ class RotaBuilder(object):
|
||||
|
||||
if self.run_start_time is not None and self.run_end_time is not None:
|
||||
timetable.append(
|
||||
f"<div>Start time: <span id='run_start_time'>{self.run_start_time:%Y-%m-%d %H:%M:%S%z}</span></div>"
|
||||
f"<div>End time: <span id='run_end_time'>{self.run_end_time:%Y-%m-%d %H:%M:%S%z}</span></div>"
|
||||
f"<div>Elapsed: <span id='run_elapsed_time'>{self.run_end_time - self.run_start_time}</span></div>"
|
||||
"<div id='run_times_row' style='display: flex; gap: 2em; margin-bottom: 1em;'>"
|
||||
f"<div id='run_start_time_cell'>Start time: <span id='run_start_time'>{self.run_start_time:%Y-%m-%d %H:%M:%S%z}</span></div>"
|
||||
f"<div id='run_end_time_cell'>End time: <span id='run_end_time'>{self.run_end_time:%Y-%m-%d %H:%M:%S%z}</span></div>"
|
||||
f"<div id='run_elapsed_time_cell'>Elapsed: <span id='run_elapsed_time'>{self.run_end_time - self.run_start_time}</span></div>"
|
||||
"</div>"
|
||||
)
|
||||
|
||||
timetable.append(
|
||||
@@ -4621,10 +4624,12 @@ class RotaBuilder(object):
|
||||
{"TEST"}
|
||||
{self.locum_availability_map}
|
||||
<br/>
|
||||
<h3>Force assigned shifts</h3>
|
||||
<details>
|
||||
<summary>Force assigned shifts</summary>
|
||||
<pre>
|
||||
{json.dumps(force_assigned_info, indent=4)}
|
||||
</pre>
|
||||
</details>
|
||||
<div>
|
||||
</details>
|
||||
<details><summary><h2>Hard Constrain Shift Min/Max</h2></summary>
|
||||
|
||||
Reference in New Issue
Block a user