UI tweaks + city case fix
- Migration 20: collapse `Nairobi`/`nairobi` in domain.vehicles → 'nairobi' - Remove the SLO panel from the top band (filter + tile rows stay) - Offline vehicles render as solid grey instead of dim-cost-centre tint; opacity now only differentiates moving (1.0) vs parked (0.75) vs offline (0.55) so colour carries identity + state cleanly
This commit is contained in:
parent
20958c0293
commit
de34103f18
3 changed files with 28 additions and 14 deletions
16
db/migrations/20260601000020_normalize_assigned_city.sql
Normal file
16
db/migrations/20260601000020_normalize_assigned_city.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- migrate:up
|
||||||
|
--
|
||||||
|
-- Data hygiene: 1 vehicle had `assigned_city = 'Nairobi'` while the other
|
||||||
|
-- 54 had `'nairobi'`. Collapse to lower-case so the filter dropdown shows
|
||||||
|
-- a single canonical option. The projector doesn't set assigned_city (it
|
||||||
|
-- comes from manual edits / roster imports) so no other code change is
|
||||||
|
-- needed to prevent recurrence.
|
||||||
|
|
||||||
|
UPDATE domain.vehicles
|
||||||
|
SET assigned_city = trim(lower(assigned_city)),
|
||||||
|
updated_at = now()
|
||||||
|
WHERE assigned_city IS NOT NULL
|
||||||
|
AND assigned_city != trim(lower(assigned_city));
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
-- No-op: re-introducing inconsistent casing would be a regression.
|
||||||
|
|
@ -141,16 +141,19 @@ export function initMap(elementId, opts = {}) {
|
||||||
15, 13,
|
15, 13,
|
||||||
18, 20,
|
18, 20,
|
||||||
],
|
],
|
||||||
// Always tint by cost-centre colour — operational state is shown
|
// Cost-centre tint for moving + parked; offline goes solid grey
|
||||||
// via opacity (moving=1, parked=0.7, offline=0.35) so colour stays
|
// (no cost-centre signal worth showing when we haven't heard from
|
||||||
// a stable identity cue and the filter dropdown can double as a
|
// the device). Opacity differentiates moving vs parked.
|
||||||
// colour legend.
|
'circle-color': [
|
||||||
'circle-color': ['coalesce', ['get', 'cost_centre_color'], '#94a3b8'],
|
'case',
|
||||||
|
['==', ['get', 'operational_state'], 'offline'], '#9ca3af',
|
||||||
|
['coalesce', ['get', 'cost_centre_color'], '#94a3b8'],
|
||||||
|
],
|
||||||
'circle-opacity': [
|
'circle-opacity': [
|
||||||
'case',
|
'case',
|
||||||
['==', ['get', 'operational_state'], 'moving'], 1.0,
|
['==', ['get', 'operational_state'], 'moving'], 1.0,
|
||||||
['==', ['get', 'operational_state'], 'parked'], 0.7,
|
['==', ['get', 'operational_state'], 'parked'], 0.75,
|
||||||
0.35,
|
0.55,
|
||||||
],
|
],
|
||||||
'circle-stroke-color': '#0b1220',
|
'circle-stroke-color': '#0b1220',
|
||||||
'circle-stroke-width': [
|
'circle-stroke-width': [
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
/* ─────────── top dashboard band: tiles + slos + filters ─────────── */
|
/* ─────────── top dashboard band: tiles + slos + filters ─────────── */
|
||||||
.top-band {
|
.top-band {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(260px,auto) minmax(260px,1fr) minmax(280px,auto);
|
grid-template-columns: minmax(260px,1fr) minmax(280px,auto);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
|
|
@ -210,10 +210,6 @@
|
||||||
<div class="band-title">Fleet now</div>
|
<div class="band-title">Fleet now</div>
|
||||||
<div class="band-row" id="summary"></div>
|
<div class="band-row" id="summary"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="band-block">
|
|
||||||
<div class="band-title">SLOs</div>
|
|
||||||
<div class="band-row" id="slos" style="flex-direction:column; gap:4px;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="band-block">
|
<div class="band-block">
|
||||||
<div class="band-title">Filters</div>
|
<div class="band-title">Filters</div>
|
||||||
<div class="band-row" id="filters">
|
<div class="band-row" id="filters">
|
||||||
|
|
@ -258,7 +254,6 @@
|
||||||
|
|
||||||
const map = initMap('map');
|
const map = initMap('map');
|
||||||
const summaryEl = document.getElementById('summary');
|
const summaryEl = document.getElementById('summary');
|
||||||
const slosEl = document.getElementById('slos');
|
|
||||||
let currentFilters = {};
|
let currentFilters = {};
|
||||||
let activeSelection = { costCentres: [], cities: [] };
|
let activeSelection = { costCentres: [], cities: [] };
|
||||||
|
|
||||||
|
|
@ -266,7 +261,7 @@
|
||||||
try {
|
try {
|
||||||
const params = Object.keys(currentFilters).length ? { filters: currentFilters } : {};
|
const params = Object.keys(currentFilters).length ? { filters: currentFilters } : {};
|
||||||
const payload = await apiFetch('/api/views/live', { params });
|
const payload = await apiFetch('/api/views/live', { params });
|
||||||
renderView(map, payload, { summaryRoot: summaryEl, sloRoot: slosEl });
|
renderView(map, payload, { summaryRoot: summaryEl });
|
||||||
// Repopulate filter dropdowns from the latest features
|
// Repopulate filter dropdowns from the latest features
|
||||||
const features = (payload.geojson && payload.geojson.features) || [];
|
const features = (payload.geojson && payload.geojson.features) || [];
|
||||||
filters.updateOptions(features);
|
filters.updateOptions(features);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue