diff --git a/db/migrations/006_trips_viz_view_v2.sql b/db/migrations/006_trips_viz_view_v2.sql new file mode 100644 index 0000000..205502a --- /dev/null +++ b/db/migrations/006_trips_viz_view_v2.sql @@ -0,0 +1,41 @@ +-- 006_trips_viz_view_v2.sql +-- Extend trips_viz_v1 with the enrichment columns added in upstream +-- migration 09 (tracksolid.trips: route_geom, start_address, end_address, +-- vehicle_plate, waypoints_count) plus webhook-supplied trip metrics from +-- migration 03 (idle_time_s, driving_time_s, fuel_consumed_l), surfaced via +-- tracksolid.v_trips_enriched (which adds trip_date_eat + daily_seq). +-- +-- Columns are appended only — CREATE OR REPLACE VIEW is safe (no column +-- removed, no type changed). Existing consumers see the same first ten +-- columns in the same positions. + +CREATE OR REPLACE VIEW public.trips_viz_v1 AS +SELECT + t.id AS trip_id, + t.imei, + d.vehicle_name, + d.vehicle_number, + COALESCE(NULLIF(TRIM(d.cost_centre), ''), 'Unassigned') AS cost_centre, + t.start_time, + t.end_time, + t.distance_km, + t.avg_speed_kmh, + t.max_speed_kmh, + -- ── Enrichment columns (upstream migration 09) ───────────────────────── + COALESCE(t.vehicle_plate, d.vehicle_number) AS vehicle_plate, + t.start_address, + t.end_address, + t.waypoints_count, + -- ── Webhook-supplied trip metrics (upstream migration 03) ────────────── + t.driving_time_s, + t.idle_time_s, + t.fuel_consumed_l, + -- ── Stable per-day trip ordinal (upstream migration 09) ──────────────── + t.trip_date_eat, + t.daily_seq +FROM tracksolid.v_trips_enriched t +JOIN tracksolid.devices d ON d.imei = t.imei +WHERE t.start_time IS NOT NULL + AND t.end_time IS NOT NULL; + +GRANT SELECT ON public.trips_viz_v1 TO viz_anon; diff --git a/db/migrations/007_trips_for_day_v2.sql b/db/migrations/007_trips_for_day_v2.sql new file mode 100644 index 0000000..1b4915c --- /dev/null +++ b/db/migrations/007_trips_for_day_v2.sql @@ -0,0 +1,82 @@ +-- 007_trips_for_day_v2.sql +-- Replaces 003_trips_for_day_rpc.sql to surface the enriched trip columns +-- exposed by 006_trips_viz_view_v2.sql. +-- +-- Path/timestamps logic is unchanged: position_history is still scanned per +-- trip to build the LineString and per-vertex timestamps_rel needed by +-- deck.gl TripsLayer animation. (route_geom alone can't replace this — it +-- carries no per-vertex time.) Win is metadata: vehicle_plate, addresses, +-- daily_seq, drive/idle/fuel — all returned in one round-trip. +-- +-- DROP + CREATE because the return type signature changes; CREATE OR REPLACE +-- FUNCTION cannot alter return columns. + +DROP FUNCTION IF EXISTS public.trips_for_day(date, text[]); + +CREATE FUNCTION public.trips_for_day( + p_date date, + p_cost_centres text[] DEFAULT NULL +) RETURNS TABLE ( + trip_id bigint, + imei text, + vehicle_name text, + vehicle_number text, + cost_centre text, + start_time timestamptz, + end_time timestamptz, + distance_km numeric, + vehicle_plate text, + start_address text, + end_address text, + driving_time_s integer, + idle_time_s integer, + fuel_consumed_l numeric, + daily_seq bigint, + path_geojson json, + timestamps_rel int[] +) +LANGUAGE sql STABLE +SECURITY DEFINER +SET search_path = public, tracksolid +AS $$ + WITH day_trips AS ( + SELECT v.* + FROM public.trips_viz_v1 v + WHERE v.trip_date_eat = p_date + AND (p_cost_centres IS NULL OR v.cost_centre = ANY(p_cost_centres)) + ) + SELECT + dt.trip_id, + dt.imei, + dt.vehicle_name, + dt.vehicle_number, + dt.cost_centre, + dt.start_time, + dt.end_time, + dt.distance_km, + dt.vehicle_plate, + dt.start_address, + dt.end_address, + dt.driving_time_s, + dt.idle_time_s, + dt.fuel_consumed_l, + dt.daily_seq, + ST_AsGeoJSON(ST_MakeLine(ph.geom ORDER BY ph.gps_time))::json AS path_geojson, + array_agg( + EXTRACT(EPOCH FROM ph.gps_time - dt.start_time)::int + ORDER BY ph.gps_time + ) AS timestamps_rel + FROM day_trips dt + JOIN tracksolid.position_history ph + ON ph.imei = dt.imei + AND ph.gps_time BETWEEN dt.start_time AND dt.end_time + AND ph.geom IS NOT NULL + GROUP BY dt.trip_id, dt.imei, dt.vehicle_name, dt.vehicle_number, + dt.cost_centre, dt.start_time, dt.end_time, dt.distance_km, + dt.vehicle_plate, dt.start_address, dt.end_address, + dt.driving_time_s, dt.idle_time_s, dt.fuel_consumed_l, + dt.daily_seq + HAVING count(ph.geom) >= 2; +$$; + +GRANT EXECUTE ON FUNCTION public.trips_for_day(date, text[]) TO viz_anon; diff --git a/db/migrations/008_trips_for_range_v2.sql b/db/migrations/008_trips_for_range_v2.sql new file mode 100644 index 0000000..08b018c --- /dev/null +++ b/db/migrations/008_trips_for_range_v2.sql @@ -0,0 +1,84 @@ +-- 008_trips_for_range_v2.sql +-- Multi-day variant of 007_trips_for_day_v2. Same enriched return shape; +-- 14-day cap retained. + +DROP FUNCTION IF EXISTS public.trips_for_range(date, date, text[]); + +CREATE FUNCTION public.trips_for_range( + p_start date, + p_end date, + p_cost_centres text[] DEFAULT NULL +) RETURNS TABLE ( + trip_id bigint, + imei text, + vehicle_name text, + vehicle_number text, + cost_centre text, + start_time timestamptz, + end_time timestamptz, + distance_km numeric, + vehicle_plate text, + start_address text, + end_address text, + driving_time_s integer, + idle_time_s integer, + fuel_consumed_l numeric, + daily_seq bigint, + path_geojson json, + timestamps_rel int[] +) +LANGUAGE plpgsql STABLE +SECURITY DEFINER +SET search_path = public, tracksolid +AS $$ +BEGIN + IF p_start IS NULL OR p_end IS NULL OR p_end < p_start THEN + RAISE EXCEPTION 'Invalid date range: p_start=%, p_end=%', p_start, p_end; + END IF; + IF (p_end - p_start) > 13 THEN + RAISE EXCEPTION 'Range too large: max 14 days, got %', (p_end - p_start + 1); + END IF; + + RETURN QUERY + WITH range_trips AS ( + SELECT v.* + FROM public.trips_viz_v1 v + WHERE v.trip_date_eat BETWEEN p_start AND p_end + AND (p_cost_centres IS NULL OR v.cost_centre = ANY(p_cost_centres)) + ) + SELECT + rt.trip_id, + rt.imei, + rt.vehicle_name, + rt.vehicle_number, + rt.cost_centre, + rt.start_time, + rt.end_time, + rt.distance_km, + rt.vehicle_plate, + rt.start_address, + rt.end_address, + rt.driving_time_s, + rt.idle_time_s, + rt.fuel_consumed_l, + rt.daily_seq, + ST_AsGeoJSON(ST_MakeLine(ph.geom ORDER BY ph.gps_time))::json AS path_geojson, + array_agg( + EXTRACT(EPOCH FROM ph.gps_time - rt.start_time)::int + ORDER BY ph.gps_time + ) AS timestamps_rel + FROM range_trips rt + JOIN tracksolid.position_history ph + ON ph.imei = rt.imei + AND ph.gps_time BETWEEN rt.start_time AND rt.end_time + AND ph.geom IS NOT NULL + GROUP BY rt.trip_id, rt.imei, rt.vehicle_name, rt.vehicle_number, + rt.cost_centre, rt.start_time, rt.end_time, rt.distance_km, + rt.vehicle_plate, rt.start_address, rt.end_address, + rt.driving_time_s, rt.idle_time_s, rt.fuel_consumed_l, + rt.daily_seq + HAVING count(ph.geom) >= 2; +END; +$$; + +GRANT EXECUTE ON FUNCTION public.trips_for_range(date, date, text[]) TO viz_anon;