feat(tickets map): day-total KPI, squircle+bolt markers, new SLA palette
- Top bar: new lead KPI "Total today (open + closed)" = open_now + closed_in_window. At midnight closed=0 so it shows the start-of-day backlog, then tracks the day's full workload as tickets close. - Markers: replace the ticket-stub with a squircle + lightning-bolt (incident/fault feel; distinct from round vehicle markers), drawn via Path2D. - SLA palette + labels: Out of SLA = strong maroon red (#A01830), At risk = deep yellow (#E0A800), Within SLA = deep purple (#6B21A8); legend/popups relabelled "Out of SLA / At risk / Within SLA". Closed stays a pastel of the same colour. Daily reset is inherent: the closed layer is windowed to today (closed_at >= today), so after midnight it empties and the map starts with open tickets by live SLA. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b653327b0f
commit
8a0e1304ab
1 changed files with 19 additions and 15 deletions
|
|
@ -906,8 +906,9 @@ const STALE_GPS_MS = 10 * 60 * 1000;
|
||||||
const LIVE_POLL_MS = 15000;
|
const LIVE_POLL_MS = 15000;
|
||||||
const EMPTY_FC = { type: 'FeatureCollection', features: [] };
|
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' };
|
// SLA palette: out-of-SLA = strong maroon red, within-SLA = deep purple, at-risk = deep yellow.
|
||||||
const SLA_LABELS = { breached: 'Breached', at_risk: 'At risk', ok: 'OK', unknown: 'Unknown' };
|
const SLA_COLORS = { breached: '#A01830', at_risk: '#E0A800', ok: '#6B21A8', unknown: '#6b7280' };
|
||||||
|
const SLA_LABELS = { breached: 'Out of SLA', at_risk: 'At risk', ok: 'Within SLA', unknown: 'Unknown' };
|
||||||
const CLOSED_COLOR = '#94a3b8'; // fallback slate — closed tickets with no SLA outcome
|
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
|
// 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
|
// tickets show vivid and closed ones a washed-out same-colour — status stays apparent
|
||||||
|
|
@ -983,25 +984,24 @@ function incQs() {
|
||||||
return p.toString();
|
return p.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ticket-stub map marker: an admission-ticket silhouette (perforation notches on the
|
// Squircle + bolt map marker: a rounded squircle tapering to a bottom point (anchors on
|
||||||
// sides + a dashed tear line) tapering to a bottom point that anchors on the location.
|
// the location) with a white lightning bolt — denotes an incident/fault, and is distinct
|
||||||
// Drawn at 4x for crispness (paired with pixelRatio 4 in addImage). The `fill` param
|
// from the round vehicle markers. Drawn via Path2D at 4x (paired with pixelRatio 4 in
|
||||||
// lets the same shape back every pin — vivid SLA colours for open, pastel for closed.
|
// addImage). `fill` = SLA colour (vivid for open, pastel for closed).
|
||||||
const _PIN_BODY = 'M5 2h18a2 2 0 0 1 2 2v3a2.4 2.4 0 0 0 0 4.8V15a2 2 0 0 1-2 2H15.3'
|
const _PIN_BODY = 'M12 1.5c5.2 0 8.5 3.3 8.5 8.5 0 4.2-3 7.2-8.5 12.5'
|
||||||
+ 'L14 20.5 12.7 17H5a2 2 0 0 1-2-2v-3.2a2.4 2.4 0 0 0 0-4.8V4a2 2 0 0 1 2-2Z';
|
+ 'C6.5 17.2 3.5 14.2 3.5 10 3.5 4.8 6.8 1.5 12 1.5Z';
|
||||||
|
const _PIN_GLYPH = 'M13 5.5l-4 5.2h2.6L10.8 15l4-5.4h-2.6L13 5.5z'; // lightning bolt
|
||||||
function pinImageData(fill) {
|
function pinImageData(fill) {
|
||||||
const S = 4, vbW = 28, vbH = 21.5; // viewBox units; tip at (14, 20.5)
|
const S = 4, vbW = 24, vbH = 24; // viewBox units; tip at (12, 22.5)
|
||||||
const cv = document.createElement('canvas');
|
const cv = document.createElement('canvas');
|
||||||
cv.width = vbW * S; cv.height = Math.ceil(vbH * S);
|
cv.width = vbW * S; cv.height = vbH * S;
|
||||||
const ctx = cv.getContext('2d');
|
const ctx = cv.getContext('2d');
|
||||||
ctx.scale(S, S);
|
ctx.scale(S, S);
|
||||||
const body = new Path2D(_PIN_BODY);
|
const body = new Path2D(_PIN_BODY);
|
||||||
ctx.fillStyle = fill; ctx.fill(body);
|
ctx.fillStyle = fill; ctx.fill(body);
|
||||||
ctx.lineJoin = 'round'; ctx.lineWidth = 1.5; ctx.strokeStyle = 'rgba(255,255,255,.95)';
|
ctx.lineJoin = 'round'; ctx.lineWidth = 1.6; ctx.strokeStyle = 'rgba(255,255,255,.95)';
|
||||||
ctx.stroke(body);
|
ctx.stroke(body);
|
||||||
ctx.setLineDash([1.6, 1.8]); ctx.lineWidth = 1.4; // perforation tear line down the middle
|
ctx.fillStyle = 'rgba(255,255,255,.95)'; ctx.fill(new Path2D(_PIN_GLYPH));
|
||||||
ctx.beginPath(); ctx.moveTo(14, 4.5); ctx.lineTo(14, 14.5); ctx.stroke();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
return ctx.getImageData(0, 0, cv.width, cv.height);
|
return ctx.getImageData(0, 0, cv.width, cv.height);
|
||||||
}
|
}
|
||||||
function addPinImages() {
|
function addPinImages() {
|
||||||
|
|
@ -1090,7 +1090,7 @@ 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(); // ticket-stub pin icons (open by SLA; closed = faded same SLA colour)
|
addPinImages(); // squircle+bolt pin icons (open by SLA; closed = faded same SLA colour)
|
||||||
// Closed overlay (windowed) — drawn UNDER the live open layer. Each closed ticket
|
// 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→
|
// 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.
|
// light green), slightly smaller, so it reads as the same status but inactive.
|
||||||
|
|
@ -1140,7 +1140,11 @@ function updateVehScale() {
|
||||||
function renderIncMetrics(m, freshness) {
|
function renderIncMetrics(m, freshness) {
|
||||||
m = m || {};
|
m = m || {};
|
||||||
const so = (m.sla && m.sla.open) || {}, sc = (m.sla && m.sla.closed) || {}, cr = m.closure_rate || {};
|
const so = (m.sla && m.sla.open) || {}, sc = (m.sla && m.sla.closed) || {}, cr = m.closure_rate || {};
|
||||||
|
// Tickets we're working with today = still-open + already-closed-today. At midnight
|
||||||
|
// closed=0 so this is the start-of-day backlog; it then tracks the day's full workload.
|
||||||
|
const dayTotal = (m.open_now || 0) + (m.closed_in_window || 0);
|
||||||
const tiles = [
|
const tiles = [
|
||||||
|
`<div class="metric"><b class="accent">${intg(dayTotal)}</b><span class="lbl">Total today (open + closed)</span></div>`,
|
||||||
`<div class="metric"><b class="accent">${intg(m.open_now)}</b><span class="lbl">Open now</span></div>`,
|
`<div class="metric"><b class="accent">${intg(m.open_now)}</b><span class="lbl">Open now</span></div>`,
|
||||||
`<div class="metric"><b>${intg(m.closed_in_window)}</b><span class="lbl">Closed in window</span></div>`,
|
`<div class="metric"><b>${intg(m.closed_in_window)}</b><span class="lbl">Closed in window</span></div>`,
|
||||||
`<div class="metric"><b class="sla-breached">${intg(so.breached)}</b><span class="lbl">Open breached</span>
|
`<div class="metric"><b class="sla-breached">${intg(so.breached)}</b><span class="lbl">Open breached</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue