Add prior adjustments summary to worker display and enhance data attributes
This commit is contained in:
+62
-1
@@ -273,10 +273,13 @@ if (shifts.size < 1) {
|
|||||||
|
|
||||||
oshifts = Array.from(shifts).sort()
|
oshifts = Array.from(shifts).sort()
|
||||||
|
|
||||||
$("body").append("<div><button id='show-colour-diff'>Show colour diff</button><table class='tsummary'></table></div>")
|
$("body").append("<div><button id='show-colour-diff'>Show colour diff</button><button id='toggle-prior-adjust' style='margin-left:10px;'>Toggle Prior Adjust</button><table class='tsummary'></table></div>")
|
||||||
$("#show-colour-diff").on("click", (evt) => {
|
$("#show-colour-diff").on("click", (evt) => {
|
||||||
$("table.tsummary").find(`td.target-assigned`).toggleClass("no-colour");
|
$("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("<tr class='header'></tr>");
|
$("table.tsummary").append("<tr class='header'></tr>");
|
||||||
h = $("table.tsummary tr.header");
|
h = $("table.tsummary tr.header");
|
||||||
@@ -291,6 +294,9 @@ oshifts.forEach((s) => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Column for prior-adjustment (hidden by default)
|
||||||
|
h.append(`<th class='prior-adjust-col' style='display:none'>Prior Adjust</th>`);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$(".table-div .worker-row .worker").each((n, tr) => {
|
$(".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 += `<span class='prior-adjust-part' data-shift='${k}' data-adj='${adj}' style='display:inline-block;padding:0 6px;margin-right:6px;border-radius:3px'>${k}:${s}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row.append(`<td class='prior-adjust' style='display:none'>${partsHtml}</td>`)
|
||||||
|
|
||||||
$("table.tsummary").append(row)
|
$("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 selectedShifts = new Set();
|
||||||
|
|
||||||
let shiftColors = {};
|
let shiftColors = {};
|
||||||
|
|||||||
@@ -4898,10 +4898,35 @@ class RotaBuilder(object):
|
|||||||
sun_count += 1
|
sun_count += 1
|
||||||
break
|
break
|
||||||
# Small HTML summary to be inserted inside the worker td (friendly display)
|
# 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 = (
|
||||||
|
"<div class='worker-adjustments auto-generated' style='margin-top:3px; font-size:0.85em; color:#666;'>"
|
||||||
|
+ "Adjust: "
|
||||||
|
+ ", ".join(adjustments_list)
|
||||||
|
+ "</div>"
|
||||||
|
)
|
||||||
|
|
||||||
worker_summary_html = (
|
worker_summary_html = (
|
||||||
f"<div class='worker-summary auto-generated' style='margin-top:4px; font-size:0.9em; color:#333;'>"
|
f"<div class='worker-summary auto-generated' style='margin-top:4px; font-size:0.9em; color:#333;'>"
|
||||||
f"<span class=\'weekend-sat\'>Sat: <strong>{sat_count}</strong></span>"
|
f"<span class=\'weekend-sat\'>Sat: <strong>{sat_count}</strong></span>"
|
||||||
f" <span class=\'weekend-sun\'>Sun: <strong>{sun_count}</strong></span>"
|
f" <span class=\'weekend-sun\'>Sun: <strong>{sun_count}</strong></span>"
|
||||||
|
f"{adjustments_html}"
|
||||||
f"</div>"
|
f"</div>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4922,6 +4947,7 @@ class RotaBuilder(object):
|
|||||||
data-bank-holiday-extra='{bank_holiday_extra}'
|
data-bank-holiday-extra='{bank_holiday_extra}'
|
||||||
data-weekend-sat='{weekend_sat}'
|
data-weekend-sat='{weekend_sat}'
|
||||||
data-weekend-sun='{weekend_sun}'
|
data-weekend-sun='{weekend_sun}'
|
||||||
|
data-previous-shifts='{previous_shifts}'
|
||||||
data-shift-diff='{shift_diff}'
|
data-shift-diff='{shift_diff}'
|
||||||
data-shift-diff-summed='{shift_diff_summed}'
|
data-shift-diff-summed='{shift_diff_summed}'
|
||||||
>
|
>
|
||||||
@@ -4944,6 +4970,7 @@ class RotaBuilder(object):
|
|||||||
weekend_sat=sat_count,
|
weekend_sat=sat_count,
|
||||||
weekend_sun=sun_count,
|
weekend_sun=sun_count,
|
||||||
worker_summary_html=worker_summary_html,
|
worker_summary_html=worker_summary_html,
|
||||||
|
previous_shifts=json.dumps(prior_map),
|
||||||
pair=worker.pair,
|
pair=worker.pair,
|
||||||
shift_balance_extra=json.dumps(worker.shift_balance_extra),
|
shift_balance_extra=json.dumps(worker.shift_balance_extra),
|
||||||
shift_diff=json.dumps(shift_diff_dict),
|
shift_diff=json.dumps(shift_diff_dict),
|
||||||
|
|||||||
Reference in New Issue
Block a user