diff --git a/migrations/15_map_exclude_cost_centres.sql b/migrations/15_map_exclude_cost_centres.sql new file mode 100644 index 0000000..3f138eb --- /dev/null +++ b/migrations/15_map_exclude_cost_centres.sql @@ -0,0 +1,105 @@ +-- 15_map_exclude_cost_centres.sql +-- Hide non-operational vehicles from the LIVE tracking map (FleetNow + liveposition SPA). +-- +-- A small, ops-editable config table lists the cost centres to exclude. reporting.v_live_positions +-- (the base view behind reporting.fn_live_positions, which dashboard_api serves) filters out any +-- plate whose device(s) carry an excluded cost centre. Editing the table changes the map on the +-- next query — no code change, no redeploy. +-- +-- Scope: LIVE map only. Trip history (reporting.v_trips materialised view) is deliberately NOT +-- touched. Initial exclusions: personal + management (staff/personal cars) and mtn (the MTN +-- contract / Uganda-Kampala fleet, outside Kenyan ops). +-- +-- The v_live_positions body below is reproduced verbatim from the live prod definition +-- (== migrations/11_reporting_schema.sql) with a single added filter in the primary_device CTE. +-- Safe to re-apply (CREATE TABLE IF NOT EXISTS / INSERT ON CONFLICT / CREATE OR REPLACE VIEW). + +SET search_path = tracksolid, reporting, public; + +-- ── exclusion config (data-driven, editable without a migration) ────────────── +CREATE TABLE IF NOT EXISTS reporting.map_excluded_cost_centres ( + cost_centre text PRIMARY KEY, -- compared case-insensitively (store lowercase) + note text, + added_at timestamptz NOT NULL DEFAULT now() +); + +INSERT INTO reporting.map_excluded_cost_centres (cost_centre, note) VALUES + ('personal', 'staff/personal vehicles — not operational fleet'), + ('management', 'management vehicles — not operational fleet'), + ('mtn', 'MTN contract / Uganda (Kampala) — outside Kenyan ops') +ON CONFLICT (cost_centre) DO NOTHING; + +-- ── v_live_positions: same definition + exclusion filter ────────────────────── +CREATE OR REPLACE VIEW reporting.v_live_positions AS + WITH primary_device AS ( + SELECT DISTINCT ON ((reporting.normalize_plate(d_1.vehicle_number))) reporting.normalize_plate(d_1.vehicle_number) AS vehicle_number, + d_1.imei AS primary_imei + FROM devices d_1 + LEFT JOIN live_positions lp_1 ON lp_1.imei = d_1.imei + WHERE d_1.vehicle_number IS NOT NULL AND d_1.enabled_flag = 1 + -- exclude plates whose device(s) carry a non-operational cost centre + AND reporting.normalize_plate(d_1.vehicle_number) NOT IN ( + SELECT reporting.normalize_plate(x.vehicle_number) + FROM devices x + WHERE x.vehicle_number IS NOT NULL + AND lower(trim(x.cost_centre)) IN ( + SELECT cost_centre FROM reporting.map_excluded_cost_centres) + ) + ORDER BY (reporting.normalize_plate(d_1.vehicle_number)), ( + CASE + WHEN (d_1.mc_type = ANY (ARRAY['GT06E'::text, 'X3'::text, 'AT4'::text])) AND lp_1.gps_time >= (now() - '24:00:00'::interval) THEN 0 + ELSE 1 + END), lp_1.gps_time DESC NULLS LAST, ( + CASE d_1.mc_type + WHEN 'GT06E'::text THEN 1 + WHEN 'X3'::text THEN 2 + WHEN 'AT4'::text THEN 3 + WHEN 'JC400P'::text THEN 4 + ELSE 5 + END), d_1.activation_time, d_1.imei + ) + SELECT lp.imei, + pd.vehicle_number, + d.driver_name AS assigned_driver, + d.cost_centre, + d.assigned_city, + d.vehicle_category, + d.vehicle_models, + d.mc_type, + CASE d.mc_type + WHEN 'GT06E'::text THEN 'tracker'::text + WHEN 'X3'::text THEN 'tracker'::text + WHEN 'AT4'::text THEN 'tracker'::text + WHEN 'JC400P'::text THEN 'camera'::text + ELSE 'other'::text + END AS device_kind, + lp.lat, + lp.lng, + lp.speed, + lp.direction, + lp.acc_status, + lp.device_status, + lp.gps_signal, + lp.gps_num, + lp.current_mileage, + lp.loc_desc, + lp.gps_time, + lp.updated_at, + (lp.gps_time AT TIME ZONE 'Africa/Nairobi'::text) AS gps_time_eat, + (lp.updated_at AT TIME ZONE 'Africa/Nairobi'::text) AS updated_at_eat, + round(EXTRACT(epoch FROM now() - lp.gps_time) / 3600::numeric, 2) AS source_age_hours + FROM live_positions lp + JOIN primary_device pd ON pd.primary_imei = lp.imei + JOIN devices d ON d.imei = lp.imei; + +COMMENT ON TABLE reporting.map_excluded_cost_centres IS + 'Cost centres hidden from the live map (reporting.v_live_positions). Edit to hide/restore; ' + 'effective on next query. Seeded: personal, management, mtn. See migration 15.'; + +-- ── grants (guarded: roles may not exist on a fresh DB) ─────────────────────── +DO $grants$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN + GRANT SELECT ON reporting.map_excluded_cost_centres TO grafana_ro; + END IF; +END $grants$; diff --git a/run_migrations.py b/run_migrations.py index a2a093a..a56b900 100644 --- a/run_migrations.py +++ b/run_migrations.py @@ -38,6 +38,7 @@ MIGRATIONS = [ "12_drop_ops.sql", # purge dormant ops schema + dispatch_log + v_sla_inflight "13_drop_dwh_gold.sql", # purge dormant dwh_gold schema + v_utilisation_daily "14_fleet_segment_and_vehicles_view.sql", # reporting.fn_fleet_segment + reporting.v_vehicles roster + "15_map_exclude_cost_centres.sql", # hide personal/management/mtn vehicles from the live map ] # ── Tables that must exist before the service is allowed to start ─────────────