From 76d1c1783069b2c6e8ea4fe4f7c1a848001e30a8 Mon Sep 17 00:00:00 2001 From: kianiadee Date: Sat, 6 Jun 2026 11:14:16 +0300 Subject: [PATCH] feat(trips): context bar with filter summary + first/last trip bookends Trips view now shows a second bar under the header: the active filter (vehicle/cost centre/city) plus first-trip and last-trip bookends, each with the server's reverse-geocoded location + timestamp (first_trip_start_*, last_trip_end_*). Pinned grid rows so a hidden tripbar doesn't collapse the map. Co-Authored-By: Claude Opus 4.8 --- README.md | 4 +++ index.html | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6805444..3cd07b3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ trips** into one view for the Fireside Communications / Tracksolid fleet. plate / cost centre / city + period and hit **Show trips** for a fleet-wide pull. The map switches to **seq-coloured trip routes** with start/end markers and a click-to-animate replay; the **● Live** pill returns to the live snapshot. +- In trips view a **context bar** (below the header) summarises the active filter + (vehicle / cost centre / city) plus the **first trip** and **last trip** — + each with its **reverse-geocoded location and timestamp** — alongside the KPI + totals (trips, km, driving/idle hours, vehicles, drivers, date range). Live: diff --git a/index.html b/index.html index 848c6a1..bbae847 100644 --- a/index.html +++ b/index.html @@ -44,7 +44,24 @@ font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); overflow: hidden; } - .app { display: grid; grid-template-rows: auto 1fr; height: 100vh; } + .app { display: grid; grid-template-rows: auto auto 1fr; height: 100vh; } + + /* Trips context bar (second row, trips mode only): active filter + first/last + trip bookends with reverse-geocoded location + timestamp. */ + .tripbar { display: none; } + .tripbar.show { + display: flex; align-items: stretch; + background: var(--panel); border-bottom: 1px solid var(--border); font-size: 12px; + } + .tripbar > div { padding: 7px 16px; display: flex; gap: 9px; align-items: baseline; min-width: 0; } + .tripbar > div + div { border-left: 1px solid var(--border); } + .tripbar .ctx { flex: 0 0 auto; color: var(--accent); font-weight: 700; align-items: center; } + .tripbar .ctx .lbl { color: var(--muted); } + .tripbar .bookend { flex: 1 1 0; } + .tripbar .lbl { font-size: 9.5px; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); flex: none; } + .tripbar .when { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; flex: none; } + .tripbar .veh { color: var(--accent); font-weight: 600; flex: none; } + .tripbar .where { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 0; min-width: 0; } /* ── Top bar ───────────────────────────────────────────────────────── */ header { @@ -101,7 +118,12 @@ .clock b { font-weight: 600; } /* ── Map + floating cards ──────────────────────────────────────────── */ - #map { position: relative; min-height: 0; height: 100%; } + /* Pin to row 3 so the map always takes the 1fr track whether or not the + tripbar (row 2) is shown — otherwise a hidden tripbar lets the map fall + into the auto row and collapse. */ + #map { position: relative; min-height: 0; height: 100%; grid-row: 3; } + header { grid-row: 1; } + .tripbar { grid-row: 2; } .placeholder { position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted); font-size: 13px; text-align: center; padding: 32px; @@ -281,6 +303,23 @@
EAT
+ +
+
Filter:
+
+ First trip + + + +
+
+ Last trip + + + +
+
+
Loading live fleet…
@@ -883,11 +922,21 @@ function switchToTripsMode() { clearTrail(); trailedVehicle = null; liveMarkers.forEach(m => m.getElement().style.display = 'none'); document.getElementById('triplist').classList.add('show'); + document.getElementById('tripbar').classList.add('show'); document.getElementById('live-pill').classList.add('show'); document.getElementById('stale-chip').style.display = 'none'; const ph = document.getElementById('placeholder'); if (ph) ph.style.display = 'none'; } +// "YYYY-MM-DD HH:MM:SS" → "DD Mon HH:MM" (EAT, compact) for the bookends. +function fmtTripWhen(ts) { + if (!ts) return '—'; + const m = String(ts).match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/); + if (!m) return ts; + const mon = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][+m[2] - 1]; + return `${+m[3]} ${mon} ${m[4]}:${m[5]}`; +} + function renderTrips(payload, sel) { const s = payload.summary || {}; document.getElementById('kpis').innerHTML = ` @@ -899,6 +948,24 @@ function renderTrips(payload, sel) {
${formatNum(s.unique_drivers)}Drivers
${s.date_min ?? '?'} → ${s.date_max ?? '?'}Range
`; + // Context bar: what was filtered + first/last trip bookends (reverse-geocoded + // location + timestamp, straight from the server summary). + const ctxParts = []; + const vlist = (sel && sel.vehicle_numbers) || []; + if (vlist.length === 1) ctxParts.push(vlist[0]); + else if (vlist.length > 1) ctxParts.push(`${vlist.length} vehicles`); + if (sel && sel.cost_centre) ctxParts.push(sel.cost_centre); + if (sel && sel.assigned_city) ctxParts.push(sel.assigned_city); + document.getElementById('trip-ctx').textContent = ctxParts.length ? ctxParts.join(' · ') : 'Whole fleet'; + + const multiVeh = (s.unique_vehicles || 0) > 1; + document.getElementById('be-first-when').textContent = fmtTripWhen(s.first_trip_start_time); + document.getElementById('be-first-veh').textContent = multiVeh ? (s.first_trip_vehicle || '') : ''; + document.getElementById('be-first-where').textContent = s.first_trip_start_address || 'location not available'; + document.getElementById('be-last-when').textContent = fmtTripWhen(s.last_trip_end_time); + document.getElementById('be-last-veh').textContent = multiVeh ? (s.last_trip_vehicle || '') : ''; + document.getElementById('be-last-where').textContent = s.last_trip_end_address || 'location not available'; + const g = payload.geojson || { type: 'FeatureCollection', features: [] }; (g.features || []).forEach(f => { f.properties = f.properties || {}; f.properties.color = seqColor(f.properties.daily_seq); }); lastTripFeatures = g.features || []; @@ -1053,6 +1120,7 @@ function backToLive() { mode = 'live'; clearAnim(); removeTripLayers(); document.getElementById('triplist').classList.remove('show'); + document.getElementById('tripbar').classList.remove('show'); document.getElementById('live-pill').classList.remove('show'); document.getElementById('stale-chip').style.display = ''; liveMarkers.forEach(m => m.getElement().style.display = '');