feat(tickets map): closed tickets render as faded same SLA colour
Previously closed INC tickets all used one flat slate pin, losing the SLA
outcome and reading as inactive/uninformative. Now a closed ticket keeps its
SLA colour but as a light ('pastel') version — Breached → light red, Compliant
→ light green — so active tickets stay vivid and closed ones are a washed-out
same-hue. Makes active-vs-closed and SLA outcome apparent across both layers.
- Faded closed pin images keyed on sla_status (Compliant|Breached, + fallback).
- inc-closed layer matches sla_status → faded pin; popup badge + legend updated
(new "Closed SLA" key); header subtitle reworded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
737f8c3d31
commit
62479c0a72
1 changed files with 27 additions and 12 deletions
|
|
@ -424,7 +424,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card span12">
|
<div class="card span12">
|
||||||
<h2>Live INC map <span class="count">open (SLA-coloured) · closed overlay · ISP vehicles</span></h2>
|
<h2>Live INC map <span class="count">open (SLA-coloured) · closed (faded same colour) · ISP vehicles</span></h2>
|
||||||
<div class="map-wrap">
|
<div class="map-wrap">
|
||||||
<div id="tk-map"></div>
|
<div id="tk-map"></div>
|
||||||
<div id="tk-layers" class="map-ctl collapsed">
|
<div id="tk-layers" class="map-ctl collapsed">
|
||||||
|
|
@ -903,7 +903,11 @@ const EMPTY_FC = { type: 'FeatureCollection', features: [] };
|
||||||
// SLA state → colour (open layer + legend); mirrors the warm-dark palette.
|
// SLA state → colour (open layer + legend); mirrors the warm-dark palette.
|
||||||
const SLA_COLORS = { breached: '#ef5b5b', at_risk: '#f0a93b', ok: '#2dd4a7', unknown: '#6b7280' };
|
const SLA_COLORS = { breached: '#ef5b5b', at_risk: '#f0a93b', ok: '#2dd4a7', unknown: '#6b7280' };
|
||||||
const SLA_LABELS = { breached: 'Breached', at_risk: 'At risk', ok: 'OK', unknown: 'Unknown' };
|
const SLA_LABELS = { breached: 'Breached', at_risk: 'At risk', ok: 'OK', unknown: 'Unknown' };
|
||||||
const CLOSED_COLOR = '#94a3b8'; // muted slate — closed tickets (status irrelevant)
|
const CLOSED_COLOR = '#94a3b8'; // fallback slate — closed tickets with no SLA outcome
|
||||||
|
// Closed tickets keep their SLA colour but as a light ('pastel') version, so active
|
||||||
|
// tickets show vivid and closed ones a washed-out same-colour — status stays apparent
|
||||||
|
// across both. sla_status (closed) is 'Compliant' | 'Breached' (capitalised).
|
||||||
|
const CLOSED_SLA_BASE = { Breached: SLA_COLORS.breached, Compliant: SLA_COLORS.ok };
|
||||||
// Coast (Mombasa / Voi) cluster classifier — splits the cluster breakdown by name
|
// Coast (Mombasa / Voi) cluster classifier — splits the cluster breakdown by name
|
||||||
// (the feed's region field is noisy; cluster names classify cleanly).
|
// (the feed's region field is noisy; cluster names classify cleanly).
|
||||||
const COAST_HINTS = ['coast', 'mombasa', 'voi', 'nyali', 'mtwapa', 'kiembeni', 'vipingo',
|
const COAST_HINTS = ['coast', 'mombasa', 'voi', 'nyali', 'mtwapa', 'kiembeni', 'vipingo',
|
||||||
|
|
@ -932,6 +936,8 @@ function pastel(hex, mix = 0.58) {
|
||||||
const t = (c) => Math.round(c + (255 - c) * mix);
|
const t = (c) => Math.round(c + (255 - c) * mix);
|
||||||
return `rgb(${t(r)}, ${t(g)}, ${t(b)})`;
|
return `rgb(${t(r)}, ${t(g)}, ${t(b)})`;
|
||||||
}
|
}
|
||||||
|
// Faded ('light') colour for a closed ticket, from its SLA outcome (see CLOSED_SLA_BASE).
|
||||||
|
const closedColor = (slaStatus) => pastel(CLOSED_SLA_BASE[slaStatus] || CLOSED_COLOR);
|
||||||
function plateTail(v) { const s = String(v || '').replace(/\s+/g, ''); return s ? s.slice(-4) : '—'; }
|
function plateTail(v) { const s = String(v || '').replace(/\s+/g, ''); return s ? s.slice(-4) : '—'; }
|
||||||
function vehState(p) {
|
function vehState(p) {
|
||||||
if (!p) return 'offline';
|
if (!p) return 'offline';
|
||||||
|
|
@ -991,7 +997,11 @@ function pinImageData(fill) {
|
||||||
function addPinImages() {
|
function addPinImages() {
|
||||||
const pins = {
|
const pins = {
|
||||||
'pin-breached': SLA_COLORS.breached, 'pin-at_risk': SLA_COLORS.at_risk,
|
'pin-breached': SLA_COLORS.breached, 'pin-at_risk': SLA_COLORS.at_risk,
|
||||||
'pin-ok': SLA_COLORS.ok, 'pin-unknown': SLA_COLORS.unknown, 'pin-closed': CLOSED_COLOR,
|
'pin-ok': SLA_COLORS.ok, 'pin-unknown': SLA_COLORS.unknown,
|
||||||
|
// closed = faded ('light') version of the same SLA colour (see CLOSED_SLA_BASE)
|
||||||
|
'pin-closed-breached': closedColor('Breached'),
|
||||||
|
'pin-closed-ok': closedColor('Compliant'),
|
||||||
|
'pin-closed-unknown': pastel(CLOSED_COLOR),
|
||||||
};
|
};
|
||||||
for (const [id, fill] of Object.entries(pins))
|
for (const [id, fill] of Object.entries(pins))
|
||||||
if (!tkMap.hasImage(id)) tkMap.addImage(id, pinImageData(fill), { pixelRatio: 2 });
|
if (!tkMap.hasImage(id)) tkMap.addImage(id, pinImageData(fill), { pixelRatio: 2 });
|
||||||
|
|
@ -1018,19 +1028,21 @@ function initIncMap() {
|
||||||
tkPopup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 14 });
|
tkPopup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 14 });
|
||||||
|
|
||||||
tkMap.on('load', () => {
|
tkMap.on('load', () => {
|
||||||
addPinImages(); // teardrop pin icons (open by SLA, closed one slate colour)
|
addPinImages(); // teardrop pin icons (open by SLA; closed = faded same SLA colour)
|
||||||
// Closed overlay (windowed) — drawn UNDER the live open layer; status irrelevant
|
// Closed overlay (windowed) — drawn UNDER the live open layer. Each closed ticket
|
||||||
// so every closed ticket uses the single muted 'pin-closed', slightly smaller.
|
// uses a faded ('light') version of its SLA colour (Breached→light red, Compliant→
|
||||||
|
// light green), slightly smaller, so it reads as the same status but inactive.
|
||||||
tkMap.addSource('inc-closed', { type: 'geojson', data: EMPTY_FC });
|
tkMap.addSource('inc-closed', { type: 'geojson', data: EMPTY_FC });
|
||||||
tkMap.addLayer({
|
tkMap.addLayer({
|
||||||
id: 'inc-closed', type: 'symbol', source: 'inc-closed',
|
id: 'inc-closed', type: 'symbol', source: 'inc-closed',
|
||||||
layout: {
|
layout: {
|
||||||
visibility: tkLayerState.closed ? 'visible' : 'none',
|
visibility: tkLayerState.closed ? 'visible' : 'none',
|
||||||
'icon-image': 'pin-closed',
|
'icon-image': ['match', ['get', 'sla_status'],
|
||||||
|
'Breached', 'pin-closed-breached', 'Compliant', 'pin-closed-ok', 'pin-closed-unknown'],
|
||||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 6, 0.42, 11, 0.6, 16, 0.78],
|
'icon-size': ['interpolate', ['linear'], ['zoom'], 6, 0.42, 11, 0.6, 16, 0.78],
|
||||||
'icon-anchor': 'bottom', 'icon-allow-overlap': true, 'icon-ignore-placement': true,
|
'icon-anchor': 'bottom', 'icon-allow-overlap': true, 'icon-ignore-placement': true,
|
||||||
},
|
},
|
||||||
paint: { 'icon-opacity': 0.85 },
|
paint: { 'icon-opacity': 0.9 },
|
||||||
});
|
});
|
||||||
// Open layer (live) — pin colour = derived SLA state; larger for hierarchy.
|
// Open layer (live) — pin colour = derived SLA state; larger for hierarchy.
|
||||||
tkMap.addSource('inc-open', { type: 'geojson', data: EMPTY_FC });
|
tkMap.addSource('inc-open', { type: 'geojson', data: EMPTY_FC });
|
||||||
|
|
@ -1229,7 +1241,7 @@ function showIncPopup(f, closed) {
|
||||||
}
|
}
|
||||||
if (p.geo_source === 'cluster') lines.push('<div class="row muted" style="font-size:10px">approx — cluster location</div>');
|
if (p.geo_source === 'cluster') lines.push('<div class="row muted" style="font-size:10px">approx — cluster location</div>');
|
||||||
tkPopup.setLngLat(f.geometry.coordinates).setHTML(`<div class="pop">
|
tkPopup.setLngLat(f.geometry.coordinates).setHTML(`<div class="pop">
|
||||||
<b>${escapeHtml(p.ticket_id || '—')} <span class="badge" style="color:${closed ? CLOSED_COLOR : (SLA_COLORS[p.sla_state] || '#fff')}">${closed ? 'CLOSED' : 'OPEN'}</span></b>
|
<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);
|
${lines.join('')}</div>`).addTo(tkMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1237,7 +1249,7 @@ function buildIncLayers() {
|
||||||
const m = (incData && incData.metrics) || {};
|
const m = (incData && incData.metrics) || {};
|
||||||
const rows = [
|
const rows = [
|
||||||
{ id: 'open', label: 'Open INC', color: SLA_COLORS.breached, n: m.open_now ?? 0 },
|
{ id: 'open', label: 'Open INC', color: SLA_COLORS.breached, n: m.open_now ?? 0 },
|
||||||
{ id: 'closed', label: 'Closed INC', color: CLOSED_COLOR, n: m.closed_in_window ?? 0 },
|
{ id: 'closed', label: 'Closed INC', color: pastel(CLOSED_COLOR), n: m.closed_in_window ?? 0 },
|
||||||
{ id: 'vehicles', label: 'ISP vehicles', color: '#E8954A', n: vehCount },
|
{ id: 'vehicles', label: 'ISP vehicles', color: '#E8954A', n: vehCount },
|
||||||
];
|
];
|
||||||
let html = rows.map((r) =>
|
let html = rows.map((r) =>
|
||||||
|
|
@ -1245,6 +1257,9 @@ function buildIncLayers() {
|
||||||
<span class="legend-dot" style="background:${r.color}"></span><span>${r.label}</span><span class="layers-n">${intg(r.n)}</span></label>`).join('');
|
<span class="legend-dot" style="background:${r.color}"></span><span>${r.label}</span><span class="layers-n">${intg(r.n)}</span></label>`).join('');
|
||||||
html += '<div class="legend-sep">Open SLA</div>' + ['breached', 'at_risk', 'ok', 'unknown'].map((s) =>
|
html += '<div class="legend-sep">Open SLA</div>' + ['breached', 'at_risk', 'ok', 'unknown'].map((s) =>
|
||||||
`<div class="layers-row"><span class="legend-dot" style="background:${SLA_COLORS[s]}"></span><span>${SLA_LABELS[s]}</span></div>`).join('');
|
`<div class="layers-row"><span class="legend-dot" style="background:${SLA_COLORS[s]}"></span><span>${SLA_LABELS[s]}</span></div>`).join('');
|
||||||
|
// Closed = faded ('light') version of the same SLA colour (see CLOSED_SLA_BASE).
|
||||||
|
html += '<div class="legend-sep">Closed SLA</div>' + [['Breached', closedColor('Breached')], ['Compliant', closedColor('Compliant')]].map(([lbl, col]) =>
|
||||||
|
`<div class="layers-row"><span class="legend-dot" style="background:${col}"></span><span>${lbl}</span></div>`).join('');
|
||||||
$('tk-layers-body').innerHTML = html;
|
$('tk-layers-body').innerHTML = html;
|
||||||
$('tk-layers-body').querySelectorAll('input[type=checkbox]').forEach((cb) =>
|
$('tk-layers-body').querySelectorAll('input[type=checkbox]').forEach((cb) =>
|
||||||
cb.addEventListener('change', () => {
|
cb.addEventListener('change', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue