diff --git a/gen_cons.py b/gen_cons.py
index eddd348..b3a763d 100644
--- a/gen_cons.py
+++ b/gen_cons.py
@@ -166,7 +166,7 @@ def load_workers():
rota_b_assignments = defaultdict(list)
for idx, row in rota_b_df.iterrows():
date_val = row.iloc[0]
- worker_val = row.iloc[1] if len(row) > 1 else None
+ worker_val = row.iloc[1].upper() if len(row) > 1 else None
if pd.isna(date_val) or pd.isna(worker_val):
continue
try:
diff --git a/output/timetable.js b/output/timetable.js
index cda2676..9c9bc91 100644
--- a/output/timetable.js
+++ b/output/timetable.js
@@ -183,16 +183,7 @@ function generateExtra() {
if (sun && sun !== "") sun_count += 1;
}
- worker_td.after(`
- Total shifts: ${total_shifts},
- Weekends: ${weekends_worked},
- Sat: ${sat_count},
- Sun: ${sun_count},
- Nights: ${nights_worked},
- Bank holidays: ${bank_holidays_worked},
- Summed shift diff: ${summed_shift_diff.toPrecision(2)},
-
-
`)
+ // worker-summary will be inserted after we compute prior-adjust badges (sat/sun)
if (summed_shift_diff >= 1) {
worker_td.addClass("large-shift-diff-pos")
@@ -247,8 +238,17 @@ function generateExtra() {
// }
summary_button = $("").click(() => {
- $(table).find(".rota-day").toggleClass("hidden");
- $(table).find(".worker-summary").toggle();
+ let $rotaDays = $(table).find(".rota-day");
+ let $workerSummaries = $(table).find(".worker-summary");
+ // determine current visibility of rota days
+ let rotaVisible = $rotaDays.length ? !$rotaDays.first().hasClass('hidden') : true;
+ if (rotaVisible) {
+ $rotaDays.addClass('hidden');
+ $workerSummaries.show();
+ } else {
+ $rotaDays.removeClass('hidden');
+ $workerSummaries.hide();
+ }
});
@@ -341,6 +341,8 @@ $(".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 = "";
+ let sat_adj = null;
+ let sun_adj = null;
if (prev_attr) {
try {
let prev_map = (typeof prev_attr === 'object') ? prev_attr : JSON.parse(prev_attr);
@@ -349,7 +351,7 @@ $(".table-div .worker-row .worker").each((n, tr) => {
if (!prev_map.hasOwnProperty(k)) continue;
let entry = prev_map[k];
let adj = null;
- if (entry && typeof entry === 'object') {
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
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) {
@@ -361,12 +363,94 @@ $(".table-div .worker-row .worker").each((n, tr) => {
partsHtml += `${k}:${s}`;
}
}
+
+ // compute per-day weekend adjustments if present under 'weekend'
+ if ('weekend' in prev_map) {
+ let w = prev_map['weekend'];
+ if (w && typeof w === 'object' && !Array.isArray(w)) {
+ // case: { 'Sat': {worked:.., allocated:..}, 'Sun': {...} } OR tuples
+ if ('Sat' in w) {
+ let e = w['Sat'];
+ if (e && typeof e === 'object' && 'allocated' in e && 'worked' in e) sat_adj = parseFloat(e.allocated) - parseFloat(e.worked);
+ else if (Array.isArray(e) && e.length >= 2) sat_adj = parseFloat(e[1]) - parseFloat(e[0]);
+ }
+ if ('Sun' in w) {
+ let e = w['Sun'];
+ if (e && typeof e === 'object' && 'allocated' in e && 'worked' in e) sun_adj = parseFloat(e.allocated) - parseFloat(e.worked);
+ else if (Array.isArray(e) && e.length >= 2) sun_adj = parseFloat(e[1]) - parseFloat(e[0]);
+ }
+ if (sat_adj === null && sun_adj === null && '__all__' in w) {
+ let e = w['__all__'];
+ if (Array.isArray(e) && e.length >= 2) {
+ let total = parseFloat(e[1]) - parseFloat(e[0]);
+ sat_adj = sun_adj = total / 2.0;
+ } else if (e && typeof e === 'object' && 'allocated' in e && 'worked' in e) {
+ let total = parseFloat(e.allocated) - parseFloat(e.worked);
+ sat_adj = sun_adj = total / 2.0;
+ }
+ }
+ } else if (Array.isArray(w) && w.length >= 2) {
+ // tuple-style: split evenly
+ let total = parseFloat(w[1]) - parseFloat(w[0]);
+ sat_adj = sun_adj = total / 2.0;
+ }
+ }
+
} catch (e) {
// ignore parse errors
}
}
- row.append(`${partsHtml} | `)
+ // helper to render small coloured adjustment badge
+ function _adjBadge(adj) {
+ if (adj === null || typeof adj === 'undefined' || isNaN(adj)) return '';
+ let adjf = parseFloat(adj);
+ if (!isFinite(adjf)) return '';
+ let sign = (adjf > 0) ? '+' : '';
+ let text = `${sign}${adjf.toFixed(2)}`;
+ let mag = Math.min(1.0, Math.abs(adjf) / 3.0);
+ let bg, color;
+ if (adjf > 0) { bg = `rgba(0,128,0,${mag.toFixed(2)})`; color = '#fff'; }
+ else if (adjf < 0) { bg = `rgba(196,0,0,${mag.toFixed(2)})`; color = '#fff'; }
+ else { bg = 'rgba(128,128,128,0.15)'; color = '#000'; }
+ return ` (${text})`;
+ }
+
+ let sat_adj_html = _adjBadge(sat_adj);
+ let sun_adj_html = _adjBadge(sun_adj);
+
+ // compute Sat / Sun counts for this worker row (ensure variables are defined)
+ let sat_count = 0;
+ let sun_count = 0;
+ try {
+ let $row = jtr.closest('tr');
+ $row.find("td[data-day='Sat']").each((i, td) => {
+ let s = $(td).attr('data-shift');
+ if (s && s !== '') sat_count += 1;
+ });
+ $row.find("td[data-day='Sun']").each((i, td) => {
+ let s = $(td).attr('data-shift');
+ if (s && s !== '') sun_count += 1;
+ });
+ } catch (e) {
+ // fallback: leave counts at 0
+ }
+
+ // Insert worker summary now that we have per-day adjustment badges
+ // Hidden by default so summaries only appear when rota days are hidden
+ let workerSummaryHtml = `
+ Total shifts: ${total_shifts},
+ Weekends: ${weekends_worked},
+ Sat: ${sat_count}${sat_adj_html},
+ Sun: ${sun_count}${sun_adj_html},
+ Nights: ${nights_worked},
+ Bank holidays: ${bank_holidays_worked},
+ Summed shift diff: ${summed_shift_diff.toPrecision(2)},
+
`;
+
+ jtr.after(workerSummaryHtml);
+
+ row.append(`${partsHtml} | `)
$("table.tsummary").append(row)
})
diff --git a/rota_generator/shifts.py b/rota_generator/shifts.py
index b3b135b..d0510de 100644
--- a/rota_generator/shifts.py
+++ b/rota_generator/shifts.py
@@ -4997,10 +4997,66 @@ class RotaBuilder(object):
+ ""
)
+ # Compute per-day (Sat/Sun) adjustments for the 'weekend' shift if provided
+ sat_adj = None
+ sun_adj = None
+ prev_all = getattr(worker, "previous_shifts", {}) or {}
+ weekend_entry = prev_all.get("weekend")
+ if weekend_entry is not None:
+ try:
+ if isinstance(weekend_entry, dict):
+ if "Sat" in weekend_entry:
+ w_worked, w_alloc = weekend_entry["Sat"]
+ sat_adj = float(w_alloc) - float(w_worked)
+ if "Sun" in weekend_entry:
+ w_worked, w_alloc = weekend_entry["Sun"]
+ sun_adj = float(w_alloc) - float(w_worked)
+ if sat_adj is None and sun_adj is None and "__all__" in weekend_entry:
+ w_worked, w_alloc = weekend_entry["__all__"]
+ total = float(w_alloc) - float(w_worked)
+ sat_adj = sun_adj = total / 2.0
+ if sat_adj is None and sun_adj is None:
+ # fallback: sum any tuple values and split
+ worked_sum = sum(v[0] for v in weekend_entry.values() if isinstance(v, (list, tuple)))
+ alloc_sum = sum(v[1] for v in weekend_entry.values() if isinstance(v, (list, tuple)))
+ total = float(alloc_sum) - float(worked_sum)
+ sat_adj = sun_adj = total / 2.0
+ else:
+ # tuple-style entry, split evenly across Sat/Sun
+ w_worked, w_alloc = weekend_entry
+ total = float(w_alloc) - float(w_worked)
+ sat_adj = sun_adj = total / 2.0
+ except Exception:
+ sat_adj = sun_adj = None
+
+ def _adj_html(adj):
+ if adj is None:
+ return ""
+ try:
+ adjf = float(adj)
+ except Exception:
+ return ""
+ sign_text = f"{adjf:+.2f}"
+ # colour intensity scaled by magnitude (cap divisor at 3.0 to avoid overpowering)
+ mag = min(1.0, abs(adjf) / 3.0)
+ if adjf > 0:
+ bg = f"rgba(0,128,0,{mag:.2f})"
+ color = "#fff"
+ elif adjf < 0:
+ bg = f"rgba(196,0,0,{mag:.2f})"
+ color = "#fff"
+ else:
+ bg = "rgba(128,128,128,0.15)"
+ color = "#000"
+ return f" ({sign_text})"
+
+ sat_adj_html = _adj_html(sat_adj)
+ sun_adj_html = _adj_html(sun_adj)
+
worker_summary_html = (
f""
- f"Sat: {sat_count}"
- f" Sun: {sun_count}"
+ f"Sat: {sat_count}{sat_adj_html}"
+ f" Sun: {sun_count}{sun_adj_html}"
f"{adjustments_html}"
f"
"
)