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),