-- 07_analytics_views.sql -- Analytics views backing the Daily Operations dashboard. -- Each view wraps a query block from 01_BusinessAnalytics.md; the section -- is recorded in COMMENT ON VIEW so debuggers can trace any panel back to -- its spec with a single \d+ command. -- -- All views are regular (not materialised). v_driver_aggregates_daily is -- the only one with performance risk as position_history grows; convert -- it to a TimescaleDB continuous aggregate once the hypertable exceeds -- ~100k rows. BEGIN; -- ───────────────────────────────────────────────────────────────────────────── -- v_fleet_today -- One row per device with today's operational roll-up. -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_fleet_today AS WITH today AS ( SELECT (NOW() AT TIME ZONE 'Africa/Nairobi')::date AS d ), trips_today AS ( SELECT t.imei, COUNT(*) AS trips, SUM(t.distance_km) AS km, SUM(t.driving_time_s)::numeric / 3600 AS drive_hours, SUM(t.idle_time_s)::numeric / 3600 AS idle_hours, MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::time AS first_departure, MAX(COALESCE(t.end_time, t.start_time) AT TIME ZONE 'Africa/Nairobi')::time AS last_return FROM tracksolid.trips t CROSS JOIN today WHERE (t.start_time AT TIME ZONE 'Africa/Nairobi')::date = today.d GROUP BY t.imei ), alarms_today AS ( SELECT a.imei, COUNT(*) AS alarm_count FROM tracksolid.alarms a CROSS JOIN today WHERE (a.alarm_time AT TIME ZONE 'Africa/Nairobi')::date = today.d GROUP BY a.imei ) SELECT d.imei, d.driver_name, d.vehicle_number, d.vehicle_name, COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city, d.enabled_flag, COALESCE(tt.km, 0) AS km_today, COALESCE(tt.trips, 0) AS trips_today, COALESCE(tt.drive_hours, 0) AS drive_hours, COALESCE(tt.idle_hours, 0) AS idle_hours, tt.first_departure, tt.last_return, COALESCE(at.alarm_count, 0) AS alarms_today, lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix, lp.speed AS last_speed, (tt.imei IS NULL) AS did_not_move FROM tracksolid.devices d LEFT JOIN trips_today tt ON tt.imei = d.imei LEFT JOIN alarms_today at ON at.imei = d.imei LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei; COMMENT ON VIEW tracksolid.v_fleet_today IS '01_BusinessAnalytics.md §9 Fleet Readiness Scorecard. One row per device with today''s roll-up.'; -- ───────────────────────────────────────────────────────────────────────────── -- v_vehicles_not_moved_today -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_vehicles_not_moved_today AS SELECT d.imei, d.vehicle_name, d.vehicle_number, d.driver_name, COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city, lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_seen, lp.speed FROM tracksolid.devices d LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei LEFT JOIN tracksolid.trips t ON t.imei = d.imei AND (t.start_time AT TIME ZONE 'Africa/Nairobi')::date = (NOW() AT TIME ZONE 'Africa/Nairobi')::date WHERE d.enabled_flag = 1 AND t.imei IS NULL; COMMENT ON VIEW tracksolid.v_vehicles_not_moved_today IS '01_BusinessAnalytics.md §2.3. Enabled vehicles with zero trips today.'; -- ───────────────────────────────────────────────────────────────────────────── -- v_active_dispatch_map -- Every enabled device with current position, colour-coded status for geomap. -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_active_dispatch_map AS SELECT d.imei, d.vehicle_number, d.vehicle_name, d.driver_name, d.driver_phone, COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city, lp.lat, lp.lng, lp.speed, lp.direction, lp.acc_status, lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_fix, CASE WHEN lp.gps_time IS NULL THEN 'never_reported' WHEN lp.gps_time < NOW() - INTERVAL '10 minutes' THEN 'stale' WHEN COALESCE(lp.speed, 0) > 5 THEN 'moving' WHEN lp.acc_status = '1' THEN 'idle_ignition_on' ELSE 'parked' END AS status FROM tracksolid.devices d LEFT JOIN tracksolid.live_positions lp ON lp.imei = d.imei WHERE d.enabled_flag = 1; COMMENT ON VIEW tracksolid.v_active_dispatch_map IS '01_BusinessAnalytics.md §4.3 All Active Vehicles Map. Geomap source.'; -- ───────────────────────────────────────────────────────────────────────────── -- v_currently_idle -- Engine on, speed ~0, fresh fix. "Currently burning fuel while parked." -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_currently_idle AS SELECT d.imei, d.vehicle_number, d.driver_name, COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city, lp.lat, lp.lng, lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS since, EXTRACT(EPOCH FROM (NOW() - lp.gps_time))::int AS idle_seconds FROM tracksolid.devices d JOIN tracksolid.live_positions lp ON lp.imei = d.imei WHERE d.enabled_flag = 1 AND lp.acc_status = '1' AND COALESCE(lp.speed, 0) < 2 AND lp.gps_time > NOW() - INTERVAL '15 minutes'; COMMENT ON VIEW tracksolid.v_currently_idle IS '01_BusinessAnalytics.md §2.2 idle lens. Engine on, speed <2 km/h, fix in last 15m.'; -- ───────────────────────────────────────────────────────────────────────────── -- v_driver_aggregates_daily -- Per-driver per-day km + speeding + harsh-driving aggregates. -- TODO: Convert to TimescaleDB continuous aggregate once position_history > 100k rows. -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_driver_aggregates_daily AS WITH trips_agg AS ( SELECT imei, (start_time AT TIME ZONE 'Africa/Nairobi')::date AS day, SUM(distance_km) AS km, COUNT(*) AS trips FROM tracksolid.trips WHERE start_time > NOW() - INTERVAL '31 days' GROUP BY imei, (start_time AT TIME ZONE 'Africa/Nairobi')::date ), speeding AS ( SELECT imei, (gps_time AT TIME ZONE 'Africa/Nairobi')::date AS day, COUNT(*) FILTER (WHERE speed > 80) AS events_80, COUNT(*) FILTER (WHERE speed > 100) AS events_100, COUNT(*) FILTER (WHERE speed > 120) AS events_120 FROM tracksolid.position_history WHERE gps_time > NOW() - INTERVAL '31 days' AND speed IS NOT NULL GROUP BY imei, (gps_time AT TIME ZONE 'Africa/Nairobi')::date ), harsh_raw AS ( SELECT imei, gps_time, speed, LAG(speed) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_speed, LAG(gps_time) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_time FROM tracksolid.position_history WHERE source = 'track_list' AND gps_time > NOW() - INTERVAL '31 days' ), harsh AS ( SELECT imei, (gps_time AT TIME ZONE 'Africa/Nairobi')::date AS day, COUNT(*) AS harsh_events FROM harsh_raw WHERE ABS(speed - prev_speed) > 30 AND EXTRACT(EPOCH FROM (gps_time - prev_time)) BETWEEN 5 AND 60 GROUP BY imei, (gps_time AT TIME ZONE 'Africa/Nairobi')::date ) SELECT d.imei, d.driver_name, d.vehicle_number, COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city, t.day, COALESCE(t.km, 0) AS km, COALESCE(t.trips, 0) AS trips, COALESCE(s.events_80, 0) AS events_80, COALESCE(s.events_100, 0) AS events_100, COALESCE(s.events_120, 0) AS events_120, COALESCE(h.harsh_events, 0) AS harsh_events, ROUND(COALESCE(s.events_80, 0) / NULLIF(t.km, 0) * 100, 2) AS speeding_per_100km, ROUND(COALESCE(h.harsh_events, 0)/ NULLIF(t.km, 0) * 100, 2) AS harsh_per_100km FROM trips_agg t JOIN tracksolid.devices d ON d.imei = t.imei LEFT JOIN speeding s ON s.imei = t.imei AND s.day = t.day LEFT JOIN harsh h ON h.imei = t.imei AND h.day = t.day; COMMENT ON VIEW tracksolid.v_driver_aggregates_daily IS '01_BusinessAnalytics.md §3.1 (speeding) + §3.2 (harsh driving). Daily grain; panels window via $__timeFilter(day).'; -- ───────────────────────────────────────────────────────────────────────────── -- v_fleet_km_daily -- Fleet-wide daily distance, broken by assigned_city. -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_fleet_km_daily AS SELECT (t.start_time AT TIME ZONE 'Africa/Nairobi')::date AS day, COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city, SUM(t.distance_km) AS km, COUNT(DISTINCT t.imei) AS active_vehicles, COUNT(*) AS trips FROM tracksolid.trips t JOIN tracksolid.devices d ON d.imei = t.imei WHERE t.start_time > NOW() - INTERVAL '90 days' GROUP BY 1, 2; COMMENT ON VIEW tracksolid.v_fleet_km_daily IS '01_BusinessAnalytics.md §7 Panel 5 Distance Trend. City-cohort cut built in.'; -- ───────────────────────────────────────────────────────────────────────────── -- v_alarms_daily -- Daily alarm counts by alarm_name for the time-series panel. -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_alarms_daily AS SELECT (alarm_time AT TIME ZONE 'Africa/Nairobi')::date AS day, COALESCE(alarm_name, alarm_type, 'unknown') AS alarm_name, COUNT(*) AS alarm_count FROM tracksolid.alarms WHERE alarm_time > NOW() - INTERVAL '90 days' GROUP BY 1, 2; COMMENT ON VIEW tracksolid.v_alarms_daily IS '01_BusinessAnalytics.md §7 Panel 7 Alarm Frequency. Stacked-by-alarm_name time series.'; -- ───────────────────────────────────────────────────────────────────────────── -- v_utilisation_daily -- Per-vehicle per-day utilisation from dwh_gold.fact_daily_fleet_metrics. -- Empty until dwh_gold.refresh_daily_metrics nightly ETL is scheduled. -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_utilisation_daily AS SELECT f.day, d.imei, d.vehicle_number, d.driver_name, COALESCE(d.assigned_city, d.city, 'unassigned') AS assigned_city, f.total_distance_km, f.total_drive_hours, f.total_idle_hours, f.alarm_count, f.overspeed_count, ROUND( f.total_drive_hours / NULLIF(f.total_drive_hours + f.total_idle_hours, 0) * 100, 1 ) AS utilisation_pct FROM dwh_gold.fact_daily_fleet_metrics f JOIN dwh_gold.dim_vehicles dv ON dv.vehicle_key = f.vehicle_key JOIN tracksolid.devices d ON d.imei = dv.imei; COMMENT ON VIEW tracksolid.v_utilisation_daily IS '01_BusinessAnalytics.md §7 Panel 8 Utilisation Heatmap. Empty until nightly ETL runs.'; -- ───────────────────────────────────────────────────────────────────────────── -- v_sla_inflight -- In-flight field-service tickets with SLA timers, joined to dispatch_log. -- Empty until ops.tickets is populated from Zoho/Freshdesk or equivalent. -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE VIEW tracksolid.v_sla_inflight AS SELECT t.ticket_id, t.customer, t.priority, t.job_type, t.status, t.created_at, t.assigned_at, t.closed_at, t.assigned_imei, COALESCE(dl.driver_name, t.driver_name) AS driver_name, dl.first_movement_at, dl.on_site_at, dl.resolved_at, EXTRACT(EPOCH FROM (COALESCE(dl.first_movement_at, NOW()) - t.assigned_at)) / 60 AS dispatch_mins, EXTRACT(EPOCH FROM (COALESCE(dl.on_site_at, NOW()) - dl.first_movement_at)) / 60 AS enroute_mins, EXTRACT(EPOCH FROM (COALESCE(dl.resolved_at, NOW()) - dl.on_site_at)) / 60 AS onsite_mins, EXTRACT(EPOCH FROM (COALESCE(dl.resolved_at, NOW()) - t.created_at)) / 60 AS resolution_mins, CASE WHEN dl.resolved_at IS NOT NULL THEN 'resolved' WHEN dl.cancelled_at IS NOT NULL THEN 'cancelled' WHEN dl.on_site_at IS NOT NULL THEN 'on_site' WHEN dl.first_movement_at IS NOT NULL THEN 'en_route' WHEN t.assigned_at IS NOT NULL THEN 'dispatched' ELSE 'open' END AS ticket_stage FROM ops.tickets t LEFT JOIN tracksolid.dispatch_log dl ON dl.ticket_id = t.ticket_id WHERE t.status NOT IN ('resolved', 'cancelled') OR t.closed_at > NOW() - INTERVAL '24 hours'; COMMENT ON VIEW tracksolid.v_sla_inflight IS '01_BusinessAnalytics.md §4.5 Field-Service SLA Metrics. Open tickets + last 24h resolved.'; -- ───────────────────────────────────────────────────────────────────────────── -- Read access for Grafana -- ───────────────────────────────────────────────────────────────────────────── GRANT USAGE ON SCHEMA ops TO grafana_ro; GRANT SELECT ON tracksolid.v_fleet_today TO grafana_ro; GRANT SELECT ON tracksolid.v_vehicles_not_moved_today TO grafana_ro; GRANT SELECT ON tracksolid.v_active_dispatch_map TO grafana_ro; GRANT SELECT ON tracksolid.v_currently_idle TO grafana_ro; GRANT SELECT ON tracksolid.v_driver_aggregates_daily TO grafana_ro; GRANT SELECT ON tracksolid.v_fleet_km_daily TO grafana_ro; GRANT SELECT ON tracksolid.v_alarms_daily TO grafana_ro; GRANT SELECT ON tracksolid.v_utilisation_daily TO grafana_ro; GRANT SELECT ON tracksolid.v_sla_inflight TO grafana_ro; COMMIT;