diff --git a/output/timetable.js b/output/timetable.js
index 9231436..d9bfc20 100644
--- a/output/timetable.js
+++ b/output/timetable.js
@@ -342,6 +342,127 @@ oshifts.forEach((s) => {
});
+// Render formatted Shifts settings and Worker details with filtering
+function renderShiftSettings() {
+ const $container = $("#shifts-container");
+ if (!$container.length) return;
+ let shiftsJson = $container.data("shifts-json");
+ try {
+ if (typeof shiftsJson === 'string') shiftsJson = JSON.parse(shiftsJson);
+ } catch (e) {
+ // fallback: no structured data
+ shiftsJson = null;
+ }
+
+ const $table = $("
");
+ $table.append(`| Name | Sites | Days | Balance offset |
`);
+ if (shiftsJson && Array.isArray(shiftsJson)) {
+ shiftsJson.forEach(s => {
+ const sites = (s.sites || []).join(", ");
+ const days = (s.days || []).join(", ");
+ $table.append(`| ${s.name} | ${sites} | ${days} | ${s.balance_offset ?? ''} |
`);
+ });
+ } else {
+ // fallback to existing content
+ const html = $("#shifts-table").html();
+ $("#shifts-table").empty().append(html);
+ }
+ $("#shifts-table").empty().append($table);
+
+ // filter input
+ $("#shift-filter").on("input", (e) => {
+ const v = $(e.target).val().toLowerCase();
+ $(".shift-row").each((i, el) => {
+ const name = $(el).data('shift-name') + '';
+ $(el).toggle(name.toLowerCase().indexOf(v) !== -1);
+ });
+ // update shift select in worker panel if present
+ renderWorkerFilterOptions();
+ });
+}
+
+function renderWorkerFilterOptions() {
+ const $wd = $("#worker_details");
+ if (!$wd.length) return;
+ let workers = $wd.data("workers");
+ try { if (typeof workers === 'string') workers = JSON.parse(workers); } catch (e) { workers = null; }
+ // populate shift select with shifts present in current shifts table
+ const shiftNames = [];
+ $(".shift-row:visible").each((i, el) => shiftNames.push($(el).data('shift-name')));
+ const $select = $("#worker-shift-select");
+ if ($select.length === 0) {
+ $("#worker_details").prepend('
');
+ }
+ const $sel = $("#worker-shift-select");
+ $sel.empty().append('');
+ shiftNames.forEach(s => $sel.append(``));
+
+ // attach events
+ $("#worker-filter").off('input').on('input', () => renderWorkerList());
+ $sel.off('change').on('change', () => renderWorkerList());
+}
+
+function renderWorkerList() {
+ const $wd = $("#worker_details");
+ if (!$wd.length) return;
+ let workers = $wd.data("workers");
+ try { if (typeof workers === 'string') workers = JSON.parse(workers); } catch (e) { workers = null; }
+ const filter = ($("#worker-filter").val() || '').toLowerCase();
+ const shiftFilter = ($("#worker-shift-select").val() || '');
+
+ let $list = $("#worker-list");
+ if ($list.length === 0) {
+ $list = $("");
+ $("#worker_details").append($list);
+ }
+ $list.empty();
+ if (!workers) return;
+ workers.forEach(w => {
+ const name = (w.name || '') + '';
+ const site = (w.site || '') + '';
+ if (filter && name.toLowerCase().indexOf(filter) === -1 && site.toLowerCase().indexOf(filter) === -1) return;
+ // if shiftFilter, ensure worker has target or assigned for that shift
+ // attempt to read per-worker targets and counts from DOM if not present in JSON
+ let targets = w.shift_target_number || {};
+ let counts = w.shift_counts || {};
+ const domRow = $(`td.worker[data-worker='${name}']`).first();
+ if (domRow && domRow.length) {
+ try {
+ const domTargets = domRow.data('worker-targets');
+ if (domTargets && Object.keys(targets).length === 0) targets = domTargets;
+ } catch (e) {}
+ try {
+ const domCounts = domRow.data('shift-counts');
+ if (domCounts && Object.keys(counts).length === 0) counts = domCounts;
+ } catch (e) {}
+ }
+ if (shiftFilter) {
+ const assigned = (counts && counts[shiftFilter]) ? counts[shiftFilter] : 0;
+ const hasTarget = Object.keys(targets || {}).indexOf(shiftFilter) !== -1 && (targets[shiftFilter] || 0) > 0;
+ const hasAssigned = assigned > 0;
+ if (!hasTarget && !hasAssigned) return;
+ }
+ const fte = w.fte || '';
+ const card = $(`${name} ${site}FTE: ${fte}
`);
+ // show shift targets summary
+ if (w.shift_target_number) {
+ const tbl = $("");
+ Object.keys(w.shift_target_number).forEach(s => {
+ const t = w.shift_target_number[s];
+ const c = (w.shift_counts && w.shift_counts[s]) ? w.shift_counts[s] : 0;
+ tbl.append(`| ${s} | ${c} assigned | target ${parseFloat(t).toFixed(2)} |
`);
+ });
+ card.append(tbl);
+ }
+ $list.append(card);
+ });
+}
+
+// initialize panels
+renderShiftSettings();
+renderWorkerFilterOptions();
+renderWorkerList();
+
// Column for prior-adjustment (hidden by default)
h.append(`Prior Adjust | `);
diff --git a/rota_generator/shifts.py b/rota_generator/shifts.py
index 966fafc..34dcc88 100644
--- a/rota_generator/shifts.py
+++ b/rota_generator/shifts.py
@@ -5722,9 +5722,14 @@ class RotaBuilder(object):
Shifts settings
-
+
+
+
+
+
{"
".join(str(i) for i in self.shifts)}
+
Worker details