feat(tickets): INC | CRQ dataset sub-tabs
Add a sub-tab bar under the Tickets tab (INC | CRQ). A DS dataset variable repoints
the dashboard/search/filter-options calls to /webhook/${DS}-* and updates the overview
/ map / legend labels; switching resets the per-dataset dropdowns + explorer and
reloads. Map, SLA legend, popups and the vehicle overlay are dataset-agnostic, so CRQ
renders "just like INC" off reporting.fn_crq_* (fleettickets migration 16).
NOT pushed yet — pushing staging auto-deploys; holding until the CRQ API + DB land.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b2c4cbe378
commit
82a6d11d95
1 changed files with 53 additions and 8 deletions
|
|
@ -99,6 +99,18 @@
|
|||
}
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { background: var(--accent); color: #1a1009; }
|
||||
/* Tickets dataset sub-tabs (INC | CRQ) — same look as the header tabs. */
|
||||
.subtabs {
|
||||
display: inline-flex; gap: 4px; background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: 8px; padding: 3px; margin-bottom: 10px;
|
||||
}
|
||||
.subtab {
|
||||
background: transparent; color: var(--muted); border: 0; border-radius: 6px;
|
||||
padding: 6px 16px; font-size: 12.5px; font-weight: 700; letter-spacing: .3px;
|
||||
cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.subtab:hover { color: var(--text); }
|
||||
.subtab.active { background: var(--accent); color: #1a1009; }
|
||||
|
||||
/* ── Tabbed views ────────────────────────────────────────────────────── */
|
||||
.view { display: none; }
|
||||
|
|
@ -391,11 +403,15 @@
|
|||
</section>
|
||||
|
||||
<section class="view" id="view-tickets">
|
||||
<div class="subtabs" id="tk-subtabs" role="tablist">
|
||||
<button class="subtab active" data-ds="inc" type="button">INC</button>
|
||||
<button class="subtab" data-ds="crq" type="button">CRQ</button>
|
||||
</div>
|
||||
<main id="tk-main">
|
||||
<div class="card span12">
|
||||
<div class="tk-overview-row">
|
||||
<div class="tk-overview-metrics">
|
||||
<h2>INC overview <span class="count" id="tk-fresh"></span></h2>
|
||||
<h2><span id="tk-ds-ov">INC</span> overview <span class="count" id="tk-fresh"></span></h2>
|
||||
<div class="metric-row" id="tk-metrics"><div class="empty">Loading…</div></div>
|
||||
</div>
|
||||
<div class="tk-filters">
|
||||
|
|
@ -425,7 +441,7 @@
|
|||
</div>
|
||||
|
||||
<div class="card span12">
|
||||
<h2>Live INC map <span class="count">open (SLA-coloured) · closed (faded same colour) · ISP vehicles</span></h2>
|
||||
<h2>Live <span id="tk-ds-map">INC</span> map <span class="count">open (SLA-coloured) · closed (faded same colour) · ISP vehicles</span></h2>
|
||||
<div class="map-wrap">
|
||||
<div id="tk-map"></div>
|
||||
<div id="tk-layers" class="map-ctl collapsed">
|
||||
|
|
@ -894,6 +910,34 @@ function switchTab(name) {
|
|||
document.querySelectorAll('.tab').forEach(b =>
|
||||
b.addEventListener('click', () => switchTab(b.dataset.tab)));
|
||||
|
||||
// ── Tickets dataset sub-tabs (INC | CRQ) ────────────────────────────────────
|
||||
// Same dashboard machinery, different source dataset: flips DS, which repoints the
|
||||
// /webhook/${DS}-dashboard|search|filter-options calls, then resets the per-dataset
|
||||
// dropdowns + explorer and reloads. The map/legend/popups are dataset-agnostic.
|
||||
function resetSelect(id) { // keep only the first (placeholder) option
|
||||
const el = $(id); if (!el) return;
|
||||
while (el.options.length > 1) el.remove(1);
|
||||
el.selectedIndex = 0;
|
||||
}
|
||||
function setDataset(ds) {
|
||||
if (ds === DS) return;
|
||||
DS = ds;
|
||||
document.querySelectorAll('.subtab').forEach(b => b.classList.toggle('active', b.dataset.ds === ds));
|
||||
const U = ds.toUpperCase();
|
||||
$('tk-ds-ov').textContent = U;
|
||||
$('tk-ds-map').textContent = U;
|
||||
// per-dataset dropdowns + explorer results are stale → clear and re-init
|
||||
incDropdownsInit = false; incFilterOptionsInit = false;
|
||||
['tk-cluster', 'tk-status', 'tk-x-eng', 'tk-x-cluster'].forEach(resetSelect);
|
||||
$('tk-x-id').value = ''; $('tk-x-id-list').innerHTML = '';
|
||||
$('tk-x-count').textContent = '';
|
||||
$('tk-x-wrap').innerHTML = '<div class="empty">Search by ticket id, engineer, cluster, state and time.</div>';
|
||||
loadInc(); // reload metrics + map for the new dataset
|
||||
loadIncFilterOptions(); // refill explorer pulldowns for the new dataset
|
||||
}
|
||||
document.querySelectorAll('.subtab').forEach(b =>
|
||||
b.addEventListener('click', () => setDataset(b.dataset.ds)));
|
||||
|
||||
// ============================================================================
|
||||
// TICKETS — INC operations dashboard (open layer + windowed closed overlay)
|
||||
// ============================================================================
|
||||
|
|
@ -963,6 +1007,7 @@ const tkMarkers = new Map(); // imei → maplibregl.M
|
|||
const tkLayerState = { open: true, closed: true, vehicles: true };
|
||||
let tkOwnerFilter = null; // when set, the closed layer is filtered to this engineer (drill-down)
|
||||
let incData = null, incDropdownsInit = false, vehCount = 0;
|
||||
let DS = 'inc'; // active Tickets dataset (inc | crq) — drives /webhook/${DS}-* endpoints + labels
|
||||
let tkLoadedDay = null; // EAT calendar day of the last INC load — drives the midnight auto-rollover
|
||||
const eatDay = () => new Date().toLocaleDateString('en-CA', { timeZone: 'Africa/Nairobi' });
|
||||
|
||||
|
|
@ -1224,7 +1269,7 @@ let incFilterOptionsInit = false;
|
|||
async function loadIncFilterOptions() {
|
||||
if (incFilterOptionsInit) return;
|
||||
try {
|
||||
const o = await api('/webhook/inc-filter-options');
|
||||
const o = await api(`/webhook/${DS}-filter-options`);
|
||||
incFilterOptionsInit = true;
|
||||
const eng = $('tk-x-eng'); (o.owners || []).forEach((n) => eng.add(new Option(n, n)));
|
||||
const cl = $('tk-x-cluster'); (o.clusters || []).forEach((c) => cl.add(new Option(c, c)));
|
||||
|
|
@ -1235,7 +1280,7 @@ async function loadIncSearch() {
|
|||
const wrap = $('tk-x-wrap');
|
||||
wrap.innerHTML = '<div class="empty">Searching…</div>';
|
||||
try {
|
||||
const j = await api(`/webhook/inc-search?${incSearchQs()}`);
|
||||
const j = await api(`/webhook/${DS}-search?${incSearchQs()}`);
|
||||
tkSearchRows = (j && j.rows) || [];
|
||||
const count = (j && j.count) || 0;
|
||||
$('tk-x-count').textContent = j ? (j.truncated ? `(first ${tkSearchRows.length} of ${intg(count)})` : `(${intg(count)})`) : '';
|
||||
|
|
@ -1287,7 +1332,7 @@ function initIncDropdowns(m) {
|
|||
async function loadInc() {
|
||||
$('tk-main').classList.add('loading');
|
||||
try {
|
||||
const j = await api(`/webhook/inc-dashboard?${incQs()}`);
|
||||
const j = await api(`/webhook/${DS}-dashboard?${incQs()}`);
|
||||
incData = j;
|
||||
tkLoadedDay = eatDay();
|
||||
if (!incDropdownsInit && j.metrics) { initIncDropdowns(j.metrics); incDropdownsInit = true; }
|
||||
|
|
@ -1296,7 +1341,7 @@ async function loadInc() {
|
|||
buildIncLayers();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
$('tk-metrics').innerHTML = `<div class="banner error">${e.message || 'Failed to load the INC dashboard. Is the API reachable?'}</div>`;
|
||||
$('tk-metrics').innerHTML = `<div class="banner error">${e.message || `Failed to load the ${DS.toUpperCase()} dashboard. Is the API reachable?`}</div>`;
|
||||
} finally {
|
||||
$('tk-main').classList.remove('loading');
|
||||
}
|
||||
|
|
@ -1389,8 +1434,8 @@ function showIncPopup(f, closed) {
|
|||
function buildIncLayers() {
|
||||
const m = (incData && incData.metrics) || {};
|
||||
const rows = [
|
||||
{ id: 'open', label: 'Open INC', color: SLA_COLORS.breached, n: m.open_now ?? 0 },
|
||||
{ id: 'closed', label: 'Closed INC', color: pastel(CLOSED_COLOR), n: m.closed_in_window ?? 0 },
|
||||
{ id: 'open', label: `Open ${DS.toUpperCase()}`, color: SLA_COLORS.breached, n: m.open_now ?? 0 },
|
||||
{ id: 'closed', label: `Closed ${DS.toUpperCase()}`, color: pastel(CLOSED_COLOR), n: m.closed_in_window ?? 0 },
|
||||
{ id: 'vehicles', label: 'ISP vehicles', color: '#E8954A', n: vehCount },
|
||||
];
|
||||
let html = rows.map((r) =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue