From 558f095392cdaf5c5a2193b78495c66f4375f270 Mon Sep 17 00:00:00 2001 From: kianiadee Date: Fri, 29 May 2026 03:36:33 +0300 Subject: [PATCH] Trip dock: default date to vehicle's last-active day; UI review fixes - Plain click defaults the trip date to the vehicle's most recent fix day (no more empty 'today' at night); manual date picks are respected. - Animated trip path/marker now use the trip's palette colour (matched the card). - Filter dropdown skips no-op rebuilds on the 15s refresh and won't rebuild under an open popover. - applyClientFilter: drop redundant length>0 guard. Co-Authored-By: Claude Opus 4.8 --- web/fleet-core.js | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/web/fleet-core.js b/web/fleet-core.js index 3789b4b..edcce4e 100644 --- a/web/fleet-core.js +++ b/web/fleet-core.js @@ -553,6 +553,7 @@ function _buildMultiSelect(root, { label, plural, showSwatch }) { const listeners = []; let options = []; // [{value, color?}] + let lastSig = ''; // skip no-op rebuilds on the 15s live refresh const updateLabel = () => { const checked = [...optsRoot.querySelectorAll('input:checked')]; @@ -597,6 +598,12 @@ function _buildMultiSelect(root, { label, plural, showSwatch }) { return { setOptions(opts) { + // Skip the DOM rebuild when the option set is unchanged (the 15s live + // refresh re-sends the same list) or while the popover is open — both + // would otherwise flicker the list / disrupt a mid-selection. + const sig = opts.map(o => `${o.value}:${o.color || ''}`).join('|'); + if (sig === lastSig || !pop.hasAttribute('hidden')) return; + lastSig = sig; options = opts; // Preserve current selections by value when re-rendering const prevChecked = new Set( @@ -635,10 +642,10 @@ function _buildMultiSelect(root, { label, plural, showSwatch }) { export function applyClientFilter(map, { costCentres = [], cities = [] } = {}) { const layers = ['vehicles-circle', 'vehicles-arrow', 'vehicles-label']; const conds = []; - if (costCentres.length > 0 && costCentres.length > 1) { + if (costCentres.length > 1) { conds.push(['in', ['get', 'cost_centre'], ['literal', costCentres]]); } - if (cities.length > 0 && cities.length > 1) { + if (cities.length > 1) { conds.push(['in', ['get', 'assigned_city'], ['literal', cities]]); } const filter = conds.length === 0 ? null @@ -703,6 +710,9 @@ let _tripAnimRAF = null; // vehicleId → { plate, driver, color, payload | null } const _selection = new Map(); let _currentDate = null; +// Once the user picks a date in the dock we stop auto-jumping to a vehicle's +// last-active day on click, so date-browsing isn't fought by every click. +let _dateUserPicked = false; function _nextColor() { const used = new Set([..._selection.values()].map(v => v.color)); @@ -725,6 +735,7 @@ export function initTripPanel(map, panelRoot) { els.close.addEventListener('click', () => _closeTripPanel(map, panelRoot, els)); els.date.addEventListener('change', async () => { + _dateUserPicked = true; _currentDate = els.date.value; // Re-fetch every currently-selected vehicle for the new date. for (const vid of [..._selection.keys()]) { @@ -749,7 +760,15 @@ export function initTripPanel(map, panelRoot) { const driver = f.properties.driver_name || ''; const multi = e.originalEvent.metaKey || e.originalEvent.ctrlKey || e.originalEvent.shiftKey; - if (!els.date.value) els.date.value = _todayEat(); + // Plain click → jump to the vehicle's most recent active day (its last + // fix), so the dock never opens on an empty "today" before the fleet has + // moved. Once the user has chosen a date, respect it. Multi-click keeps + // the current date so compared vehicles share one day. + if (!multi && !_dateUserPicked) { + els.date.value = _eatDate(f.properties.occurred_at); + } else if (!els.date.value) { + els.date.value = _todayEat(); + } _currentDate = els.date.value; panelRoot.classList.add('open'); panelRoot.setAttribute('aria-hidden', 'false'); @@ -1048,6 +1067,9 @@ function _showAndAnimateTrip(map, trip) { _clearSingleTripLayers(map); if (!trip.path || !trip.path.coordinates || trip.path.coordinates.length < 2) return; const coords = trip.path.coordinates; + // Match the trip card's palette colour so the animated route reads as the + // same trip the user clicked. + const color = _tripColor(trip.trip_id); map.addSource(TRIP_PATH_SOURCE, { type: 'geojson', data: trip.path }); map.addLayer({ @@ -1056,7 +1078,7 @@ function _showAndAnimateTrip(map, trip) { source: TRIP_PATH_SOURCE, layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { - 'line-color': '#10b981', + 'line-color': color, 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 2, 14, 4, 17, 6], 'line-opacity': 0.85, }, @@ -1073,7 +1095,7 @@ function _showAndAnimateTrip(map, trip) { paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 8, 4, 14, 8, 17, 12], 'circle-color': '#ffffff', - 'circle-stroke-color': '#10b981', + 'circle-stroke-color': color, 'circle-stroke-width': 3, }, }); @@ -1161,6 +1183,14 @@ function _todayEat() { return eat.toISOString().slice(0, 10); } +// EAT (UTC+3) calendar date of a UTC ISO timestamp, as YYYY-MM-DD. Falls back +// to today when the timestamp is missing/unparseable. +function _eatDate(iso) { + const d = iso ? new Date(iso) : new Date(); + if (Number.isNaN(d.getTime())) return _todayEat(); + return new Date(d.getTime() + 3 * 3600 * 1000).toISOString().slice(0, 10); +} + function _formatTimeOnly(iso) { if (!iso) return '—'; const d = new Date(iso);