Page routes, Home link, documented-app filters, Overview colors and list fixes
- Every nav page has its own address (/tracked, /status, /devices, ...); the logo and name go to the Overview; back/forward work. - List filters gain an "Only ticked" mode; software columns get a one-click "devices with only documented apps" pick and an easiest-first sort. - Overview: the device list uses the dashboard's table header (sticky, with Excel filters); what opened it is clearly highlighted; locations colored by how much is done (red to green); stages read red / amber / green. - Details: one-line title bar; presence and monitors only in Advanced View. - Presence sweep pings only completed pairs (everything else is pinged by the sweep itself), hourly and at midnight. - Custom View scrollbar smaller, in the table header blue. - Docs: no sideways scroll in the article list; no blank lines around code.
This commit is contained in:
1 parent
e2125c760a
commit
f72e39892c
13 files changed
+332
-117
No files matched your search
+55
-32
@@ -258,6 +258,7 @@
|
||||
const k = el('i', 'ov-key'); k.style.background = `var(--ov-${s.key})`;
|
||||
item.appendChild(k); item.appendChild(el('span', null, `${s.label} `)); item.appendChild(el('b', null, String(counts[s.key])));
|
||||
item.onclick = () => openList('stage', s.key);
|
||||
if (state.kind === 'stage' && state.arg === s.key) item.classList.add('ov-selected');
|
||||
legend.appendChild(item);
|
||||
});
|
||||
const other = all.length - usmt.length;
|
||||
@@ -282,6 +283,7 @@
|
||||
t.appendChild(top);
|
||||
t.appendChild(el('div', 'ov-tile-num', waiting ? '–' : String(n)));
|
||||
t.appendChild(el('div', 'ov-tile-sub', waiting ? 'waiting for worker' : (n ? issue.sub(list) : 'clear')));
|
||||
if (state.kind === 'issue' && state.arg === issue.key) t.appendChild(el('span', 'ov-showing', 'Listed below \u2193'));
|
||||
t.setAttribute('aria-label', `${issue.label}: ${n}. ${issue.hint}`);
|
||||
clickable(t, () => openList('issue', issue.key));
|
||||
issuesWrap.appendChild(t);
|
||||
@@ -303,6 +305,9 @@
|
||||
row.appendChild(el('span', 'ov-bar-label', s));
|
||||
const track = el('span', 'ov-bar-track');
|
||||
const bar = el('span', 'ov-bar');
|
||||
const st = s === 'Completed' ? 'done' : IN_PROGRESS.includes(s) ? 'doing' : ['Not Started', 'Not Associated'].includes(s) ? 'todo' : null;
|
||||
bar.style.background = st ? `var(--ov-${st})` : 'var(--ov-other)';
|
||||
if (state.kind === 'status' && state.arg === s) row.classList.add('ov-selected');
|
||||
bar.style.width = n ? `max(4px, ${(100 * n / max).toFixed(1)}%)` : '0';
|
||||
track.appendChild(bar);
|
||||
row.appendChild(track);
|
||||
@@ -325,7 +330,7 @@
|
||||
frag.appendChild(calendarCard(all));
|
||||
|
||||
// Drill-down
|
||||
const drill = el('section', 'ov-card ov-drill');
|
||||
const drill = el('section', 'table-wrapper ov-drill');
|
||||
drill.id = 'ov-drill';
|
||||
frag.appendChild(drill);
|
||||
|
||||
@@ -354,17 +359,19 @@
|
||||
function locationCard(all) {
|
||||
const c = card('Where the work is', null);
|
||||
const toggle = el('div', 'ov-toggle');
|
||||
[['left', 'Still to migrate'], ['all', 'All devices']].forEach(([k, label]) => {
|
||||
[['left', 'Still to migrate'], ['all', 'All migrations']].forEach(([k, label]) => {
|
||||
const b = el('button', state.map === k ? 'on' : '', label); b.type = 'button';
|
||||
b.onclick = () => { state.map = k; render(); };
|
||||
toggle.appendChild(b);
|
||||
});
|
||||
c.querySelector('.ov-card-head').appendChild(toggle);
|
||||
|
||||
// Every USMT migration, by source location. The cell shows how many are left (or all of
|
||||
// them) and its color shows how far along the location is: red = little done, green = done.
|
||||
const cells = {}, buildings = new Set(), floors = new Set();
|
||||
let unknown = 0;
|
||||
all.forEach(r => {
|
||||
if (state.map === 'left' && (stageOf(r) === 'done' || stageOf(r) === null)) return;
|
||||
if (stageOf(r) === null) return;
|
||||
const a = adv(r['Source Device']);
|
||||
const b = a && a.Building && a.Building !== 'N/A' ? a.Building : null;
|
||||
const f = floorOf(a);
|
||||
@@ -381,40 +388,46 @@
|
||||
c.appendChild(el('p', 'ov-empty-text', all.length ? 'No location data yet (waiting for inventory).' : 'Waiting for the worker...'));
|
||||
return c;
|
||||
}
|
||||
const max = Math.max(1, ...Object.values(cells).map(v => v.length));
|
||||
const BANDS = ['Under 20% done', '20-39% done', '40-59% done', '60-79% done', '80% or more done'];
|
||||
const band = pct => Math.min(4, Math.floor(pct / 20));
|
||||
const wrap = el('div', 'ov-heat-wrap');
|
||||
const table = el('table', 'ov-heat');
|
||||
const thead = el('thead'), hr = el('tr');
|
||||
hr.appendChild(el('th', 'ov-heat-corner', 'Building'));
|
||||
fList.forEach(f => hr.appendChild(el('th', null, f === '?' ? 'Unknown' : 'Floor ' + f)));
|
||||
hr.appendChild(el('th', 'ov-heat-total', 'Total'));
|
||||
hr.appendChild(el('th', 'ov-heat-total', state.map === 'left' ? 'Left' : 'Total'));
|
||||
thead.appendChild(hr); table.appendChild(thead);
|
||||
const tb = el('tbody');
|
||||
const sel = state.kind === 'place' ? state.arg : null;
|
||||
bList.forEach(b => {
|
||||
const tr = el('tr');
|
||||
tr.appendChild(el('th', null, b));
|
||||
let total = 0;
|
||||
let total = 0, totalDone = 0;
|
||||
fList.forEach(f => {
|
||||
const list = cells[b + '|' + f] || [];
|
||||
total += list.length;
|
||||
const td = el('td');
|
||||
if (list.length) {
|
||||
const cell = el('span', 'ov-heat-cell', String(list.length));
|
||||
const t = list.length / max;
|
||||
// sequential blue: 7 steps, light -> dark; text flips to white on dark steps
|
||||
const step = Math.min(6, Math.floor(t * 6.999));
|
||||
cell.classList.add('ov-s' + step);
|
||||
const c3 = { todo: 0, doing: 0, done: 0 };
|
||||
list.forEach(r => { const s = stageOf(r); if (s) c3[s]++; });
|
||||
hover(cell, e => showTip(e, String(list.length), `${b}, ${f === '?' ? 'floor unknown' : 'floor ' + f}`,
|
||||
STAGES.filter(s => c3[s.key]).map(s => [s.key, `${c3[s.key]} ${s.label.toLowerCase()}`])));
|
||||
list.forEach(r => c3[stageOf(r)]++);
|
||||
const left = list.length - c3.done;
|
||||
total += list.length; totalDone += c3.done;
|
||||
const pct = Math.round(100 * c3.done / list.length);
|
||||
const shown = state.map === 'left' ? left : list.length;
|
||||
const cell = el('span', 'ov-heat-cell ov-h' + band(pct), String(shown));
|
||||
if (sel && sel.b === b && sel.f === f) cell.classList.add('ov-selected');
|
||||
const where = `${b}, ${f === '?' ? 'floor unknown' : 'floor ' + f}`;
|
||||
cell.setAttribute('aria-label', `${where}: ${left} of ${list.length} left, ${pct}% done`);
|
||||
hover(cell, e => showTip(e, `${pct}% done`, where,
|
||||
[[null, `${left} of ${list.length} still to migrate`]].concat(STAGES.filter(s => c3[s.key]).map(s => [s.key, `${c3[s.key]} ${s.label.toLowerCase()}`]))));
|
||||
clickable(cell, () => openList('place', { b, f }));
|
||||
td.appendChild(cell);
|
||||
} else td.appendChild(el('span', 'ov-heat-none', '·'));
|
||||
} else td.appendChild(el('span', 'ov-heat-none', '\u00b7'));
|
||||
tr.appendChild(td);
|
||||
});
|
||||
const tt = el('td', 'ov-heat-total');
|
||||
const tbtn = el('span', 'ov-heat-sum', String(total));
|
||||
const tbtn = el('span', 'ov-heat-sum', String(state.map === 'left' ? total - totalDone : total));
|
||||
tbtn.title = total ? `${Math.round(100 * totalDone / total)}% done` : '';
|
||||
if (sel && sel.b === b && sel.f === null) tbtn.classList.add('ov-selected');
|
||||
clickable(tbtn, () => openList('place', { b, f: null }));
|
||||
tt.appendChild(tbtn);
|
||||
tr.appendChild(tt);
|
||||
@@ -423,8 +436,11 @@
|
||||
table.appendChild(tb);
|
||||
wrap.appendChild(table);
|
||||
c.appendChild(wrap);
|
||||
const note = el('p', 'ov-note', (state.map === 'left' ? 'Devices not yet completed, by source location. ' : 'Every tracked source, by location. ') +
|
||||
'The stronger the shade, the more devices.' + (unknown ? ` ${plural(unknown, 'device')} without a building yet.` : ''));
|
||||
const key = el('div', 'ov-heat-key');
|
||||
BANDS.forEach((label, i) => { const k = el('span', 'ov-heat-key-item'); const sw = el('i', 'ov-h' + i); k.appendChild(sw); k.appendChild(el('span', null, label)); key.appendChild(k); });
|
||||
c.appendChild(key);
|
||||
const note = el('p', 'ov-note', (state.map === 'left' ? 'Numbers are migrations still to finish' : 'Numbers are all USMT migrations') +
|
||||
' at each source location; the color is how much of that location is done.' + (unknown ? ` ${plural(unknown, 'device')} without a building yet.` : ''));
|
||||
c.appendChild(note);
|
||||
return c;
|
||||
}
|
||||
@@ -528,7 +544,7 @@
|
||||
const { b, f } = state.arg;
|
||||
title = f === null ? b : `${b}, ${f === '?' ? 'floor unknown' : 'floor ' + f}`;
|
||||
items = all.filter(r => {
|
||||
if (state.map === 'left' && (stageOf(r) === 'done' || stageOf(r) === null)) return false;
|
||||
if (stageOf(r) === null || (state.map === 'left' && stageOf(r) === 'done')) return false;
|
||||
const a = adv(r['Source Device']);
|
||||
return a && a.Building === b && (f === null || (floorOf(a) || '?') === f);
|
||||
}).map(r => { const a = adv(r['Source Device']); return { r, detail: a ? a.Location : '' }; });
|
||||
@@ -540,9 +556,9 @@
|
||||
}
|
||||
|
||||
box.appendChild(listHead(title, items.length));
|
||||
if (!items.length) { box.appendChild(el('p', 'ov-empty-text', 'Nothing here. That one is at zero.')); return; }
|
||||
const wrap = el('div', 'table-wrapper ov-table-wrap');
|
||||
if (!items.length) { box.appendChild(el('p', 'ov-empty-text ov-drill-empty', 'Nothing here. That one is at zero.')); return; }
|
||||
const table = el('table', 'master-table ov-table');
|
||||
table.id = 'ov-table';
|
||||
const thead = el('thead'), tr = el('tr');
|
||||
['Source Device', 'Destination', 'Backup Status', detailLabel, 'Tags'].forEach(h => tr.appendChild(el('th', null, h)));
|
||||
thead.appendChild(tr); table.appendChild(thead);
|
||||
@@ -559,22 +575,24 @@
|
||||
tb.appendChild(row);
|
||||
});
|
||||
table.appendChild(tb);
|
||||
wrap.appendChild(table);
|
||||
box.appendChild(wrap);
|
||||
box.appendChild(table);
|
||||
if (window.ExcelFilter) ExcelFilter.refresh(table);
|
||||
}
|
||||
|
||||
// Same header as the dashboard's other tables (sticky, with the Excel filter "Clear" button)
|
||||
function listHead(title, n) {
|
||||
const h = el('div', 'ov-card-head');
|
||||
h.appendChild(el('h2', null, `${title} (${n})`));
|
||||
const h = el('div', 'group-header ov-drill-head');
|
||||
h.appendChild(el('span', 'ov-drill-title', `${title} (${n})`));
|
||||
const copy = el('button', 'ov-btn', 'Copy names'); copy.type = 'button';
|
||||
copy.onclick = () => {
|
||||
const names = Array.from(document.querySelectorAll('#ov-drill tbody tr td:first-child')).map(td => td.textContent.trim());
|
||||
const names = Array.from(document.querySelectorAll('#ov-drill tbody tr')).filter(tr => tr.style.display !== 'none').map(tr => tr.cells[0].textContent.trim());
|
||||
if (typeof fallbackCopyTextToClipboard === 'function') fallbackCopyTextToClipboard(names.join('\n'));
|
||||
copy.textContent = 'Copied'; setTimeout(() => { copy.textContent = 'Copy names'; }, 1500);
|
||||
};
|
||||
const close = el('button', 'ov-btn', 'Close'); close.type = 'button';
|
||||
close.onclick = () => { state.kind = null; state.arg = null; render(); };
|
||||
const btns = el('div', 'ov-head-btns'); btns.appendChild(copy); btns.appendChild(close);
|
||||
copy.className = close.className = 'copy-table-btn no-copy';
|
||||
h.appendChild(btns);
|
||||
return h;
|
||||
}
|
||||
@@ -582,9 +600,9 @@
|
||||
function renderApps(box, title, items) {
|
||||
box.appendChild(listHead(title, items.length));
|
||||
if (!items.length) { box.appendChild(el('p', 'ov-empty-text', 'Every specialty app has an article.')); return; }
|
||||
box.appendChild(el('p', 'ov-note', 'Specialty apps found on source devices that have no knowledge-base article with the same name. Write one so techs know how to install it.'));
|
||||
const wrap = el('div', 'table-wrapper ov-table-wrap');
|
||||
box.appendChild(el('p', 'ov-note ov-drill-note', 'Specialty apps found on source devices that have no knowledge-base article with the same name. Write one so techs know how to install it.'));
|
||||
const table = el('table', 'master-table ov-table');
|
||||
table.id = 'ov-table';
|
||||
const thead = el('thead'), tr = el('tr');
|
||||
['App', 'Devices', 'Sources with it', ''].forEach(h => tr.appendChild(el('th', null, h)));
|
||||
thead.appendChild(tr); table.appendChild(thead);
|
||||
@@ -609,8 +627,8 @@
|
||||
tb.appendChild(row);
|
||||
});
|
||||
table.appendChild(tb);
|
||||
wrap.appendChild(table);
|
||||
box.appendChild(wrap);
|
||||
box.appendChild(table);
|
||||
if (window.ExcelFilter) ExcelFilter.refresh(table);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
@@ -630,7 +648,12 @@
|
||||
render();
|
||||
}
|
||||
|
||||
window.Overview = { render: () => maybeRender(true), refresh: () => maybeRender(false) };
|
||||
window.Overview = {
|
||||
render: () => maybeRender(true), refresh: () => maybeRender(false),
|
||||
// used by the Excel-style filters: filters belong to the list that's open
|
||||
listKey: () => 'overview:' + state.kind + ':' + JSON.stringify(state.arg),
|
||||
renderList: () => renderList()
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const orig = window.switchPage;
|
||||
|
||||
Reference in new issue
Block a user