diff --git a/output/timetable.js b/output/timetable.js index e7e36e4..cda2676 100644 --- a/output/timetable.js +++ b/output/timetable.js @@ -273,10 +273,13 @@ if (shifts.size < 1) { oshifts = Array.from(shifts).sort() -$("body").append("
") +$("body").append("
") $("#show-colour-diff").on("click", (evt) => { $("table.tsummary").find(`td.target-assigned`).toggleClass("no-colour"); }); +$("#toggle-prior-adjust").on("click", (evt) => { + $("table.tsummary").find(`td.prior-adjust, th.prior-adjust-col`).toggle(); +}); $("table.tsummary").append(""); h = $("table.tsummary tr.header"); @@ -291,6 +294,9 @@ oshifts.forEach((s) => { }); +// Column for prior-adjustment (hidden by default) +h.append(`Prior Adjust`); + $(".table-div .worker-row .worker").each((n, tr) => { @@ -332,6 +338,36 @@ $(".table-div .worker-row .worker").each((n, tr) => { } }) + // Build per-shift prior-adjust spans from data-previous-shifts + let prev_attr = jtr.attr("data-previous-shifts"); + let partsHtml = ""; + if (prev_attr) { + try { + let prev_map = (typeof prev_attr === 'object') ? prev_attr : JSON.parse(prev_attr); + let parts = []; + for (let k in prev_map) { + if (!prev_map.hasOwnProperty(k)) continue; + let entry = prev_map[k]; + let adj = null; + if (entry && typeof entry === 'object') { + if ('adjustment' in entry) adj = parseFloat(entry.adjustment); + else if ('allocated' in entry && 'worked' in entry) adj = parseFloat(entry.allocated) - parseFloat(entry.worked); + } else if (Array.isArray(entry) && entry.length >= 2) { + adj = parseFloat(entry[1]) - parseFloat(entry[0]); + } + if (adj && Math.abs(adj) > 0.0001) { + let s = (adj > 0 ? '+' : '') + Number(adj).toFixed(2); + // span includes data attributes for per-shift colouring later + partsHtml += `${k}:${s}`; + } + } + } catch (e) { + // ignore parse errors + } + } + + row.append(`${partsHtml}`) + $("table.tsummary").append(row) }) @@ -380,6 +416,31 @@ oshifts.forEach((shift) => { }); +// Colour each per-shift prior-adjust span independently. +// For each shift, find the maximum absolute adjustment across rows and scale saturation accordingly. +let priorShiftMax = {}; +$("table.tsummary").find("td.prior-adjust .prior-adjust-part").each((n, span) => { + let $span = $(span); + let shift = $span.attr('data-shift'); + let adj = parseFloat($span.attr('data-adj')) || 0; + let absadj = Math.abs(adj); + if (!(shift in priorShiftMax)) priorShiftMax[shift] = 0; + priorShiftMax[shift] = Math.max(priorShiftMax[shift], absadj); +}); + +// Apply colouring to each span using hsv2rgb (green for positive, red for negative) +$("table.tsummary").find("td.prior-adjust .prior-adjust-part").each((n, span) => { + let $span = $(span); + let shift = $span.attr('data-shift'); + let adj = parseFloat($span.attr('data-adj')) || 0; + if (Math.abs(adj) < 1e-9) return; + let max = priorShiftMax[shift] || Math.abs(adj) || 1; + let sat = (max === 0) ? 1 : Math.min(1, Math.abs(adj) / max); + let hue = (adj > 0) ? 120 : 0; // green or red + $span.css({ backgroundColor: hsv2rgb(hue, sat, 1), color: '#000' }); + $span.addClass('no-colour'); +}); + let selectedShifts = new Set(); let shiftColors = {}; diff --git a/rota_generator/shifts.py b/rota_generator/shifts.py index 459bd62..04899cd 100644 --- a/rota_generator/shifts.py +++ b/rota_generator/shifts.py @@ -4898,10 +4898,35 @@ class RotaBuilder(object): sun_count += 1 break # Small HTML summary to be inserted inside the worker td (friendly display) + # Build a small adjustments summary from prior allocations (if any) + prior_map = {} + adjustments_list = [] + for shift in self.get_shifts(): + prev = getattr(worker, "previous_shifts", {}) or {} + if shift.name in prev: + worked, allocated = prev[shift.name] + try: + adj = float(allocated) - float(worked) + except Exception: + adj = 0.0 + prior_map[shift.name] = {"worked": worked, "allocated": allocated, "adjustment": adj} + if adj != 0: + adjustments_list.append(f"{shift.name}:{adj:+g}") + + adjustments_html = "" + if adjustments_list: + adjustments_html = ( + "
" + + "Adjust: " + + ", ".join(adjustments_list) + + "
" + ) + worker_summary_html = ( f"
" f"Sat: {sat_count}" f"   Sun: {sun_count}" + f"{adjustments_html}" f"
" ) @@ -4922,6 +4947,7 @@ class RotaBuilder(object): data-bank-holiday-extra='{bank_holiday_extra}' data-weekend-sat='{weekend_sat}' data-weekend-sun='{weekend_sun}' + data-previous-shifts='{previous_shifts}' data-shift-diff='{shift_diff}' data-shift-diff-summed='{shift_diff_summed}' > @@ -4944,6 +4970,7 @@ class RotaBuilder(object): weekend_sat=sat_count, weekend_sun=sun_count, worker_summary_html=worker_summary_html, + previous_shifts=json.dumps(prior_map), pair=worker.pair, shift_balance_extra=json.dumps(worker.shift_balance_extra), shift_diff=json.dumps(shift_diff_dict),