Trip dock: default date to vehicle's last-active day; UI review fixes
Some checks are pending
build / lint-test (push) Waiting to run
build / build-push (push) Blocked by required conditions

- 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 <noreply@anthropic.com>
This commit is contained in:
kianiadee 2026-05-29 03:36:33 +03:00
parent d410216a4d
commit 558f095392

View file

@ -553,6 +553,7 @@ function _buildMultiSelect(root, { label, plural, showSwatch }) {
const listeners = []; const listeners = [];
let options = []; // [{value, color?}] let options = []; // [{value, color?}]
let lastSig = ''; // skip no-op rebuilds on the 15s live refresh
const updateLabel = () => { const updateLabel = () => {
const checked = [...optsRoot.querySelectorAll('input:checked')]; const checked = [...optsRoot.querySelectorAll('input:checked')];
@ -597,6 +598,12 @@ function _buildMultiSelect(root, { label, plural, showSwatch }) {
return { return {
setOptions(opts) { 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; options = opts;
// Preserve current selections by value when re-rendering // Preserve current selections by value when re-rendering
const prevChecked = new Set( const prevChecked = new Set(
@ -635,10 +642,10 @@ function _buildMultiSelect(root, { label, plural, showSwatch }) {
export function applyClientFilter(map, { costCentres = [], cities = [] } = {}) { export function applyClientFilter(map, { costCentres = [], cities = [] } = {}) {
const layers = ['vehicles-circle', 'vehicles-arrow', 'vehicles-label']; const layers = ['vehicles-circle', 'vehicles-arrow', 'vehicles-label'];
const conds = []; const conds = [];
if (costCentres.length > 0 && costCentres.length > 1) { if (costCentres.length > 1) {
conds.push(['in', ['get', 'cost_centre'], ['literal', costCentres]]); 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]]); conds.push(['in', ['get', 'assigned_city'], ['literal', cities]]);
} }
const filter = conds.length === 0 ? null const filter = conds.length === 0 ? null
@ -703,6 +710,9 @@ let _tripAnimRAF = null;
// vehicleId → { plate, driver, color, payload | null } // vehicleId → { plate, driver, color, payload | null }
const _selection = new Map(); const _selection = new Map();
let _currentDate = null; 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() { function _nextColor() {
const used = new Set([..._selection.values()].map(v => v.color)); 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.close.addEventListener('click', () => _closeTripPanel(map, panelRoot, els));
els.date.addEventListener('change', async () => { els.date.addEventListener('change', async () => {
_dateUserPicked = true;
_currentDate = els.date.value; _currentDate = els.date.value;
// Re-fetch every currently-selected vehicle for the new date. // Re-fetch every currently-selected vehicle for the new date.
for (const vid of [..._selection.keys()]) { for (const vid of [..._selection.keys()]) {
@ -749,7 +760,15 @@ export function initTripPanel(map, panelRoot) {
const driver = f.properties.driver_name || ''; const driver = f.properties.driver_name || '';
const multi = e.originalEvent.metaKey || e.originalEvent.ctrlKey || e.originalEvent.shiftKey; 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; _currentDate = els.date.value;
panelRoot.classList.add('open'); panelRoot.classList.add('open');
panelRoot.setAttribute('aria-hidden', 'false'); panelRoot.setAttribute('aria-hidden', 'false');
@ -1048,6 +1067,9 @@ function _showAndAnimateTrip(map, trip) {
_clearSingleTripLayers(map); _clearSingleTripLayers(map);
if (!trip.path || !trip.path.coordinates || trip.path.coordinates.length < 2) return; if (!trip.path || !trip.path.coordinates || trip.path.coordinates.length < 2) return;
const coords = trip.path.coordinates; 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.addSource(TRIP_PATH_SOURCE, { type: 'geojson', data: trip.path });
map.addLayer({ map.addLayer({
@ -1056,7 +1078,7 @@ function _showAndAnimateTrip(map, trip) {
source: TRIP_PATH_SOURCE, source: TRIP_PATH_SOURCE,
layout: { 'line-join': 'round', 'line-cap': 'round' }, layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { paint: {
'line-color': '#10b981', 'line-color': color,
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 2, 14, 4, 17, 6], 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 2, 14, 4, 17, 6],
'line-opacity': 0.85, 'line-opacity': 0.85,
}, },
@ -1073,7 +1095,7 @@ function _showAndAnimateTrip(map, trip) {
paint: { paint: {
'circle-radius': ['interpolate', ['linear'], ['zoom'], 8, 4, 14, 8, 17, 12], 'circle-radius': ['interpolate', ['linear'], ['zoom'], 8, 4, 14, 8, 17, 12],
'circle-color': '#ffffff', 'circle-color': '#ffffff',
'circle-stroke-color': '#10b981', 'circle-stroke-color': color,
'circle-stroke-width': 3, 'circle-stroke-width': 3,
}, },
}); });
@ -1161,6 +1183,14 @@ function _todayEat() {
return eat.toISOString().slice(0, 10); 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) { function _formatTimeOnly(iso) {
if (!iso) return '—'; if (!iso) return '—';
const d = new Date(iso); const d = new Date(iso);