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:
parent
620a82de55
commit
76d1c17830
2 changed files with 74 additions and 2 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
72
index.html
72
index.html
|
|
@ -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 = '');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue