feat(filters): sort number plates A→Z + add assigned-city filter
- Number-plate dropdown now sorted alphabetically (natural/numeric, placeholder pinned), re-sorted as live-discovered plates are added. - New 'Assigned city' filter: populated from the API + live feed, filters the live map instantly (with cost centre + plate), auto-fills from a picked vehicle, and is passed to the trips query (assigned_city). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
135253d37d
commit
b930582dc8
1 changed files with 38 additions and 10 deletions
48
index.html
48
index.html
|
|
@ -299,6 +299,10 @@
|
|||
<label for="f-cc">Cost centre</label>
|
||||
<select id="f-cc"><option value="">All cost centres</option></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="f-city">Assigned city</label>
|
||||
<select id="f-city"><option value="">All cities</option></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="f-period">Time</label>
|
||||
<select id="f-period">
|
||||
|
|
@ -683,6 +687,7 @@ async function loadFilters() {
|
|||
const data = await resp.json();
|
||||
fillVehicleSelect(data.vehicles || []);
|
||||
fillSelect('f-cc', data.cost_centres || []);
|
||||
fillSelect('f-city', data.cities || []);
|
||||
} catch (e) { console.error('loadFilters failed:', e); }
|
||||
}
|
||||
function fillVehicleSelect(vehicles) {
|
||||
|
|
@ -695,34 +700,53 @@ function fillVehicleSelect(vehicles) {
|
|||
opt.value = plate; opt.textContent = v.drivers ? `${plate} — ${v.drivers}` : plate;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sortSelect('f-vehicle');
|
||||
sel.addEventListener('change', applyVehicleAutoFilter);
|
||||
}
|
||||
function fillSelect(id, values) {
|
||||
const sel = document.getElementById(id);
|
||||
const have = new Set(Array.from(sel.options).map(o => o.value));
|
||||
values.forEach(v => { if (have.has(v)) return; const o = document.createElement('option'); o.value = v; o.textContent = v; sel.appendChild(o); });
|
||||
let added = false;
|
||||
values.forEach(v => { if (have.has(v)) return; const o = document.createElement('option'); o.value = v; o.textContent = v; sel.appendChild(o); added = true; });
|
||||
if (added) sortSelect(id);
|
||||
}
|
||||
// Add any plate/cc seen in the live feed that the filter-options endpoint missed
|
||||
// Reorder a select's options A→Z by value (natural/numeric), keeping any
|
||||
// blank-value placeholder ("All …") pinned at the top and preserving selection.
|
||||
function sortSelect(id) {
|
||||
const sel = document.getElementById(id);
|
||||
const selected = new Set(Array.from(sel.selectedOptions).map(o => o.value));
|
||||
const opts = Array.from(sel.options);
|
||||
const head = opts.filter(o => o.value === '');
|
||||
const body = opts.filter(o => o.value !== '')
|
||||
.sort((a, b) => a.value.localeCompare(b.value, undefined, { numeric: true, sensitivity: 'base' }));
|
||||
sel.replaceChildren(...head, ...body);
|
||||
Array.from(sel.options).forEach(o => { o.selected = selected.has(o.value); });
|
||||
}
|
||||
// Add any plate/cc/city seen in the live feed that the filter-options endpoint missed
|
||||
function populateFiltersFromLive(features) {
|
||||
const ccs = new Set(), vehSel = document.getElementById('f-vehicle');
|
||||
const ccs = new Set(), cities = new Set(), vehSel = document.getElementById('f-vehicle');
|
||||
const havePlates = new Set(Array.from(vehSel.options).map(o => o.value));
|
||||
let addedPlate = false;
|
||||
features.forEach(f => {
|
||||
const p = f.properties;
|
||||
if (p.cost_centre) ccs.add(p.cost_centre);
|
||||
if (p.assigned_city) cities.add(p.assigned_city);
|
||||
if (p.vehicle_number && !havePlates.has(p.vehicle_number)) {
|
||||
havePlates.add(p.vehicle_number);
|
||||
VEHICLE_META.set(p.vehicle_number, { cost_centre: p.cost_centre, assigned_city: p.assigned_city });
|
||||
const o = document.createElement('option'); o.value = p.vehicle_number; o.textContent = p.vehicle_number; vehSel.appendChild(o);
|
||||
const o = document.createElement('option'); o.value = p.vehicle_number; o.textContent = p.vehicle_number; vehSel.appendChild(o); addedPlate = true;
|
||||
}
|
||||
});
|
||||
if (addedPlate) sortSelect('f-vehicle');
|
||||
fillSelect('f-cc', Array.from(ccs).sort());
|
||||
fillSelect('f-city', Array.from(cities).sort());
|
||||
}
|
||||
function applyVehicleAutoFilter() {
|
||||
const sel = document.getElementById('f-vehicle');
|
||||
const selected = Array.from(sel.selectedOptions).map(o => o.value);
|
||||
const metas = selected.map(p => VEHICLE_META.get(p)).filter(Boolean);
|
||||
const sharedCC = collapse(metas.map(m => m.cost_centre));
|
||||
setSelectValue('f-cc', sharedCC ?? '');
|
||||
setSelectValue('f-cc', collapse(metas.map(m => m.cost_centre)) ?? '');
|
||||
setSelectValue('f-city', collapse(metas.map(m => m.assigned_city)) ?? '');
|
||||
applyLiveFilters(); // reflect the plate selection on the live map immediately
|
||||
}
|
||||
function collapse(values) { if (!values.length) return null; const f = values[0]; return values.every(v => v === f) ? f : null; }
|
||||
|
|
@ -742,10 +766,11 @@ function applyLiveFilters() {
|
|||
if (mode !== 'live') return;
|
||||
const plates = new Set(Array.from(document.getElementById('f-vehicle').selectedOptions).map(o => o.value).filter(Boolean));
|
||||
const cc = document.getElementById('f-cc').value;
|
||||
const city = document.getElementById('f-city').value;
|
||||
let total = 0, moving = 0, parked = 0, offline = 0; const speeds = [];
|
||||
(lastLivePayload?.geojson?.features || []).forEach(f => {
|
||||
const p = f.properties; const m = liveMarkers.get(p.imei); if (!m) return;
|
||||
const pass = (plates.size === 0 || plates.has(p.vehicle_number)) && (!cc || p.cost_centre === cc);
|
||||
const pass = (plates.size === 0 || plates.has(p.vehicle_number)) && (!cc || p.cost_centre === cc) && (!city || p.assigned_city === city);
|
||||
m.getElement().style.display = pass ? '' : 'none';
|
||||
if (!pass) return;
|
||||
total++;
|
||||
|
|
@ -761,8 +786,9 @@ function applyLiveFilters() {
|
|||
document.getElementById('f-period').addEventListener('change', e => {
|
||||
document.getElementById('custom').classList.toggle('show', e.target.value === 'custom');
|
||||
});
|
||||
// Selecting a cost centre filters the live map immediately.
|
||||
// Selecting a cost centre or assigned city filters the live map immediately.
|
||||
document.getElementById('f-cc').addEventListener('change', applyLiveFilters);
|
||||
document.getElementById('f-city').addEventListener('change', applyLiveFilters);
|
||||
function periodToRange(period, cs, ce) {
|
||||
const today = new Date(); const fmt = d => d.toISOString().slice(0, 10);
|
||||
const minus = n => { const x = new Date(today); x.setDate(x.getDate() - n); return x; };
|
||||
|
|
@ -780,19 +806,20 @@ function currentFilterSelection() {
|
|||
const period = document.getElementById('f-period').value;
|
||||
return {
|
||||
vehicles, cost_centre: document.getElementById('f-cc').value || '',
|
||||
assigned_city: document.getElementById('f-city').value || '',
|
||||
period, start: document.getElementById('f-start').value, end: document.getElementById('f-end').value,
|
||||
};
|
||||
}
|
||||
// "Show trips" button → fleet-wide / multi-vehicle trips from the filter card
|
||||
document.getElementById('show-trips').addEventListener('click', () => {
|
||||
const sel = currentFilterSelection();
|
||||
enterTrips({ vehicle_numbers: sel.vehicles, cost_centre: sel.cost_centre, period: sel.period, start: sel.start, end: sel.end });
|
||||
enterTrips({ vehicle_numbers: sel.vehicles, cost_centre: sel.cost_centre, assigned_city: sel.assigned_city, period: sel.period, start: sel.start, end: sel.end });
|
||||
});
|
||||
// Map-dot path → single vehicle, current period in the card
|
||||
function enterTripsForVehicle(plate) {
|
||||
if (!plate) return;
|
||||
const sel = currentFilterSelection();
|
||||
enterTrips({ vehicle_numbers: [plate], cost_centre: '', period: sel.period, start: sel.start, end: sel.end });
|
||||
enterTrips({ vehicle_numbers: [plate], cost_centre: '', assigned_city: '', period: sel.period, start: sel.start, end: sel.end });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -812,6 +839,7 @@ async function enterTrips(sel) {
|
|||
const params = new URLSearchParams({
|
||||
vehicle_numbers: (sel.vehicle_numbers || []).join(','),
|
||||
cost_centre: sel.cost_centre || '',
|
||||
assigned_city: sel.assigned_city || '',
|
||||
period: sel.period, start_date: range.start_date, end_date: range.end_date,
|
||||
});
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue