Add functionality to export timetable.js and enhance worker list rendering with detailed information
This commit is contained in:
@@ -694,6 +694,9 @@ def main(
|
|||||||
subprocess.run(
|
subprocess.run(
|
||||||
["scp", Rota.exported_rota_file, "ross@46.101.13.46:proc/proc-rota/output/"]
|
["scp", Rota.exported_rota_file, "ross@46.101.13.46:proc/proc-rota/output/"]
|
||||||
)
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["scp", "output/timetable.js", "ross@46.101.13.46:proc/proc-rota/output/"]
|
||||||
|
)
|
||||||
|
|
||||||
if suspend_on_finish:
|
if suspend_on_finish:
|
||||||
os.system("systemctl suspend")
|
os.system("systemctl suspend")
|
||||||
|
|||||||
+176
-25
@@ -403,36 +403,50 @@ function renderWorkerFilterOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderWorkerList() {
|
function renderWorkerList() {
|
||||||
console.log("Rendering worker list with filters");
|
|
||||||
const $wd = $("#worker_details");
|
const $wd = $("#worker_details");
|
||||||
console.log("Worker details container:", $wd.length ? $wd.get(0) : "not found");
|
|
||||||
if (!$wd.length) return;
|
if (!$wd.length) return;
|
||||||
let workers = $wd.data("workers");
|
let workers = $wd.data("workers");
|
||||||
try { if (typeof workers === 'string') workers = JSON.parse(workers); } catch (e) { workers = null; }
|
try { if (typeof workers === 'string') workers = JSON.parse(workers); } catch (e) { workers = null; }
|
||||||
const filter = ($("#worker-filter").val() || '').toLowerCase();
|
const filter = ($("#worker-filter").val() || '').toLowerCase();
|
||||||
console.log("Text filter:", filter);
|
|
||||||
const shiftFilter = ($("#worker-shift-select").val() || '');
|
const shiftFilter = ($("#worker-shift-select").val() || '');
|
||||||
|
|
||||||
|
// Parse the original pre block into per-worker text blocks so rich details are preserved.
|
||||||
|
const detailsByName = {};
|
||||||
|
const preText = (($wd.find("pre").first().text() || '') + '').trim();
|
||||||
|
const escapeHtml = (text) => String(text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
if (preText.length > 0) {
|
||||||
|
preText.split(/\n\s*\n(?=Name:\s*)/g).forEach(block => {
|
||||||
|
const trimmed = (block || '').trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
const nameMatch = trimmed.match(/^Name:\s*(.+)$/m);
|
||||||
|
if (!nameMatch) return;
|
||||||
|
detailsByName[nameMatch[1].trim()] = trimmed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let $list = $("#worker-list");
|
let $list = $("#worker-list");
|
||||||
if ($list.length === 0) {
|
if ($list.length === 0) {
|
||||||
$list = $("<div id='worker-list' style='margin-top:8px;'></div>");
|
$list = $("<div id='worker-list' style='margin-top:8px;'></div>");
|
||||||
$("#worker_details").append($list);
|
$("#worker_details").append($list);
|
||||||
}
|
}
|
||||||
|
// Hide the static pre block since we render interactive cards
|
||||||
|
$("#worker_details pre").hide();
|
||||||
|
$list.show();
|
||||||
$list.empty();
|
$list.empty();
|
||||||
if (!workers) return;
|
if (!workers) return;
|
||||||
workers.forEach(w => {
|
workers.forEach(w => {
|
||||||
console.log("Processing worker:", w.name);
|
|
||||||
const name = (w.name || '') + '';
|
const name = (w.name || '') + '';
|
||||||
const displayName = (w.display_name || '') + '';
|
|
||||||
const site = (w.site || '') + '';
|
const site = (w.site || '') + '';
|
||||||
const searchName = (displayName || name).toLowerCase();
|
if (filter.length > 0) {
|
||||||
console.log(`Worker "${name}" at site "${site}" with display name "${displayName}"`);
|
const matchesName = name.toLowerCase().indexOf(filter) !== -1;
|
||||||
if (filter) {
|
|
||||||
const matchesName = searchName.indexOf(filter) !== -1;
|
|
||||||
const matchesSite = site.toLowerCase().indexOf(filter) !== -1;
|
const matchesSite = site.toLowerCase().indexOf(filter) !== -1;
|
||||||
if (!matchesName && !matchesSite) return;
|
if (!matchesName && !matchesSite) return;
|
||||||
}
|
}
|
||||||
console.log(`Worker "${name}" passed text filter`);
|
|
||||||
// if shiftFilter, ensure worker has target or assigned for that shift
|
// 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
|
// attempt to read per-worker targets and counts from DOM if not present in JSON
|
||||||
let targets = w.shift_target_number || {};
|
let targets = w.shift_target_number || {};
|
||||||
@@ -455,38 +469,62 @@ function renderWorkerList() {
|
|||||||
if (!hasTarget && !hasAssigned) return;
|
if (!hasTarget && !hasAssigned) return;
|
||||||
}
|
}
|
||||||
const fte = w.fte || '';
|
const fte = w.fte || '';
|
||||||
const labelName = displayName || name;
|
|
||||||
const grade = w.grade || '';
|
const grade = w.grade || '';
|
||||||
const id = w.id ? String(w.id).substring(0, 8) : '';
|
const id = w.id ? String(w.id).substring(0, 8) : '';
|
||||||
const card = $(`<div class='worker-card'><div class='worker-header'><strong>${labelName}</strong> <span class='worker-id'>${id}</span></div><div class='worker-meta'><span class='worker-site'>${site}</span> • <span class='worker-grade'>Grade: ${grade}</span> • <span class='worker-fte'>FTE: ${fte}%</span></div></div>`);
|
const card = $(`<div class='worker-card'><div class='worker-header'><strong>${name}</strong> <span class='worker-id'>${id}</span></div><div class='worker-meta'><span class='worker-site'>${site}</span> • <span class='worker-grade'>Grade: ${grade}</span> • <span class='worker-fte'>FTE: ${fte}%</span></div></div>`);
|
||||||
// show shift targets summary
|
// show shift targets summary
|
||||||
if (w.shift_target_number && Object.keys(w.shift_target_number).length > 0) {
|
if (targets && Object.keys(targets).length > 0) {
|
||||||
const tbl = $("<table class='worker-shifts-table'></table>");
|
const tbl = $("<table class='worker-shifts-table'></table>");
|
||||||
const header = $("<thead><tr><th>Shift</th><th class='text-right'>Assigned</th><th class='text-right'>Target</th></tr></thead>");
|
const header = $("<thead><tr><th>Shift</th><th class='text-right'>Assigned</th><th class='text-right'>Target</th></tr></thead>");
|
||||||
tbl.append(header);
|
tbl.append(header);
|
||||||
const tbody = $("<tbody></tbody>");
|
const tbody = $("<tbody></tbody>");
|
||||||
Object.keys(w.shift_target_number).forEach(s => {
|
Object.keys(targets).forEach(s => {
|
||||||
const t = w.shift_target_number[s];
|
const t = targets[s];
|
||||||
const c = (w.shift_counts && w.shift_counts[s]) ? w.shift_counts[s] : 0;
|
const c = (counts && counts[s]) ? counts[s] : 0;
|
||||||
tbody.append(`<tr><td>${s}</td><td class='text-right'>${c}</td><td class='text-right'>${parseFloat(t).toFixed(2)}</td></tr>`);
|
tbody.append(`<tr><td>${s}</td><td class='text-right'>${c}</td><td class='text-right'>${parseFloat(t).toFixed(2)}</td></tr>`);
|
||||||
});
|
});
|
||||||
tbl.append(tbody);
|
tbl.append(tbody);
|
||||||
card.append(tbl);
|
card.append(tbl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include the original full worker detail text block so no information is lost.
|
||||||
|
const fullDetails = detailsByName[name] || '';
|
||||||
|
if (fullDetails) {
|
||||||
|
card.append(`<details class='worker-full-details'><summary>Full details</summary><pre>${escapeHtml(fullDetails)}</pre></details>`);
|
||||||
|
}
|
||||||
$list.append(card);
|
$list.append(card);
|
||||||
});
|
});
|
||||||
console.log($("#worker-list"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add CSS for worker details styling and dark mode
|
// Add CSS for worker details styling and comprehensive dark mode support
|
||||||
if ($('#worker-details-style').length === 0) {
|
if ($('#worker-details-style').length === 0) {
|
||||||
$('head').append(`<style id='worker-details-style'>
|
$('head').append(`<style id='worker-details-style'>
|
||||||
|
/* ===== Light mode defaults ===== */
|
||||||
|
body {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
#worker_details {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
#worker-filter, #worker-shift-select {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
#worker-filter:focus, #worker-shift-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
box-shadow: 0 0 4px rgba(0,102,204,0.3);
|
||||||
|
}
|
||||||
.worker-card {
|
.worker-card {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
transition: background-color 0.2s, border-color 0.2s;
|
transition: background-color 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
.worker-card:hover {
|
.worker-card:hover {
|
||||||
@@ -502,6 +540,7 @@ if ($('#worker-details-style').length === 0) {
|
|||||||
.worker-header strong {
|
.worker-header strong {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.worker-id {
|
.worker-id {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
@@ -521,44 +560,147 @@ if ($('#worker-details-style').length === 0) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.worker-shifts-table thead {
|
.worker-shifts-table thead {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.worker-shifts-table th, .worker-shifts-table td {
|
.worker-shifts-table th, .worker-shifts-table td {
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.worker-shifts-table tr:nth-child(even) {
|
.worker-shifts-table tr:nth-child(even) {
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
.worker-full-details {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.worker-full-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.worker-full-details pre {
|
||||||
|
margin-top: 6px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
.text-right {
|
.text-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
pre {
|
||||||
.worker-card {
|
background: #f5f5f5;
|
||||||
|
color: #000;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
input, select, textarea {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
table th {
|
||||||
|
background: #f9f9f9;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
border-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Dark mode overrides (toggled via html.dark-mode class) ===== */
|
||||||
|
html.dark-mode body {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
html.dark-mode #worker_details {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
html.dark-mode #worker-filter, html.dark-mode #worker-shift-select {
|
||||||
|
background: #3d3d3d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
html.dark-mode #worker-filter:focus, html.dark-mode #worker-shift-select:focus {
|
||||||
|
border-color: #0088ff;
|
||||||
|
box-shadow: 0 0 4px rgba(0,136,255,0.5);
|
||||||
|
}
|
||||||
|
html.dark-mode .worker-card {
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
border-color: #444;
|
border-color: #444;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
.worker-card:hover {
|
html.dark-mode .worker-card:hover {
|
||||||
background: #333;
|
background: #333;
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
}
|
}
|
||||||
.worker-meta, .worker-id {
|
html.dark-mode .worker-header strong {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
html.dark-mode .worker-meta, html.dark-mode .worker-id {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
.worker-shifts-table thead {
|
html.dark-mode .worker-shifts-table {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
html.dark-mode .worker-shifts-table thead {
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
.worker-shifts-table th, .worker-shifts-table td {
|
html.dark-mode .worker-shifts-table th, html.dark-mode .worker-shifts-table td {
|
||||||
border-color: #444;
|
border-color: #444;
|
||||||
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
.worker-shifts-table tr:nth-child(even) {
|
html.dark-mode .worker-shifts-table tr:nth-child(even) {
|
||||||
background: #252525;
|
background: #252525;
|
||||||
}
|
}
|
||||||
|
html.dark-mode .worker-full-details summary {
|
||||||
|
color: #cfd8e3;
|
||||||
|
}
|
||||||
|
html.dark-mode pre {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
html.dark-mode input, html.dark-mode select, html.dark-mode textarea {
|
||||||
|
background: #3d3d3d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
html.dark-mode table {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
html.dark-mode table th {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
html.dark-mode table td {
|
||||||
|
border-color: #444;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
html.dark-mode h1, html.dark-mode h2, html.dark-mode h3, html.dark-mode h4, html.dark-mode h5, html.dark-mode h6 {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
html.dark-mode a {
|
||||||
|
color: #4db8ff;
|
||||||
|
}
|
||||||
|
html.dark-mode a:visited {
|
||||||
|
color: #8866ff;
|
||||||
|
}
|
||||||
|
html.dark-mode button {
|
||||||
|
background: #3d3d3d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
html.dark-mode button:hover {
|
||||||
|
background: #4d4d4d;
|
||||||
}
|
}
|
||||||
</style>`);
|
</style>`);
|
||||||
}
|
}
|
||||||
@@ -597,6 +739,15 @@ if ($('#tsummary-style').length === 0) {
|
|||||||
|
|
||||||
/* Slightly thinner avoid highlight for compact layout */
|
/* Slightly thinner avoid highlight for compact layout */
|
||||||
td.rota-day.avoid-shift.avoid-highlight { border-width: 1.5px !important; box-shadow: 0 0 4px rgba(255,140,0,0.18); }
|
td.rota-day.avoid-shift.avoid-highlight { border-width: 1.5px !important; box-shadow: 0 0 4px rgba(255,140,0,0.18); }
|
||||||
|
|
||||||
|
/* Higher-contrast dark mode for timetable readability */
|
||||||
|
html.dark-mode table.tsummary { background: #111922; color: #e8eef5; }
|
||||||
|
html.dark-mode table.tsummary th,
|
||||||
|
html.dark-mode table.tsummary td { border-color: #516274; }
|
||||||
|
html.dark-mode table.tsummary th { background: #253241; color: #f5f8fc; }
|
||||||
|
html.dark-mode table.tsummary td { background: #16212d; color: #e8eef5; }
|
||||||
|
html.dark-mode table.tsummary tr:nth-child(even) td { background: #1a2734; }
|
||||||
|
html.dark-mode table.tsummary td.target-assigned { color: #f2f6fb; }
|
||||||
</style>
|
</style>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user