@@ -903,7 +903,11 @@ const EMPTY_FC = { type: 'FeatureCollection', features: [] };
// 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_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
// (the feed's region field is noisy; cluster names classify cleanly).
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);
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 vehState(p) {
if (!p) return 'offline';
@@ -991,7 +997,11 @@ function pinImageData(fill) {
function addPinImages() {
const pins = {
'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))
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 });
tkMap.on('load', () => {
- addPinImages(); // teardrop pin icons (open by SLA, closed one slate colour)
- // Closed overlay (windowed) — drawn UNDER the live open layer; status irrelevant
- // so every closed ticket uses the single muted 'pin-closed', slightly smaller.
+ addPinImages(); // teardrop pin icons (open by SLA; closed = faded same SLA colour)
+ // Closed overlay (windowed) — drawn UNDER the live open layer. Each closed ticket
+ // 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.addLayer({
id: 'inc-closed', type: 'symbol', source: 'inc-closed',
layout: {
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-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.
tkMap.addSource('inc-open', { type: 'geojson', data: EMPTY_FC });
@@ -1229,22 +1241,25 @@ function showIncPopup(f, closed) {
}
if (p.geo_source === 'cluster') lines.push('
approx — cluster location
');
tkPopup.setLngLat(f.geometry.coordinates).setHTML(`
- ${escapeHtml(p.ticket_id || '—')} ${closed ? 'CLOSED' : 'OPEN'}
+ ${escapeHtml(p.ticket_id || '—')} ${closed ? 'CLOSED' : 'OPEN'}
${lines.join('')}
`).addTo(tkMap);
}
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: CLOSED_COLOR, n: m.closed_in_window ?? 0 },
- { id: 'vehicles', label: 'ISP vehicles', color: '#E8954A', n: vehCount },
+ { 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: 'vehicles', label: 'ISP vehicles', color: '#E8954A', n: vehCount },
];
let html = rows.map((r) =>
`
`).join('');
html += '
Open SLA
' + ['breached', 'at_risk', 'ok', 'unknown'].map((s) =>
`
${SLA_LABELS[s]}
`).join('');
+ // Closed = faded ('light') version of the same SLA colour (see CLOSED_SLA_BASE).
+ html += '
Closed SLA
' + [['Breached', closedColor('Breached')], ['Compliant', closedColor('Compliant')]].map(([lbl, col]) =>
+ `
${lbl}
`).join('');
$('tk-layers-body').innerHTML = html;
$('tk-layers-body').querySelectorAll('input[type=checkbox]').forEach((cb) =>
cb.addEventListener('change', () => {