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>
|
<label for="f-cc">Cost centre</label>
|
||||||
<select id="f-cc"><option value="">All cost centres</option></select>
|
<select id="f-cc"><option value="">All cost centres</option></select>
|
||||||
</div>
|
</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">
|
<div class="field">
|
||||||
<label for="f-period">Time</label>
|
<label for="f-period">Time</label>
|
||||||
<select id="f-period">
|
<select id="f-period">
|
||||||
|
|
@ -683,6 +687,7 @@ async function loadFilters() {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
fillVehicleSelect(data.vehicles || []);
|
fillVehicleSelect(data.vehicles || []);
|
||||||
fillSelect('f-cc', data.cost_centres || []);
|
fillSelect('f-cc', data.cost_centres || []);
|
||||||
|
fillSelect('f-city', data.cities || []);
|
||||||
} catch (e) { console.error('loadFilters failed:', e); }
|
} catch (e) { console.error('loadFilters failed:', e); }
|
||||||
}
|
}
|
||||||
function fillVehicleSelect(vehicles) {
|
function fillVehicleSelect(vehicles) {
|
||||||
|
|
@ -695,34 +700,53 @@ function fillVehicleSelect(vehicles) {
|
||||||
opt.value = plate; opt.textContent = v.drivers ? `${plate} — ${v.drivers}` : plate;
|
opt.value = plate; opt.textContent = v.drivers ? `${plate} — ${v.drivers}` : plate;
|
||||||
sel.appendChild(opt);
|
sel.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
sortSelect('f-vehicle');
|
||||||
sel.addEventListener('change', applyVehicleAutoFilter);
|
sel.addEventListener('change', applyVehicleAutoFilter);
|
||||||
}
|
}
|
||||||
function fillSelect(id, values) {
|
function fillSelect(id, values) {
|
||||||
const sel = document.getElementById(id);
|
const sel = document.getElementById(id);
|
||||||
const have = new Set(Array.from(sel.options).map(o => o.value));
|
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) {
|
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));
|
const havePlates = new Set(Array.from(vehSel.options).map(o => o.value));
|
||||||
|
let addedPlate = false;
|
||||||
features.forEach(f => {
|
features.forEach(f => {
|
||||||
const p = f.properties;
|
const p = f.properties;
|
||||||
if (p.cost_centre) ccs.add(p.cost_centre);
|
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)) {
|
if (p.vehicle_number && !havePlates.has(p.vehicle_number)) {
|
||||||
havePlates.add(p.vehicle_number);
|
havePlates.add(p.vehicle_number);
|
||||||
VEHICLE_META.set(p.vehicle_number, { cost_centre: p.cost_centre, assigned_city: p.assigned_city });
|
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-cc', Array.from(ccs).sort());
|
||||||
|
fillSelect('f-city', Array.from(cities).sort());
|
||||||
}
|
}
|
||||||
function applyVehicleAutoFilter() {
|
function applyVehicleAutoFilter() {
|
||||||
const sel = document.getElementById('f-vehicle');
|
const sel = document.getElementById('f-vehicle');
|
||||||
const selected = Array.from(sel.selectedOptions).map(o => o.value);
|
const selected = Array.from(sel.selectedOptions).map(o => o.value);
|
||||||
const metas = selected.map(p => VEHICLE_META.get(p)).filter(Boolean);
|
const metas = selected.map(p => VEHICLE_META.get(p)).filter(Boolean);
|
||||||
const sharedCC = collapse(metas.map(m => m.cost_centre));
|
setSelectValue('f-cc', collapse(metas.map(m => m.cost_centre)) ?? '');
|
||||||
setSelectValue('f-cc', sharedCC ?? '');
|
setSelectValue('f-city', collapse(metas.map(m => m.assigned_city)) ?? '');
|
||||||
applyLiveFilters(); // reflect the plate selection on the live map immediately
|
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; }
|
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;
|
if (mode !== 'live') return;
|
||||||
const plates = new Set(Array.from(document.getElementById('f-vehicle').selectedOptions).map(o => o.value).filter(Boolean));
|
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 cc = document.getElementById('f-cc').value;
|
||||||
|
const city = document.getElementById('f-city').value;
|
||||||
let total = 0, moving = 0, parked = 0, offline = 0; const speeds = [];
|
let total = 0, moving = 0, parked = 0, offline = 0; const speeds = [];
|
||||||
(lastLivePayload?.geojson?.features || []).forEach(f => {
|
(lastLivePayload?.geojson?.features || []).forEach(f => {
|
||||||
const p = f.properties; const m = liveMarkers.get(p.imei); if (!m) return;
|
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';
|
m.getElement().style.display = pass ? '' : 'none';
|
||||||
if (!pass) return;
|
if (!pass) return;
|
||||||
total++;
|
total++;
|
||||||
|
|
@ -761,8 +786,9 @@ function applyLiveFilters() {
|
||||||
document.getElementById('f-period').addEventListener('change', e => {
|
document.getElementById('f-period').addEventListener('change', e => {
|
||||||
document.getElementById('custom').classList.toggle('show', e.target.value === 'custom');
|
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-cc').addEventListener('change', applyLiveFilters);
|
||||||
|
document.getElementById('f-city').addEventListener('change', applyLiveFilters);
|
||||||
function periodToRange(period, cs, ce) {
|
function periodToRange(period, cs, ce) {
|
||||||
const today = new Date(); const fmt = d => d.toISOString().slice(0, 10);
|
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; };
|
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;
|
const period = document.getElementById('f-period').value;
|
||||||
return {
|
return {
|
||||||
vehicles, cost_centre: document.getElementById('f-cc').value || '',
|
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,
|
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
|
// "Show trips" button → fleet-wide / multi-vehicle trips from the filter card
|
||||||
document.getElementById('show-trips').addEventListener('click', () => {
|
document.getElementById('show-trips').addEventListener('click', () => {
|
||||||
const sel = currentFilterSelection();
|
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
|
// Map-dot path → single vehicle, current period in the card
|
||||||
function enterTripsForVehicle(plate) {
|
function enterTripsForVehicle(plate) {
|
||||||
if (!plate) return;
|
if (!plate) return;
|
||||||
const sel = currentFilterSelection();
|
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({
|
const params = new URLSearchParams({
|
||||||
vehicle_numbers: (sel.vehicle_numbers || []).join(','),
|
vehicle_numbers: (sel.vehicle_numbers || []).join(','),
|
||||||
cost_centre: sel.cost_centre || '',
|
cost_centre: sel.cost_centre || '',
|
||||||
|
assigned_city: sel.assigned_city || '',
|
||||||
period: sel.period, start_date: range.start_date, end_date: range.end_date,
|
period: sel.period, start_date: range.start_date, end_date: range.end_date,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue