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");
$("#extra-div").append("
");
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) => {
let data_shift = $(td).attr("data-shift");
if (data_shift && data_shift !== "") {
// Split by comma for multiple shifts per day
let split_shifts = data_shift.split(",");
split_shifts.forEach(s => {
if (s && s.trim() !== "") {
shifts.push(s.trim());
}
});
}
})
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;
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);
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(`
Total shifts: ${total_shifts}, Weekends: ${weekends_worked}, Nights: ${nights_worked}, Bank holidays: ${bank_holidays_worked}, Summed shift diff: ${summed_shift_diff.toPrecision(2)},
`)
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("
Average shifts for a 100% FTE trainee
");
// for(const site in sites) {
// $(table).find(".site-summary").append(`
${site}
`);
// for(const shift in sites[site]) {
// average = mean(sites[site][shift])
// $(table).find(`.site-${site}`).append(`
`)
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")
locum_shift_counts = jtr.data("locum-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;
}
locum_shifts = ""
if (s in locum_shift_counts) {
locum_shifts = `+${locum_shift_counts[s]}`
}
row.append(`
`)
}
})
$("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)
})
});
let selectedShifts = new Set();
let shiftColors = {};
oshifts.forEach((shift, i) => {
selectedShifts.add(shift);
// Assign a unique color for each shift using HSL
shiftColors[shift] = `hsl(${(i * 360 / oshifts.length)}, 70%, 80%)`;
});
// Add a button to toggle grid view
$("#shift-timetable-options").append(
` `
);
let gridViewEnabled = false;
$("#toggle-grid-view").on("click", function () {
gridViewEnabled = !gridViewEnabled;
renderShiftTimetable();
});
const shiftColourControls = $(`
Shift Timetable Colours
Shift timetable colours:
`);
$("#shift-timetable-options").append(shiftColourControls);
// Add colour pickers for each shift
oshifts.forEach(shift => {
// Colour picker for the shift
let colorInput = $(``);
// Label for the shift
let 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));
});
// Add the toggle button if not already present
if ($("#show-all-shifts").length === 0) {
$("#shift-timetable-options").append(
``
);
}
let allShiftsShown = false; // Start with all shifts shown by default
function updateShowAllShiftsButton() {
if (allShiftsShown) {
$("#show-all-shifts").addClass("active").text("Hide All Shifts");
} else {
$("#show-all-shifts").removeClass("active").text("Show All Shifts");
}
}
// Toggle logic
$("#show-all-shifts").off("click").on("click", function () {
allShiftsShown = !allShiftsShown;
if (allShiftsShown) {
// Select all shifts
oshifts.forEach((shift) => {
selectedShifts.add(shift);
$(`#shift-timetable-options button[data-shift='${shift}']`)
.addClass("selected")
.css("background", shiftColors[shift]);
});
} else {
// Deselect all shifts
oshifts.forEach((shift) => {
selectedShifts.delete(shift);
$(`#shift-timetable-options button[data-shift='${shift}']`)
.removeClass("selected")
.css("background", "");
});
}
updateShowAllShiftsButton();
renderShiftTimetable();
});
// Ensure the button state is correct on page load and after timetable render
updateShowAllShiftsButton();
// 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";
}
// --- Add worker checkboxes to the shift timetable section ---
function renderWorkerCheckboxes() {
// Get all unique worker names and IDs from the main table
let workerList = [];
$("table#main-table td.worker").each(function () {
let workerId = $(this).attr("data-worker-id");
let workerName = $(this).find("span.name").text().trim();
if (workerId && !workerList.some(w => w.id === workerId)) {
workerList.push({ id: workerId, name: workerName });
}
});
// Build checkboxes
let html = `Show workers:`;
// Add a "Toggle All" button
html += ``;
workerList.forEach(w => {
html += ``;
});
// Attach toggle logic after DOM insertion
setTimeout(() => {
$("#toggle-all-workers").off("click").on("click", function() {
const $checkboxes = $("input[name='worker-filter-checkbox']");
const allChecked = $checkboxes.length === $checkboxes.filter(":checked").length;
$checkboxes.prop("checked", !allChecked);
// Update the filter set and re-render once, instead of triggering "change" for each
workerFilterSet = new Set(
Array.from(document.querySelectorAll("input[name='worker-filter-checkbox']:checked"))
.map(cb => cb.value)
);
// Call your function here after the map has finished
renderShiftTimetable();
});
}, 0);
html += ``;
// Prepend to the shift timetable options
$("#shift-timetable-options").prepend(html);
}
// Store the current filter value (set of workerIds)
let workerFilterSet = new Set();
// Store the current setting
let showShiftNames = false;
// Call this after the DOM is ready and after timetable is rendered
renderWorkerCheckboxes();
// On first render, select all workers by default
$("input[name='worker-filter-checkbox']").each(function() {
workerFilterSet.add($(this).val());
});
renderShiftTimetable();
// Update the filter set on checkbox change
$(document).on("change", "input[name='worker-filter-checkbox']", function() {
workerFilterSet = new Set(
$("input[name='worker-filter-checkbox']:checked").map(function() { return $(this).val(); }).get()
);
renderShiftTimetable();
});
// Add a checkbox to show/hide shift names in the shift timetable section
$("#shift-timetable-options").prepend(`
`);
// Add a button to remove all shift timetable colours (set to white)
$("#shift-colour-controls").append(
``
);
$("#remove-shift-colours").on("click", function () {
// Set all shift colours to white
oshifts.forEach(shift => {
shiftColors[shift] = "#ffffff";
// Update the colour picker if present
$(`.shift-colour-picker[data-shift='${shift}']`).val("#ffffff");
// Update the corresponding shift button color if selected
$(`#shift-timetable-options button[data-shift='${shift}']`).css("background", selectedShifts.has(shift) ? "#ffffff" : "");
});
renderShiftTimetable();
});
$("#show-shift-names-checkbox").on("change", function() {
showShiftNames = $(this).is(":checked");
renderShiftTimetable();
});
// Patch renderShiftTimetable to show shift names if enabled
function renderShiftTimetable() {
console.log("Render shift timetable");
console.log(workerFilterSet)
$("#shift-timetable-div").empty();
if (selectedShifts.size === 0) return;
if (workerFilterSet.size === 0) return;
// Helper: returns true if worker id matches filter set or set is empty (all)
function workerMatchesFilter(workerId) {
if (workerFilterSet.size === 0) return true;
return workerFilterSet.has(workerId);
}
if (!gridViewEnabled) {
// Linear view
let table = $("
");
let headerRow = $("
");
headerRow.append("
Date
Week
Day
Worker(s)
");
table.append(headerRow);
$("table#main-table th.date").each((n, th) => {
let date = th.dataset.date;
let week = "";
let day = "";
let firstTd = $(`table#main-table td[data-date='${date}']`).first();
if (firstTd.length) {
week = firstTd.attr("data-week") || "";
day = firstTd.attr("data-day") || "";
}
let row = $("
");
row.append(`
${date}
`);
row.append(`
${week}
`);
row.append(`
${day}
`);
let workerCells = [];
$(`table#main-table td[data-date='${date}']`).filter(function () {
let shifts = $(this).attr("data-shift");
if (!shifts) return false;
let cell_shifts = shifts.split(",").map(s => s.trim());
return Array.from(selectedShifts).some(s => cell_shifts.includes(s));
}).each((n, td) => {
let workerTd = $(td).closest("tr").children("td:first");
let workerName = workerTd.find("span.name").text().trim();
let workerId = workerTd.attr("data-worker-id");
if (!workerMatchesFilter(workerId)) return;
let cell_shifts = $(td).attr("data-shift").split(",").map(s => s.trim());
let matchedShifts = Array.from(selectedShifts).filter(s => cell_shifts.includes(s));
let color = "#fff";
let shiftLabel = "";
if (matchedShifts.length === 1) {
color = shiftColors[matchedShifts[0]];
if (showShiftNames) shiftLabel = ` (${matchedShifts[0]})`;
} else if (matchedShifts.length > 1) {
color = `linear-gradient(90deg, ${shiftColors[matchedShifts[0]]} 0 50%, ${shiftColors[matchedShifts[1]]} 50% 100%)`;
if (showShiftNames) shiftLabel = ` (${matchedShifts.join(", ")})`;
}
workerCells.push(`
${workerName}${shiftLabel}
`);
});
if (workerCells.length === 0) {
row.append("
");
} else {
row.append(workerCells.join(""));
}
table.append(row);
});
$("#shift-timetable-div").append(table);
return;
}
// Grid view
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));
let workerIds = [];
let workerIdToName = {};
$("table#main-table td.worker").each(function () {
let workerId = $(this).attr("data-worker-id");
let workerName = $(this).find("span.name").text().trim();
if (workerId && !workerIds.includes(workerId)) {
if (workerMatchesFilter(workerId)) {
workerIds.push(workerId);
workerIdToName[workerId] = workerName;
}
}
});
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 = $(`
Shift Grid
`);
let header = $("
Week
");
daysOfWeek.forEach(day => header.append(`
${day}
`));
gridTable.append(header);
weeks.forEach(week => {
let row = $(`
`);
row.append(`
${week}
`);
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";
let shiftLabel = "";
if (matchedShifts.length === 1) {
color = shiftColors[matchedShifts[0]];
if (showShiftNames) shiftLabel = ` (${matchedShifts[0]})`;
} else if (matchedShifts.length > 1) {
color = `linear-gradient(90deg, ${shiftColors[matchedShifts[0]]} 0 50%, ${shiftColors[matchedShifts[1]]} 50% 100%)`;
if (showShiftNames) shiftLabel = ` (${matchedShifts.join(", ")})`;
}
cellContent += `
${worker.name}${shiftLabel}
`;
}
}
row.append(`
${cellContent}
`);
});
gridTable.append(row);
});
$("#shift-timetable-div").append(gridTable);
}
// Re-render worker checkboxes after timetable is updated (in case workers change)
function rerenderWorkerCheckboxesIfNeeded() {
$("#shift-timetable-worker-filter").remove();
renderWorkerCheckboxes();
// Restore checked state
$("input[name='worker-filter-checkbox']").each(function() {
if (workerFilterSet.has($(this).val())) {
$(this).prop("checked", true);
}
});
}
// Call this after timetable is (re)rendered if needed
rerenderWorkerCheckboxesIfNeeded();
// Patch the timetable button logic to use renderShiftTimetable
oshifts.forEach((shift) => {
let 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);
});
// Add help text to the shift timetable section
$("#shift-timetable-options").prepend(`
Shift Timetables Help:
Select one or more shifts below to view which workers are assigned on each day.
Use the colour pickers to customise shift colours in the timetable and main table.
Toggle between linear and grid view using the "Toggle Linear/Grid View" button.
`);
//$("table.summary")
// $("body").prepend("
Settings: Customise training days lost per shift (you need to press recalculate for changes to take effect)
")
// shifts.forEach((s) => {
// disable = false;
// if(s.includes("twilight")) {
// value = 0.5;
// } else if(s == "night_weekend") {
// value = 4 / 3;
// } else if(s == "night_weekday") {
// value = 5 / 4;
// } else {
// disable = true;
// value = 0;
// }
// if(!disable) {
// $("#global-settings form").append(``);
// }
// })
// $("#global-settings").append($("").click(() => {
// generateExtra();
// }))
// generateExtra();
$(".table-div + pre").each((n, pre) => {
$(pre).before($("").click(() => {
$(pre).toggle();
}));
})
$("td").hover((e) => {
// start hover
worker_td = $(e.target).closest('tr').find('td:first')[0]
pair = worker_td.dataset.pair;
index = $(e.target).index() - 1;
if (pair > 0) {
$(`td[data-pair=${pair}]`).each((n, el) => {
$($(el).closest('tr').find('td').get(index)).addClass("pair-match")
})
}
// 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);
if (targetSpan) {
// Only highlight spans with the same text
let shiftDisplay = $(targetSpan).text().trim();
$("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 () {
let shifts = $(this).attr("data-shift");
if (!shifts) return false;
let cell_shifts = shifts.split(",").map(s => s.trim());
return hovered_shifts.some(s => cell_shifts.includes(s));
}).addClass("shift-highlight");
}
}, (e) => {
// end hover
$("td.pair-match").removeClass("pair-match")
$("td.shift-highlight").removeClass("shift-highlight")
$("span.shift-highlight").removeClass("shift-highlight")
$("td.remote-site-match").removeClass("remote-site-match")
});
$("#rota-table tr td:first-child").each((n, td) => {
$(td).click(() => {
viewWorker(td);
})
})
function viewWorker(worker) {
ds = worker.dataset;
dlg = $(`
Site: ${ds.site}
FTE: ${ds.fte} (${ds.fte_adj})
Start date: ${ds.start_date}
End date: ${ds.end_date}
Non working days: ${ds.nwds}
Remote site: ${ds.remoteSite}
Summed shift diff: ${ds.shiftDiffSummed}
`)
$("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] = $("
");
}
while (newrows[colToRowIndex].find("td, th").length < rowToColIndex) {
newrows[colToRowIndex].append($("
"));
}
newrows[colToRowIndex].append($(this));
});
});
$this.find("tr").remove();
$.each(newrows, function () {
$this.append(this);
});
$this.find("td.rota-day").each((n, el) => {
$(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('');
};
//$("#workers-json-target").append(syntaxHighlight(JSON.stringify(JSON.parse(($("#worker_details").data("workers"))))))
function syntaxHighlight(json) {
if (typeof json != 'string') {
json = JSON.stringify(json, undefined, 2);
}
json = json.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) {
var cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '' + match + '';
});
}
(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(
``
);
// --- 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 = $(`
Display Settings
Highlight days of week:
`);
$("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 = $(`
`);
$("#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 = $(``);
// Color picker
let colorInput = $(``);
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();
});