UI tweaks + city case fix
Some checks are pending
build / lint-test (push) Waiting to run
build / build-push (push) Blocked by required conditions

- 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:
kianiadee 2026-05-27 22:07:03 +03:00
parent 20958c0293
commit de34103f18
3 changed files with 28 additions and 14 deletions

View 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.

View file

@ -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': [

View file

@ -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);