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 <noreply@anthropic.com>
This commit is contained in:
kianiadee 2026-06-06 11:14:16 +03:00
parent 620a82de55
commit 76d1c17830
2 changed files with 74 additions and 2 deletions

View file

@ -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. 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 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. 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: <https://fleetnow.rahamafresh.com> Live: <https://fleetnow.rahamafresh.com>

View file

@ -44,7 +44,24 @@
font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg); color: var(--text); overflow: hidden; 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 ───────────────────────────────────────────────────────── */ /* ── Top bar ───────────────────────────────────────────────────────── */
header { header {
@ -101,7 +118,12 @@
.clock b { font-weight: 600; } .clock b { font-weight: 600; }
/* ── Map + floating cards ──────────────────────────────────────────── */ /* ── 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 { .placeholder {
position: absolute; inset: 0; display: grid; place-items: center; position: absolute; inset: 0; display: grid; place-items: center;
color: var(--muted); font-size: 13px; text-align: center; padding: 32px; color: var(--muted); font-size: 13px; text-align: center; padding: 32px;
@ -281,6 +303,23 @@
<div class="clock"><span class="label">EAT</span><b id="clock-time"></b></div> <div class="clock"><span class="label">EAT</span><b id="clock-time"></b></div>
</header> </header>
<!-- Trips context bar (shown only in trips mode) -->
<div class="tripbar" id="tripbar">
<div class="ctx"><span class="lbl">Filter:</span> <span id="trip-ctx"></span></div>
<div class="bookend">
<span class="lbl">First trip</span>
<span class="when" id="be-first-when"></span>
<span class="veh" id="be-first-veh"></span>
<span class="where" id="be-first-where"></span>
</div>
<div class="bookend">
<span class="lbl">Last trip</span>
<span class="when" id="be-last-when"></span>
<span class="veh" id="be-last-veh"></span>
<span class="where" id="be-last-where"></span>
</div>
</div>
<div id="map"> <div id="map">
<div class="placeholder" id="placeholder">Loading live fleet…</div> <div class="placeholder" id="placeholder">Loading live fleet…</div>
@ -883,11 +922,21 @@ function switchToTripsMode() {
clearTrail(); trailedVehicle = null; clearTrail(); trailedVehicle = null;
liveMarkers.forEach(m => m.getElement().style.display = 'none'); liveMarkers.forEach(m => m.getElement().style.display = 'none');
document.getElementById('triplist').classList.add('show'); document.getElementById('triplist').classList.add('show');
document.getElementById('tripbar').classList.add('show');
document.getElementById('live-pill').classList.add('show'); document.getElementById('live-pill').classList.add('show');
document.getElementById('stale-chip').style.display = 'none'; document.getElementById('stale-chip').style.display = 'none';
const ph = document.getElementById('placeholder'); if (ph) ph.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) { function renderTrips(payload, sel) {
const s = payload.summary || {}; const s = payload.summary || {};
document.getElementById('kpis').innerHTML = ` document.getElementById('kpis').innerHTML = `
@ -899,6 +948,24 @@ function renderTrips(payload, sel) {
<div class="kpi"><b>${formatNum(s.unique_drivers)}</b><span>Drivers</span></div> <div class="kpi"><b>${formatNum(s.unique_drivers)}</b><span>Drivers</span></div>
<div class="kpi"><b style="font-size:13px">${s.date_min ?? '?'} → ${s.date_max ?? '?'}</b><span>Range</span></div>`; <div class="kpi"><b style="font-size:13px">${s.date_min ?? '?'} → ${s.date_max ?? '?'}</b><span>Range</span></div>`;
// 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: [] }; const g = payload.geojson || { type: 'FeatureCollection', features: [] };
(g.features || []).forEach(f => { f.properties = f.properties || {}; f.properties.color = seqColor(f.properties.daily_seq); }); (g.features || []).forEach(f => { f.properties = f.properties || {}; f.properties.color = seqColor(f.properties.daily_seq); });
lastTripFeatures = g.features || []; lastTripFeatures = g.features || [];
@ -1053,6 +1120,7 @@ function backToLive() {
mode = 'live'; mode = 'live';
clearAnim(); removeTripLayers(); clearAnim(); removeTripLayers();
document.getElementById('triplist').classList.remove('show'); document.getElementById('triplist').classList.remove('show');
document.getElementById('tripbar').classList.remove('show');
document.getElementById('live-pill').classList.remove('show'); document.getElementById('live-pill').classList.remove('show');
document.getElementById('stale-chip').style.display = ''; document.getElementById('stale-chip').style.display = '';
liveMarkers.forEach(m => m.getElement().style.display = ''); liveMarkers.forEach(m => m.getElement().style.display = '');