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:
david kiania 2026-06-26 00:20:23 +03:00
parent b2c4cbe378
commit 82a6d11d95

View file

@ -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) =>