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 = ``; + + 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"
" )