feat(tickets map): closures-by-engineer panel, drill-down + dispatcher popup details
- New "Closures by engineer" leaderboard panel (metrics.by_owner): engineer, closed, breached, avg MTTR. Clicking an engineer toggles a drill-down that filters the closed map pins to only their closures (and ensures the closed layer is visible). - Popups now carry the details dispatchers need: open popups show location_name and the true coordinates (copyable; the fan-out keeps the real coord even when the pin is offset); closed popups show "closed by <engineer>". Backed by fn_inc_dashboard migration 12 (owner case-normalized server-side). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a32735bca3
commit
51a84b66c6
1 changed files with 49 additions and 5 deletions
|
|
@ -439,6 +439,11 @@
|
|||
<div class="chart-wrap"><canvas id="tk-closureChart"></canvas></div>
|
||||
</div>
|
||||
|
||||
<div class="card span3">
|
||||
<h2>Closures by engineer <span class="count" id="tk-owner-count"></span></h2>
|
||||
<div class="tbl-scroll" id="tk-owner-wrap"><div class="empty">Loading…</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card span3">
|
||||
<h2>By status <span class="count" id="tk-status-count"></span></h2>
|
||||
<div class="tbl-scroll" id="tk-status-wrap"><div class="empty">Loading…</div></div>
|
||||
|
|
@ -952,6 +957,7 @@ function vehState(p) {
|
|||
let tkMap = null, tkPopup = null, tkLivePoll = null, tkClosureChart = null;
|
||||
const tkMarkers = new Map(); // imei → maplibregl.Marker
|
||||
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;
|
||||
|
||||
// ── INC helpers ───────────────────────────────────────────────────────────
|
||||
|
|
@ -1039,7 +1045,9 @@ function fanOutColocated(openFC, closedFC) {
|
|||
coords = [c[0] + (r * Math.cos(a)) / Math.max(Math.cos(latRad), 0.2), c[1] + r * Math.sin(a)];
|
||||
}
|
||||
(it.layer === 'open' ? outOpen : outClosed).push(
|
||||
{ type: 'Feature', properties: it.f.properties, geometry: { type: 'Point', coordinates: coords } });
|
||||
{ type: 'Feature',
|
||||
properties: { ...it.f.properties, __lng: c[0], __lat: c[1] }, // keep TRUE coords for popups
|
||||
geometry: { type: 'Point', coordinates: coords } });
|
||||
});
|
||||
}
|
||||
return { open: { type: 'FeatureCollection', features: outOpen },
|
||||
|
|
@ -1047,7 +1055,12 @@ function fanOutColocated(openFC, closedFC) {
|
|||
}
|
||||
function renderIncMap() {
|
||||
if (!tkMap || !incData) return;
|
||||
const { open, closed } = fanOutColocated(incData.open, incData.closed);
|
||||
let closedFC = incData.closed || EMPTY_FC;
|
||||
if (tkOwnerFilter) { // drill-down: only the selected engineer's closures
|
||||
closedFC = { type: 'FeatureCollection',
|
||||
features: (closedFC.features || []).filter((f) => (f.properties || {}).owner === tkOwnerFilter) };
|
||||
}
|
||||
const { open, closed } = fanOutColocated(incData.open, closedFC);
|
||||
if (tkMap.getSource('inc-open')) tkMap.getSource('inc-open').setData(open);
|
||||
if (tkMap.getSource('inc-closed')) tkMap.getSource('inc-closed').setData(closed);
|
||||
}
|
||||
|
|
@ -1153,6 +1166,33 @@ function renderIncTables(m) {
|
|||
const cn = incTable(nbo), cc = incTable(coast);
|
||||
$('tk-nbo-wrap').innerHTML = cn.html; $('tk-nbo-count').textContent = cn.n ? `(${cn.n})` : '';
|
||||
$('tk-coast-wrap').innerHTML = cc.html; $('tk-coast-count').textContent = cc.n ? `(${cc.n})` : '';
|
||||
renderIncOwners(m.by_owner);
|
||||
}
|
||||
|
||||
// Closures-by-engineer leaderboard (metrics.by_owner — owner case-normalized in SQL).
|
||||
// Clicking a row toggles a drill-down that filters the closed map pins to that engineer.
|
||||
function renderIncOwners(arr) {
|
||||
arr = arr || [];
|
||||
$('tk-owner-count').textContent = arr.length ? `(${arr.length})` : '';
|
||||
const wrap = $('tk-owner-wrap');
|
||||
if (!arr.length) { wrap.innerHTML = '<div class="empty">No closures in window.</div>'; return; }
|
||||
const body = arr.map((o) => {
|
||||
const hl = (o.owner === tkOwnerFilter) ? ' style="background:rgba(232,149,74,.14)"' : '';
|
||||
const mttrH = (o.avg_mttr_min != null) ? num(o.avg_mttr_min / 60, 1) : '—';
|
||||
return `<tr data-owner="${escapeHtml(o.owner)}"${hl} style="cursor:pointer">
|
||||
<td class="plate">${escapeHtml(o.owner)}</td><td>${intg(o.closed)}</td>
|
||||
<td>${intg(o.breached || 0)}</td><td>${mttrH}</td></tr>`;
|
||||
}).join('');
|
||||
wrap.innerHTML = `<table><thead><tr><th>Engineer</th><th>Closed</th><th>Breach</th><th>MTTR h</th></tr></thead><tbody>${body}</tbody></table>`;
|
||||
wrap.querySelectorAll('tr[data-owner]').forEach((tr) => tr.addEventListener('click', () => {
|
||||
const o = tr.getAttribute('data-owner');
|
||||
tkOwnerFilter = (tkOwnerFilter === o) ? null : o; // toggle off if re-clicked
|
||||
if (tkOwnerFilter && tkMap && !tkLayerState.closed) { // make sure closures are visible
|
||||
tkLayerState.closed = true; tkMap.setLayoutProperty('inc-closed', 'visibility', 'visible'); buildIncLayers();
|
||||
}
|
||||
renderIncOwners(arr); // re-highlight selection
|
||||
renderIncMap(); // re-filter the closed pins
|
||||
}));
|
||||
}
|
||||
|
||||
function renderClosureChart(cr) {
|
||||
|
|
@ -1271,16 +1311,20 @@ function showIncPopup(f, closed) {
|
|||
const p = f.properties || {};
|
||||
const lines = [`<div class="row">${escapeHtml(p.normalized_status || '—')}</div>`];
|
||||
if (p.cluster) lines.push(`<div class="row muted">${escapeHtml(p.cluster)}${p.region ? ' · ' + escapeHtml(p.region) : ''}</div>`);
|
||||
const who = p.assigned_team || p.owner;
|
||||
if (who) lines.push(`<div class="row muted">${escapeHtml(who)}</div>`);
|
||||
if (p.location_name) lines.push(`<div class="row muted">${escapeHtml(p.location_name)}</div>`);
|
||||
if (closed) {
|
||||
if (p.owner) lines.push(`<div class="row muted">closed by ${escapeHtml(p.owner)}</div>`);
|
||||
lines.push(`<div class="row muted">closed ${escapeHtml(eatShort(p.closed_at))} · MTTR ${mttrFmt(p.mttr)}</div>`);
|
||||
if (p.sla_status) lines.push(`<div class="row muted">${escapeHtml(p.sla_status)}</div>`);
|
||||
} else {
|
||||
const who = p.assigned_team || p.owner;
|
||||
if (who) lines.push(`<div class="row muted">${escapeHtml(who)}</div>`);
|
||||
const st = p.sla_state || 'unknown';
|
||||
lines.push(`<div class="row"><span class="badge" style="color:${SLA_COLORS[st] || '#fff'}">${SLA_LABELS[st] || st}</span>${p.hours_open != null ? ' · ' + num(p.hours_open, 0) + 'h open' : ''}</div>`);
|
||||
}
|
||||
if (p.geo_source === 'cluster') lines.push('<div class="row muted" style="font-size:10px">approx — cluster location</div>');
|
||||
// true location for the dispatcher (copyable; the pin itself may be fanned out)
|
||||
if (p.__lat != null && p.__lng != null)
|
||||
lines.push(`<div class="row muted" style="font-size:10px">${num(p.__lat, 5)}, ${num(p.__lng, 5)}${p.geo_source === 'cluster' ? ' · approx (cluster)' : ''}</div>`);
|
||||
tkPopup.setLngLat(f.geometry.coordinates).setHTML(`<div class="pop">
|
||||
<b>${escapeHtml(p.ticket_id || '—')} <span class="badge" style="color:${closed ? closedColor(p.sla_status) : (SLA_COLORS[p.sla_state] || '#fff')}">${closed ? 'CLOSED' : 'OPEN'}</span></b>
|
||||
${lines.join('')}</div>`).addTo(tkMap);
|
||||
|
|
|
|||
Loading…
Reference in a new issue