Enhance weekend shift handling by adding per-day adjustments and improving prior allocation parsing
This commit is contained in:
@@ -222,6 +222,7 @@ def load_workers():
|
||||
|
||||
if prev:
|
||||
priors_map[initial] = prev
|
||||
logger.debug("priors_map: {}", priors_map)
|
||||
except FileNotFoundError:
|
||||
print(f"Prior allocations file not found: {priors_path}")
|
||||
except Exception as e:
|
||||
@@ -348,6 +349,8 @@ def load_workers():
|
||||
|
||||
if prev:
|
||||
priors_map[initial] = prev
|
||||
|
||||
logger.debug("priors_map after per-day merge: {}", priors_map)
|
||||
except FileNotFoundError:
|
||||
print(f"Per-day prior file not found: {priordays_path}")
|
||||
except Exception as e:
|
||||
|
||||
+34
-1
@@ -339,7 +339,9 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
})
|
||||
|
||||
// Build per-shift prior-adjust spans from data-previous-shifts
|
||||
// Try both the raw attribute and jQuery-parsed data for robustness
|
||||
let prev_attr = jtr.attr("data-previous-shifts");
|
||||
if (!prev_attr) prev_attr = jtr.data("previous-shifts");
|
||||
let partsHtml = "";
|
||||
let sat_adj = null;
|
||||
let sun_adj = null;
|
||||
@@ -347,8 +349,10 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
try {
|
||||
let prev_map = (typeof prev_attr === 'object') ? prev_attr : JSON.parse(prev_attr);
|
||||
let parts = [];
|
||||
// Build parts for non-weekend shifts first; we'll always render weekend per-day below.
|
||||
for (let k in prev_map) {
|
||||
if (!prev_map.hasOwnProperty(k)) continue;
|
||||
if (k === 'weekend') continue;
|
||||
let entry = prev_map[k];
|
||||
let adj = null;
|
||||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||
@@ -367,8 +371,8 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
// compute per-day weekend adjustments if present under 'weekend'
|
||||
if ('weekend' in prev_map) {
|
||||
let w = prev_map['weekend'];
|
||||
// Prefer explicit per-day entries when present
|
||||
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);
|
||||
@@ -379,6 +383,20 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
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 no per-day entries but there is an overall weekend value, split it evenly
|
||||
if (sat_adj === null && sun_adj === null && 'allocated' in w && 'worked' in w) {
|
||||
try {
|
||||
let total = parseFloat(w.allocated) - parseFloat(w.worked);
|
||||
if (isFinite(total)) {
|
||||
sat_adj = sun_adj = total / 2.0;
|
||||
}
|
||||
} catch (err) {
|
||||
// leave as null on parse error
|
||||
}
|
||||
}
|
||||
|
||||
// Handle a '__all__' bucket (tuple or object) as fallback
|
||||
if (sat_adj === null && sun_adj === null && '__all__' in w) {
|
||||
let e = w['__all__'];
|
||||
if (Array.isArray(e) && e.length >= 2) {
|
||||
@@ -394,6 +412,16 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
let total = parseFloat(w[1]) - parseFloat(w[0]);
|
||||
sat_adj = sun_adj = total / 2.0;
|
||||
}
|
||||
|
||||
// Always append per-day prior-adjust parts for weekend so the section is per-day
|
||||
if (sat_adj !== null && Math.abs(sat_adj) > 0.0001) {
|
||||
let s = (sat_adj > 0 ? '+' : '') + Number(sat_adj).toFixed(2);
|
||||
partsHtml += `<span class='prior-adjust-part' data-shift='weekend' data-day='Sat' data-adj='${sat_adj}' style='display:inline-block;padding:0 6px;margin-right:6px;border-radius:3px'>Sat:${s}</span>`;
|
||||
}
|
||||
if (sun_adj !== null && Math.abs(sun_adj) > 0.0001) {
|
||||
let s = (sun_adj > 0 ? '+' : '') + Number(sun_adj).toFixed(2);
|
||||
partsHtml += `<span class='prior-adjust-part' data-shift='weekend' data-day='Sun' data-adj='${sun_adj}' style='display:inline-block;padding:0 6px;margin-right:6px;border-radius:3px'>Sun:${s}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
@@ -408,7 +436,9 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
if (!isFinite(adjf)) return '';
|
||||
let sign = (adjf > 0) ? '+' : '';
|
||||
let text = `${sign}${adjf.toFixed(2)}`;
|
||||
// scale alpha by magnitude but ensure a minimum so small adjustments remain visible
|
||||
let mag = Math.min(1.0, Math.abs(adjf) / 3.0);
|
||||
if (mag < 0.12) mag = 0.12;
|
||||
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'; }
|
||||
@@ -436,6 +466,9 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
||||
// fallback: leave counts at 0
|
||||
}
|
||||
|
||||
console.log("Computed weekend counts and adjustments for worker:", jtr.data("worker"));
|
||||
console.log(`sat_count: ${sat_count}, sun_count: ${sun_count}, sat_adj: ${sat_adj}, sun_adj: ${sun_adj}`);
|
||||
|
||||
// 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 = `<div class='worker-summary auto-generated' style='display: none;'>
|
||||
|
||||
+62
-14
@@ -4967,27 +4967,72 @@ class RotaBuilder(object):
|
||||
prev = getattr(worker, "previous_shifts", {}) or {}
|
||||
if shift.name in prev:
|
||||
entry = prev[shift.name]
|
||||
# entry may be a tuple (worked, allocated) or a dict with per-day entries
|
||||
# entry may be:
|
||||
# - a tuple/list: (worked, allocated)
|
||||
# - a dict with per-day entries (e.g., {'Sat': (w,a), 'Sun': (w,a)})
|
||||
# - a dict with an '__all__' key or other buckets
|
||||
if isinstance(entry, dict):
|
||||
# prefer a day-specific entry if available (variable `d` holds the day name in scope)
|
||||
if d in entry:
|
||||
worked, allocated = entry[d]
|
||||
elif "__all__" in entry:
|
||||
worked, allocated = entry["__all__"]
|
||||
# If the dict already contains per-day keys (Sat/Sun) or an explicit
|
||||
# '__all__' bucket, preserve the dict as-is so downstream code
|
||||
# can render per-day adjustments. Otherwise, attempt to aggregate
|
||||
# any tuple-like values found in the dict into totals.
|
||||
if any(k in entry for k in ("Sat", "Sun")) or "__all__" in entry:
|
||||
prior_map[shift.name] = entry
|
||||
# also compute an overall adjustment for the adjustments list
|
||||
try:
|
||||
# compute totals by summing tuple/list values where present
|
||||
worked = sum(v[0] for v in entry.values() if isinstance(v, (list, tuple)))
|
||||
allocated = sum(v[1] for v in entry.values() if isinstance(v, (list, tuple)))
|
||||
adj = float(allocated) - float(worked)
|
||||
except Exception:
|
||||
adj = 0
|
||||
else:
|
||||
# fallback: sum any tuple values found in the dict
|
||||
worked = sum(v[0] for v in entry.values() if isinstance(v, (list, tuple)))
|
||||
allocated = sum(v[1] for v in entry.values() if isinstance(v, (list, tuple)))
|
||||
try:
|
||||
worked = sum(v[0] for v in entry.values() if isinstance(v, (list, tuple)))
|
||||
allocated = sum(v[1] for v in entry.values() if isinstance(v, (list, tuple)))
|
||||
adj = float(allocated) - float(worked)
|
||||
except Exception:
|
||||
worked = 0
|
||||
allocated = 0
|
||||
adj = 0
|
||||
prior_map[shift.name] = dict(worked=worked, allocated=allocated, adjustment=adj)
|
||||
else:
|
||||
worked, allocated = entry
|
||||
try:
|
||||
adj = float(allocated) - float(worked)
|
||||
except Exception:
|
||||
adj = 0
|
||||
prior_map[shift.name] = dict(worked=worked, allocated=allocated, adjustment=adj)
|
||||
# tuple/list style entry
|
||||
try:
|
||||
worked, allocated = entry
|
||||
adj = float(allocated) - float(worked)
|
||||
except Exception:
|
||||
worked = 0
|
||||
allocated = 0
|
||||
adj = 0
|
||||
prior_map[shift.name] = dict(worked=worked, allocated=allocated, adjustment=adj)
|
||||
|
||||
if adj != 0:
|
||||
adjustments_list.append(f"{shift.name}:{adj:+g}")
|
||||
|
||||
# If weekend prior is present as a top-level worked/allocated dict (not per-day),
|
||||
# expand it into per-day entries so Sat/Sun adjustments are always available.
|
||||
if 'weekend' in prior_map:
|
||||
w = prior_map['weekend']
|
||||
# detect the simple dict shape produced earlier: {'worked': x, 'allocated': y, 'adjustment': z}
|
||||
if isinstance(w, dict) and 'worked' in w and 'allocated' in w and not ('Sat' in w or 'Sun' in w or '__all__' in w):
|
||||
try:
|
||||
worked = float(w.get('worked', 0))
|
||||
allocated = float(w.get('allocated', 0))
|
||||
# split evenly across Sat and Sun
|
||||
sat_worked = worked / 2.0
|
||||
sun_worked = worked - sat_worked
|
||||
sat_alloc = allocated / 2.0
|
||||
sun_alloc = allocated - sat_alloc
|
||||
prior_map['weekend'] = {
|
||||
'Sat': (sat_worked, sat_alloc),
|
||||
'Sun': (sun_worked, sun_alloc),
|
||||
}
|
||||
except Exception:
|
||||
# leave as-is if parsing fails
|
||||
pass
|
||||
|
||||
adjustments_html = ""
|
||||
if adjustments_list:
|
||||
adjustments_html = (
|
||||
@@ -5039,6 +5084,9 @@ class RotaBuilder(object):
|
||||
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)
|
||||
# ensure a minimum alpha so small adjustments remain visible
|
||||
if mag < 0.12:
|
||||
mag = 0.12
|
||||
if adjf > 0:
|
||||
bg = f"rgba(0,128,0,{mag:.2f})"
|
||||
color = "#fff"
|
||||
|
||||
Reference in New Issue
Block a user