Add weekend shift adjustment calculations and update worker summary display
This commit is contained in:
+1
-1
@@ -166,7 +166,7 @@ def load_workers():
|
|||||||
rota_b_assignments = defaultdict(list)
|
rota_b_assignments = defaultdict(list)
|
||||||
for idx, row in rota_b_df.iterrows():
|
for idx, row in rota_b_df.iterrows():
|
||||||
date_val = row.iloc[0]
|
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):
|
if pd.isna(date_val) or pd.isna(worker_val):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
|||||||
+98
-14
@@ -183,16 +183,7 @@ function generateExtra() {
|
|||||||
if (sun && sun !== "") sun_count += 1;
|
if (sun && sun !== "") sun_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
worker_td.after(`<div class='worker-summary auto-generated'>
|
// worker-summary will be inserted after we compute prior-adjust badges (sat/sun)
|
||||||
<span>Total shifts: ${total_shifts}, </span>
|
|
||||||
<span>Weekends: ${weekends_worked}, </span>
|
|
||||||
<span>Sat: ${sat_count}, </span>
|
|
||||||
<span>Sun: ${sun_count}, </span>
|
|
||||||
<span>Nights: ${nights_worked}, </span>
|
|
||||||
<span>Bank holidays: ${bank_holidays_worked}, </span>
|
|
||||||
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
|
|
||||||
|
|
||||||
</div>`)
|
|
||||||
|
|
||||||
if (summed_shift_diff >= 1) {
|
if (summed_shift_diff >= 1) {
|
||||||
worker_td.addClass("large-shift-diff-pos")
|
worker_td.addClass("large-shift-diff-pos")
|
||||||
@@ -247,8 +238,17 @@ function generateExtra() {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
summary_button = $("<button class='auto-generated'>Toggle Summary</button>").click(() => {
|
summary_button = $("<button class='auto-generated'>Toggle Summary</button>").click(() => {
|
||||||
$(table).find(".rota-day").toggleClass("hidden");
|
let $rotaDays = $(table).find(".rota-day");
|
||||||
$(table).find(".worker-summary").toggle();
|
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
|
// Build per-shift prior-adjust spans from data-previous-shifts
|
||||||
let prev_attr = jtr.attr("data-previous-shifts");
|
let prev_attr = jtr.attr("data-previous-shifts");
|
||||||
let partsHtml = "";
|
let partsHtml = "";
|
||||||
|
let sat_adj = null;
|
||||||
|
let sun_adj = null;
|
||||||
if (prev_attr) {
|
if (prev_attr) {
|
||||||
try {
|
try {
|
||||||
let prev_map = (typeof prev_attr === 'object') ? prev_attr : JSON.parse(prev_attr);
|
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;
|
if (!prev_map.hasOwnProperty(k)) continue;
|
||||||
let entry = prev_map[k];
|
let entry = prev_map[k];
|
||||||
let adj = null;
|
let adj = null;
|
||||||
if (entry && typeof entry === 'object') {
|
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||||
if ('adjustment' in entry) adj = parseFloat(entry.adjustment);
|
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 ('allocated' in entry && 'worked' in entry) adj = parseFloat(entry.allocated) - parseFloat(entry.worked);
|
||||||
} else if (Array.isArray(entry) && entry.length >= 2) {
|
} else if (Array.isArray(entry) && entry.length >= 2) {
|
||||||
@@ -361,12 +363,94 @@ $(".table-div .worker-row .worker").each((n, tr) => {
|
|||||||
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>`;
|
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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} catch (e) {
|
||||||
// ignore parse errors
|
// ignore parse errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
row.append(`<td class='prior-adjust' style='display:none'>${partsHtml}</td>`)
|
// 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 ` <span class='weekend-day-adjust' style='margin-left:6px; padding:1px 6px; border-radius:4px; background:${bg}; color:${color}; font-weight:600; font-size:0.85em;'>(${text})</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<div class='worker-summary auto-generated' style='display: none;'>
|
||||||
|
<span>Total shifts: ${total_shifts}, </span>
|
||||||
|
<span>Weekends: ${weekends_worked}, </span>
|
||||||
|
<span>Sat: ${sat_count}${sat_adj_html}, </span>
|
||||||
|
<span>Sun: ${sun_count}${sun_adj_html}, </span>
|
||||||
|
<span>Nights: ${nights_worked}, </span>
|
||||||
|
<span>Bank holidays: ${bank_holidays_worked}, </span>
|
||||||
|
<span class='shift-diff-span'>Summed shift diff: ${summed_shift_diff.toPrecision(2)}, </span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
jtr.after(workerSummaryHtml);
|
||||||
|
|
||||||
|
row.append(`<td class='prior-adjust' style='display:none'>${partsHtml}</td>`)
|
||||||
|
|
||||||
$("table.tsummary").append(row)
|
$("table.tsummary").append(row)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4997,10 +4997,66 @@ class RotaBuilder(object):
|
|||||||
+ "</div>"
|
+ "</div>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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" <span class='weekend-day-adjust' style='margin-left:6px; padding:1px 6px; border-radius:4px; background:{bg}; color:{color}; font-weight:600; font-size:0.85em;'>({sign_text})</span>"
|
||||||
|
|
||||||
|
sat_adj_html = _adj_html(sat_adj)
|
||||||
|
sun_adj_html = _adj_html(sun_adj)
|
||||||
|
|
||||||
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>{sat_adj_html}</span>"
|
||||||
f" <span class=\'weekend-sun\'>Sun: <strong>{sun_count}</strong></span>"
|
f" <span class='weekend-sun'>Sun: <strong>{sun_count}</strong>{sun_adj_html}</span>"
|
||||||
f"{adjustments_html}"
|
f"{adjustments_html}"
|
||||||
f"</div>"
|
f"</div>"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user