tracksolid_timescale_grafan.../07_analytics_views.sql

349 lines
17 KiB
MySQL
Raw Permalink Normal View History

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