diff --git a/01_BusinessAnalytics.md b/01_BusinessAnalytics.md index 3b0b846..28138be 100644 --- a/01_BusinessAnalytics.md +++ b/01_BusinessAnalytics.md @@ -6,14 +6,36 @@ ## Table of Contents +0. [How to Use This Document](#0-how-to-use-this-document) 1. [Data Foundation Summary](#1-data-foundation-summary) 2. [Fleet Utilisation](#2-fleet-utilisation) 3. [Driver Behaviour](#3-driver-behaviour) -4. [Real-Time Dispatch — Nearest Vehicle to Job](#4-real-time-dispatch--nearest-vehicle-to-job) +4. [Real-Time Dispatch & Field-Service SLAs](#4-real-time-dispatch--field-service-slas) 5. [Distance per Driver per Day](#5-distance-per-driver-per-day) 6. [Business Questions Now Answerable](#6-business-questions-now-answerable) 7. [Grafana Dashboard Blueprint](#7-grafana-dashboard-blueprint) 8. [What Unlocks the Remaining 30%](#8-what-unlocks-the-remaining-30) +9. [Fleet Readiness Scorecard](#9-fleet-readiness-scorecard) +10. [Service-Interval Forecaster](#10-service-interval-forecaster) + +--- + +## 0. How to Use This Document + +Every query in this document is tagged by intended consumption cadence. Build Grafana panels, alert rules, and scheduled reports against the tag — not the SQL text — so that moving a metric between dashboard and alert is a one-line change. + +| Tag | Meaning | Typical cadence | Owner | +|---|---|---|---| +| `[DASHBOARD]` | Live or near-live panel | Refresh 30 s – 5 min | Ops / Dispatch | +| `[ALERT]` | Trigger a page or ticket | Evaluate 1 – 15 min | On-call / Fleet Manager | +| `[MONTHLY]` | Management / exec reporting | Run on 1st of month | Finance / Ops Lead | +| `[AD-HOC]` | Investigation, audit, one-off | On demand | Analyst / Ops | + +**Reading a query block**: each section lead-in states the tag(s). If a query has no tag it is reference material (schema, benchmark tables, appendix). + +**Thresholds are starting points, not gospel**. Every red/amber/green band in this document must be re-calibrated against your own 30-day distribution once data matures. See [Appendix B — Threshold Calibration Guide](#appendix-b--threshold-calibration-guide). + +**City-cohort cuts**. Fireside operates in Nairobi, Mombasa, and Kampala. Traffic, fuel prices, and shift norms differ materially between them. Any fleet-level metric should be sliceable by `devices.assigned_city` once that column is populated (see §3.7). --- @@ -119,6 +141,8 @@ WHERE day >= DATE_TRUNC('month', CURRENT_DATE); ### 2.3 Vehicles That Did Not Move Today +`[DASHBOARD]` `[ALERT]` — alert if a vehicle has not moved for ≥ 2 consecutive working days. + ```sql SELECT d.imei, @@ -139,6 +163,73 @@ ORDER BY d.imei; --- +### 2.4 Cost-per-Ticket and Cost-per-Km + +`[MONTHLY]` — the single most actionable finance metric: *what does one completed field-service job actually cost in fuel?* Pairs the trip table with the ticketing system (replace `ops.tickets` with the actual source — Zoho Desk, Freshdesk, or the Fireside job-management export). + +Requires `devices.fuel_100km` (see §8 Step 2). Diesel price is parameterised so this query works across Nairobi / Mombasa / Kampala without editing. + +```sql +WITH fuel_rates AS ( + SELECT + 'NBO'::TEXT AS city, 180.0::NUMERIC AS price_per_litre -- Nairobi diesel KES + UNION ALL SELECT 'MBA', 175.0 + UNION ALL SELECT 'KLA', 5200.0 -- Kampala UGX → convert in BI layer +), +daily_cost AS ( + SELECT + t.imei, + DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS working_day, + SUM(t.distance_km) AS km, + SUM(t.distance_km) * (d.fuel_100km / 100.0) AS litres, + SUM(t.distance_km) * (d.fuel_100km / 100.0) * f.price_per_litre AS fuel_cost + FROM tracksolid.trips t + JOIN tracksolid.devices d ON d.imei = t.imei + LEFT JOIN fuel_rates f ON f.city = d.assigned_city + WHERE t.start_time >= DATE_TRUNC('month', CURRENT_DATE) + AND t.end_time IS NOT NULL + GROUP BY t.imei, working_day, d.fuel_100km, f.price_per_litre +), +tickets AS ( + SELECT + assigned_imei AS imei, + DATE(closed_at AT TIME ZONE 'Africa/Nairobi') AS working_day, + COUNT(*) FILTER (WHERE status = 'resolved') AS tickets_closed + FROM ops.tickets + WHERE closed_at >= DATE_TRUNC('month', CURRENT_DATE) + GROUP BY assigned_imei, working_day +) +SELECT + dc.imei, + d.driver_name, + d.vehicle_number, + SUM(dc.km) AS km_month, + ROUND(SUM(dc.fuel_cost), 0) AS fuel_cost_kes_month, + COALESCE(SUM(tk.tickets_closed), 0) AS tickets_closed, + ROUND(SUM(dc.fuel_cost) / NULLIF(SUM(tk.tickets_closed), 0), 0) AS cost_per_ticket_kes, + ROUND(SUM(dc.fuel_cost) / NULLIF(SUM(dc.km), 0), 2) AS cost_per_km_kes +FROM daily_cost dc +JOIN tracksolid.devices d ON d.imei = dc.imei +LEFT JOIN tickets tk + ON tk.imei = dc.imei + AND tk.working_day = dc.working_day +GROUP BY dc.imei, d.driver_name, d.vehicle_number +ORDER BY cost_per_ticket_kes DESC NULLS LAST; +``` + +**Interpretation bands** — driver-level cost-per-ticket (van fleet, Nairobi baseline): + +| KES / ticket | Signal | Typical cause | +|---|---|---| +| < 400 | Efficient | Dense route, minimal backtracking | +| 400 – 900 | Normal | Mixed urban route | +| 900 – 1500 | Review | Scattered geography or low ticket throughput | +| > 1500 | Investigate | Idle time, off-route driving, or single-ticket days | + +> **Dependency:** requires ticket data joined on IMEI or driver ID. If only driver-level data is available, swap `assigned_imei` for a driver→imei lookup. + +--- + ## 3. Driver Behaviour ### 3.1 Speeding @@ -421,7 +512,169 @@ ORDER BY t.imei, week_start; --- -## 4. Real-Time Dispatch — Nearest Vehicle to Job +### 3.6 Alarm-While-Parked — Tamper and Theft Signal + +`[ALERT]` — an alarm event on a vehicle that has been stationary for > 10 minutes is qualitatively different from an alarm mid-drive. Stationary alarms are the strongest signal for tamper, battery disconnect, unauthorised ignition, or geofence breach by a *parked* vehicle being loaded. Fires highest-priority page. + +```sql +SELECT + a.imei, + d.driver_name, + d.vehicle_number, + a.alarm_name, + a.alarm_time AT TIME ZONE 'Africa/Nairobi' AS event_time, + ROUND( + EXTRACT(EPOCH FROM (a.alarm_time - p.end_time)) / 60.0, 1 + ) AS minutes_parked_before_alarm, + p.address AS park_location, + a.lat, a.lng +FROM tracksolid.alarms a +JOIN tracksolid.devices d ON d.imei = a.imei +JOIN LATERAL ( + SELECT end_time, address + FROM tracksolid.parking_events p + WHERE p.imei = a.imei + AND p.start_time <= a.alarm_time + AND (p.end_time IS NULL OR p.end_time >= a.alarm_time) + ORDER BY p.start_time DESC + LIMIT 1 +) p ON TRUE +WHERE a.alarm_time > NOW() - INTERVAL '24 hours' + AND a.alarm_type IN ('vibration', 'power_cut', 'geofence_enter', 'geofence_exit', 'unauthorized_ignition') +ORDER BY a.alarm_time DESC; +``` + +> **Page rule:** any row where `alarm_type IN ('power_cut', 'unauthorized_ignition')` AND vehicle has been parked > 10 min pages the on-call operations lead immediately. Other stationary alarms ticket to the fleet manager for next-day review. + +--- + +### 3.7 Geographic Drift — Vehicles Operating Outside Assigned City + +`[MONTHLY]` `[ALERT]` — detects vehicles running outside their assigned operating territory. Protects against unauthorised inter-city trips, fuel tourism, and route fraud. + +**Prerequisite** — add an `assigned_city` column to the devices table: + +```sql +ALTER TABLE tracksolid.devices ADD COLUMN IF NOT EXISTS assigned_city TEXT; +-- Example back-fill: +UPDATE tracksolid.devices SET assigned_city = 'NBO' WHERE imei IN (...); +UPDATE tracksolid.devices SET assigned_city = 'MBA' WHERE imei IN (...); +UPDATE tracksolid.devices SET assigned_city = 'KLA' WHERE imei IN (...); +``` + +City bounding boxes (approximate; widen as needed for suburban coverage): + +| City | Code | min lat | max lat | min lng | max lng | +|---|---|---|---|---|---| +| Nairobi metro | NBO | -1.45 | -1.15 | 36.65 | 37.05 | +| Mombasa metro | MBA | -4.15 | -3.90 | 39.55 | 39.80 | +| Kampala metro | KLA | 0.20 | 0.45 | 32.50 | 32.75 | + +```sql +WITH city_box AS ( + SELECT * FROM (VALUES + ('NBO', -1.45, -1.15, 36.65, 37.05), + ('MBA', -4.15, -3.90, 39.55, 39.80), + ('KLA', 0.20, 0.45, 32.50, 32.75) + ) AS c(code, min_lat, max_lat, min_lng, max_lng) +), +out_of_zone AS ( + SELECT + ph.imei, + d.assigned_city, + DATE(ph.gps_time AT TIME ZONE 'Africa/Nairobi') AS day, + COUNT(*) AS fixes_outside_zone + FROM tracksolid.position_history ph + JOIN tracksolid.devices d ON d.imei = ph.imei + JOIN city_box c ON c.code = d.assigned_city + WHERE ph.gps_time > NOW() - INTERVAL '30 days' + AND ( + ph.lat < c.min_lat OR ph.lat > c.max_lat + OR ph.lng < c.min_lng OR ph.lng > c.max_lng + ) + GROUP BY ph.imei, d.assigned_city, day +) +SELECT + o.imei, + d.driver_name, + d.vehicle_number, + o.assigned_city, + o.day, + o.fixes_outside_zone +FROM out_of_zone o +JOIN tracksolid.devices d ON d.imei = o.imei +WHERE o.fixes_outside_zone > 20 -- ~10 minutes of continuous out-of-zone driving +ORDER BY o.day DESC, o.fixes_outside_zone DESC; +``` + +> **Alert threshold:** > 50 fixes outside zone in a single day = escalate. Expected legitimate cases: cross-city service trips, driver taking vehicle home across a city boundary (policy decision). + +--- + +### 3.8 Odometer Divergence — Tracker vs Physical Reading + +`[MONTHLY]` — compares cumulative distance recorded by the tracker against the vehicle's physical odometer (captured at service or fuel card events). Divergence > 10% suggests sensor drift, GPS gaps, or unauthorised driving with the tracker disabled. + +```sql +WITH tracker_km AS ( + SELECT + imei, + SUM(distance_km) AS trips_km_30d + FROM tracksolid.trips + WHERE start_time > NOW() - INTERVAL '30 days' + AND end_time IS NOT NULL + GROUP BY imei +), +physical_readings AS ( + -- Replace with actual odometer log source (service records, fuel card, manual entry) + SELECT + imei, + reading_km, + reading_date, + LAG(reading_km) OVER (PARTITION BY imei ORDER BY reading_date) AS prev_reading_km, + LAG(reading_date) OVER (PARTITION BY imei ORDER BY reading_date) AS prev_reading_date + FROM ops.odometer_readings + WHERE reading_date > NOW() - INTERVAL '60 days' +), +physical_delta AS ( + SELECT + imei, + reading_km - prev_reading_km AS physical_km, + EXTRACT(DAY FROM (reading_date - prev_reading_date)) AS period_days + FROM physical_readings + WHERE prev_reading_km IS NOT NULL + AND period_days BETWEEN 20 AND 40 +) +SELECT + p.imei, + d.driver_name, + d.vehicle_number, + ROUND(p.physical_km, 0) AS odometer_km_period, + ROUND(tk.trips_km_30d, 0) AS tracker_km_30d, + ROUND( + (p.physical_km - tk.trips_km_30d) / NULLIF(p.physical_km, 0) * 100, + 1 + ) AS divergence_pct +FROM physical_delta p +JOIN tracker_km tk ON tk.imei = p.imei +JOIN tracksolid.devices d ON d.imei = p.imei +WHERE ABS( + (p.physical_km - tk.trips_km_30d) / NULLIF(p.physical_km, 0) +) > 0.10 +ORDER BY ABS(p.physical_km - tk.trips_km_30d) DESC; +``` + +**Interpretation:** + +| Divergence | Likely cause | Action | +|---|---|---| +| Tracker < physical (> 10%) | GPS outage, tracker powered off, engine driven with no fix | Audit device uptime; inspect for tamper | +| Tracker > physical (> 10%) | Duplicate trip records, distance-correction bug | Run migration check; review `trips.distance_km` distribution | +| Divergence growing month-over-month | Sensor drift, antenna degradation | Replace device or antenna | + +--- + +## 4. Real-Time Dispatch & Field-Service SLAs ### 4.1 Find the 5 Closest Available Vehicles @@ -512,6 +765,148 @@ ORDER BY lp.imei; --- +### 4.4 Dispatch Log Schema + +A persistent record of every dispatch decision, needed for every SLA and cost metric that follows. Create once: + +```sql +CREATE TABLE IF NOT EXISTS tracksolid.dispatch_log ( + dispatch_id BIGSERIAL PRIMARY KEY, + ticket_id TEXT NOT NULL, + imei TEXT NOT NULL REFERENCES tracksolid.devices(imei), + driver_name TEXT, + job_lat DOUBLE PRECISION NOT NULL, + job_lng DOUBLE PRECISION NOT NULL, + job_geom GEOMETRY(POINT, 4326), + assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + first_movement_at TIMESTAMPTZ, -- populated when vehicle leaves depot + on_site_at TIMESTAMPTZ, -- vehicle enters 150 m radius of job + resolved_at TIMESTAMPTZ, -- ticket closed in ops system + cancelled_at TIMESTAMPTZ, + distance_km NUMERIC(8, 2), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_dispatch_log_ticket ON tracksolid.dispatch_log(ticket_id); +CREATE INDEX IF NOT EXISTS idx_dispatch_log_imei_assigned + ON tracksolid.dispatch_log(imei, assigned_at DESC); +CREATE INDEX IF NOT EXISTS idx_dispatch_log_assigned_at + ON tracksolid.dispatch_log(assigned_at DESC); +``` + +**Population plan:** n8n or the ops integration layer writes one row per dispatch at assignment. A nightly job back-fills `first_movement_at` / `on_site_at` by joining `trips` and `live_positions` against `job_geom`. + +--- + +### 4.5 Field-Service SLA Metrics + +`[DASHBOARD]` `[ALERT]` `[MONTHLY]` — the operational heartbeat of a field-services business. Four timings per ticket, each a discrete SLA with its own band. + +``` +ticket_created ─► assigned ─► first_movement ─► on_site ─► resolved + (dispatch (depot depart (vehicle (job done) + latency) latency) arrived) +``` + +**(a) Dispatch latency** — from ticket creation to vehicle assignment: + +```sql +SELECT + t.ticket_id, + EXTRACT(EPOCH FROM (dl.assigned_at - t.created_at)) / 60 AS dispatch_latency_min +FROM ops.tickets t +JOIN tracksolid.dispatch_log dl ON dl.ticket_id = t.ticket_id +WHERE t.created_at > NOW() - INTERVAL '7 days'; +``` + +**(b) Dispatch-to-depart** — from assignment to vehicle actually leaving the depot: + +```sql +SELECT + dl.ticket_id, + dl.imei, + d.driver_name, + EXTRACT(EPOCH FROM (dl.first_movement_at - dl.assigned_at)) / 60 AS depart_delay_min +FROM tracksolid.dispatch_log dl +JOIN tracksolid.devices d ON d.imei = dl.imei +WHERE dl.assigned_at > NOW() - INTERVAL '7 days' + AND dl.first_movement_at IS NOT NULL +ORDER BY depart_delay_min DESC; +``` + +**(c) Time-to-site** — from assignment to arrival at the job location (vehicle within 150 m): + +```sql +SELECT + dl.ticket_id, + dl.imei, + ROUND(dl.distance_km, 1) AS distance_km, + EXTRACT(EPOCH FROM (dl.on_site_at - dl.assigned_at)) / 60 AS time_to_site_min, + ROUND( + dl.distance_km / + NULLIF(EXTRACT(EPOCH FROM (dl.on_site_at - dl.assigned_at)) / 3600, 0), + 1 + ) AS avg_transit_kmh +FROM tracksolid.dispatch_log dl +WHERE dl.assigned_at > NOW() - INTERVAL '7 days' + AND dl.on_site_at IS NOT NULL; +``` + +**(d) On-site to resolution** — wrench time at the job: + +```sql +SELECT + dl.ticket_id, + dl.imei, + EXTRACT(EPOCH FROM (dl.resolved_at - dl.on_site_at)) / 60 AS wrench_time_min +FROM tracksolid.dispatch_log dl +WHERE dl.on_site_at IS NOT NULL + AND dl.resolved_at IS NOT NULL + AND dl.assigned_at > NOW() - INTERVAL '30 days'; +``` + +**Monthly SLA attainment per driver:** + +```sql +SELECT + dl.imei, + d.driver_name, + COUNT(*) AS tickets, + ROUND(AVG( + EXTRACT(EPOCH FROM (dl.first_movement_at - dl.assigned_at)) + ) / 60, 1) AS avg_depart_min, + ROUND(AVG( + EXTRACT(EPOCH FROM (dl.on_site_at - dl.assigned_at)) + ) / 60, 1) AS avg_time_to_site_min, + ROUND(AVG( + EXTRACT(EPOCH FROM (dl.resolved_at - dl.on_site_at)) + ) / 60, 1) AS avg_wrench_min, + ROUND( + 100.0 * COUNT(*) FILTER ( + WHERE EXTRACT(EPOCH FROM (dl.on_site_at - dl.assigned_at)) / 60 <= 90 + ) / NULLIF(COUNT(*), 0), + 1 + ) AS pct_on_site_within_90min +FROM tracksolid.dispatch_log dl +JOIN tracksolid.devices d ON d.imei = dl.imei +WHERE dl.assigned_at >= DATE_TRUNC('month', CURRENT_DATE) + AND dl.on_site_at IS NOT NULL +GROUP BY dl.imei, d.driver_name +ORDER BY pct_on_site_within_90min DESC; +``` + +**Target bands** (baseline — recalibrate after 90 days of data): + +| SLA | Green | Amber | Red | +|---|---|---|---| +| Dispatch latency (ops → driver) | < 10 min | 10 – 25 min | > 25 min | +| Depart delay (assigned → moving) | < 15 min | 15 – 35 min | > 35 min | +| Time-to-site (assigned → on-site) | < 60 min | 60 – 120 min | > 120 min | +| Wrench time (on-site → resolved) | < 90 min | 90 – 180 min | > 180 min | +| % on-site within 90 min (monthly) | ≥ 85% | 70 – 85% | < 70% | + +--- + ## 5. Distance per Driver per Day ### 5.1 Today's Summary @@ -753,7 +1148,229 @@ docker compose up -d --build ingest_movement ingest_events webhook_receiver --- -## Appendix — Key Metric Thresholds Reference +## 9. Fleet Readiness Scorecard + +`[DASHBOARD]` `[MONTHLY]` — a single composite number per vehicle, useful as a morning briefing and a monthly fleet health report. Runs against only the tables you already have — no new DDL required — so this is the fastest concrete win in this document. + +Five sub-scores (0 – 100), averaged with weights: + +| Sub-score | Weight | Signal | +|---|---|---| +| **Freshness** | 25% | GPS fix age vs. a 5-minute target | +| **Coverage** | 20% | Active days in the last 7 | +| **Silence** | 15% | Tracker went dark > 30 min during working hours | +| **Alarm pressure** | 20% | Alarms per 100 km over 30 days | +| **Driver behaviour** | 20% | Aggression + speeding index | + +```sql +WITH freshness AS ( + SELECT + imei, + EXTRACT(EPOCH FROM (NOW() - gps_time)) / 60 AS minutes_since_fix + FROM tracksolid.live_positions +), +coverage AS ( + SELECT + imei, + COUNT(DISTINCT DATE(start_time AT TIME ZONE 'Africa/Nairobi')) AS days_active_7d + FROM tracksolid.trips + WHERE start_time > NOW() - INTERVAL '7 days' + GROUP BY imei +), +silence AS ( + -- Gaps > 30 min during 07:00 – 19:00 EAT in the last 7 days + SELECT + imei, + COUNT(*) AS silence_events_7d + FROM ( + SELECT + imei, + gps_time, + LAG(gps_time) OVER (PARTITION BY imei ORDER BY gps_time) AS prev_time + FROM tracksolid.position_history + WHERE gps_time > NOW() - INTERVAL '7 days' + AND EXTRACT(HOUR FROM gps_time AT TIME ZONE 'Africa/Nairobi') BETWEEN 7 AND 19 + ) gaps + WHERE EXTRACT(EPOCH FROM (gps_time - prev_time)) > 1800 + GROUP BY imei +), +alarm_pressure AS ( + SELECT + a.imei, + COUNT(*) AS alarms_30d, + SUM(t.distance_km) AS km_30d + FROM tracksolid.alarms a + LEFT JOIN tracksolid.trips t + ON t.imei = a.imei + AND t.start_time > NOW() - INTERVAL '30 days' + WHERE a.alarm_time > NOW() - INTERVAL '30 days' + GROUP BY a.imei +), +behaviour AS ( + SELECT + ph.imei, + COUNT(*) FILTER (WHERE ph.speed > 100) AS over_100, + COUNT(*) FILTER ( + WHERE ABS(ph.speed - LAG(ph.speed) OVER ( + PARTITION BY ph.imei ORDER BY ph.gps_time + )) > 30 + ) AS harsh_events + FROM tracksolid.position_history ph + WHERE ph.gps_time > NOW() - INTERVAL '30 days' + AND ph.source = 'track_list' + GROUP BY ph.imei +) +SELECT + d.imei, + d.driver_name, + d.vehicle_number, + ROUND( + GREATEST(0, 100 - COALESCE(f.minutes_since_fix, 999) / 5.0 * 20) + ) AS freshness_score, + ROUND( + LEAST(100, COALESCE(c.days_active_7d, 0) / 5.0 * 100) + ) AS coverage_score, + ROUND( + GREATEST(0, 100 - COALESCE(s.silence_events_7d, 0) * 10) + ) AS silence_score, + ROUND( + GREATEST(0, 100 - COALESCE( + ap.alarms_30d::NUMERIC / NULLIF(ap.km_30d, 0) * 100 * 20, 0 + )) + ) AS alarm_score, + ROUND( + GREATEST(0, 100 - COALESCE(b.over_100, 0) * 2 - COALESCE(b.harsh_events, 0) * 3) + ) AS behaviour_score, + ROUND( + GREATEST(0, 100 - COALESCE(f.minutes_since_fix, 999) / 5.0 * 20) * 0.25 + + LEAST(100, COALESCE(c.days_active_7d, 0) / 5.0 * 100) * 0.20 + + GREATEST(0, 100 - COALESCE(s.silence_events_7d, 0) * 10) * 0.15 + + GREATEST(0, 100 - COALESCE( + ap.alarms_30d::NUMERIC / NULLIF(ap.km_30d, 0) * 100 * 20, 0 + )) * 0.20 + + GREATEST(0, 100 - COALESCE(b.over_100, 0) * 2 - COALESCE(b.harsh_events, 0) * 3) * 0.20 + ) AS readiness_score +FROM tracksolid.devices d +LEFT JOIN freshness f ON f.imei = d.imei +LEFT JOIN coverage c ON c.imei = d.imei +LEFT JOIN silence s ON s.imei = d.imei +LEFT JOIN alarm_pressure ap ON ap.imei = d.imei +LEFT JOIN behaviour b ON b.imei = d.imei +WHERE d.enabled_flag = 1 +ORDER BY readiness_score ASC NULLS FIRST; +``` + +**Interpretation:** + +| Score | Band | Action | +|---|---|---| +| 85 – 100 | Green — ready | Dispatch freely | +| 60 – 84 | Amber — monitor | Review the lowest sub-score; fix trackers or coach driver | +| < 60 | Red — unreliable | Do not dispatch for priority jobs; service or replace | +| NULL | Silent | Vehicle never reported — investigate install / commission | + +The scorecard is also the cleanest Panel 2 replacement for the Grafana Fleet Status Summary. + +--- + +## 10. Service-Interval Forecaster + +`[MONTHLY]` `[ALERT]` — predicts when each vehicle will hit its next service interval (default 10,000 km), based on its trailing 30-day km rate. Lets ops pre-book workshop slots and avoid fleet-wide conflicts. + +Requires a service-log table (create once): + +```sql +CREATE TABLE IF NOT EXISTS ops.service_log ( + service_id BIGSERIAL PRIMARY KEY, + imei TEXT NOT NULL REFERENCES tracksolid.devices(imei), + service_date DATE NOT NULL, + odometer_km INTEGER NOT NULL, + service_type TEXT, -- 'scheduled', 'repair', 'tyre', etc. + cost_kes INTEGER, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_service_log_imei_date + ON ops.service_log(imei, service_date DESC); +``` + +**Forecaster query** — km until next service, projected service date: + +```sql +WITH last_service AS ( + SELECT DISTINCT ON (imei) + imei, + service_date, + odometer_km + FROM ops.service_log + WHERE service_type = 'scheduled' + ORDER BY imei, service_date DESC +), +current_odometer AS ( + SELECT imei, current_mileage_km + FROM tracksolid.devices +), +trailing_rate AS ( + SELECT + imei, + SUM(distance_km) / 30.0 AS km_per_day_30d + FROM tracksolid.trips + WHERE start_time > NOW() - INTERVAL '30 days' + AND end_time IS NOT NULL + GROUP BY imei +) +SELECT + d.imei, + d.driver_name, + d.vehicle_number, + ls.service_date AS last_service_date, + ls.odometer_km AS last_service_odo, + co.current_mileage_km AS current_odo, + (co.current_mileage_km - COALESCE(ls.odometer_km, 0)) AS km_since_service, + GREATEST( + 0, + 10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0)) + ) AS km_to_next_service, + ROUND(tr.km_per_day_30d, 1) AS km_per_day_30d, + CASE + WHEN tr.km_per_day_30d > 0 THEN + CURRENT_DATE + ( + GREATEST(0, 10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0))) + / tr.km_per_day_30d + )::INT + ELSE NULL + END AS projected_service_date +FROM tracksolid.devices d +LEFT JOIN last_service ls ON ls.imei = d.imei +LEFT JOIN current_odometer co ON co.imei = d.imei +LEFT JOIN trailing_rate tr ON tr.imei = d.imei +WHERE d.enabled_flag = 1 +ORDER BY projected_service_date NULLS LAST; +``` + +**Weekly booking view** — how many vehicles need service in each of the next 8 weeks: + +```sql +WITH forecast AS ( + -- (same CTE body as above; wrap as subquery or view `ops.vw_service_forecast`) + SELECT imei, projected_service_date + FROM ops.vw_service_forecast + WHERE projected_service_date IS NOT NULL +) +SELECT + DATE_TRUNC('week', projected_service_date)::DATE AS week_start, + COUNT(*) AS vehicles_due +FROM forecast +WHERE projected_service_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '8 weeks' +GROUP BY week_start +ORDER BY week_start; +``` + +> **Alert:** any vehicle with `km_to_next_service < (7 × km_per_day_30d)` fires an amber ticket to the fleet manager. Any vehicle already overdue (`km_to_next_service = 0`) fires red. + +--- + +## Appendix A — Key Metric Thresholds Reference | Metric | Green | Amber | Red | |---|---|---|---| @@ -765,8 +1382,56 @@ docker compose up -d --build ingest_movement ingest_events webhook_receiver | Days vehicle not used (per month) | 0–2 | 3–5 | > 5 | | GPS fix age (live_positions) | < 2 min | 2–10 min | > 10 min | | Alarm rate per vehicle per week | 0–2 | 3–7 | > 7 | +| Readiness score (§9) | ≥ 85 | 60–84 | < 60 | +| Cost per ticket (van, NBO baseline) | < 400 KES | 400–900 KES | > 900 KES | +| On-site within 90 min (§4.5) | ≥ 85% | 70–85% | < 70% | --- -*Document generated: 2026-04-10 · Stack: TimescaleDB 2.15 + PostGIS + Tracksolid Pro Open Platform API* +## Appendix B — Threshold Calibration Guide + +Every threshold in Appendix A is a **starting point**. They are drawn from general field-services norms and three Fireside incident reviews — not from Fireside's own distribution. After ~30 days of clean data, recalibrate each one against your own observed p50 / p90 / p99. + +**The principle:** green should catch ≥ 50% of vehicle-days, amber ≥ 30%, red ≤ 20%. If red is firing on more than 25% of the fleet every day, the alert is noise and will be ignored. + +**Calibration recipe** — run monthly for each threshold-backed metric: + +```sql +-- Example: utilisation % — recompute green/amber/red cut-points from the live distribution +WITH daily AS ( + SELECT + t.imei, + DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS day, + SUM(t.driving_time_s) / (10.0 * 3600) * 100 AS utilisation_pct + FROM tracksolid.trips t + WHERE t.start_time > NOW() - INTERVAL '30 days' + AND t.end_time IS NOT NULL + GROUP BY t.imei, day +) +SELECT + PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY utilisation_pct) AS p25_red_cut, + PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY utilisation_pct) AS p50_amber_cut, + PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY utilisation_pct) AS p75_green_cut, + PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY utilisation_pct) AS p90_stretch +FROM daily; +``` + +Replace the Appendix A band edges with the returned percentiles. Repeat for idle %, speeding rate, harsh driving index, alarms per week. Document the recalibration date and the previous values in a changelog so band drift is visible. + +**City-cohort cuts.** Nairobi traffic, Mombasa port runs, and Kampala cross-border routes produce genuinely different distributions. Group the recalibration by `devices.assigned_city` so you end up with three threshold sets, not one fleet-average compromise: + +```sql +-- Apply the same percentile function grouped by city +SELECT + d.assigned_city, + PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY utilisation_pct) AS p50, + PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY utilisation_pct) AS p75 +FROM daily +JOIN tracksolid.devices d ON d.imei = daily.imei +GROUP BY d.assigned_city; +``` + +--- + +*Document generated: 2026-04-18 · Stack: TimescaleDB 2.15 + PostGIS + Tracksolid Pro Open Platform API* *Ingestion pipeline: `ingest_movement_rev.py` v2.2 · `ingest_events_rev.py` · `webhook_receiver_rev.py`* diff --git a/06_business_analytics_migration.sql b/06_business_analytics_migration.sql new file mode 100644 index 0000000..0eccf22 --- /dev/null +++ b/06_business_analytics_migration.sql @@ -0,0 +1,225 @@ +-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +-- Migration 06 — Business Analytics Schema Support +-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +-- Adds the schema objects referenced by 01_BusinessAnalytics.md: +-- • tracksolid.devices.assigned_city (§3.7 Geographic Drift) +-- • tracksolid.dispatch_log (§4.4, §4.5 Field-Service SLAs) +-- • ops schema (external ops integration namespace) +-- • ops.service_log (§10 Service-Interval Forecaster) +-- • ops.odometer_readings (§3.8 Odometer Divergence) +-- • ops.tickets (§2.4 Cost-per-Ticket — skeleton) +-- • ops.vw_service_forecast (§10 weekly booking view) +-- +-- Run after migration 05. Safe to re-run (uses IF NOT EXISTS / DO NOTHING / +-- CREATE OR REPLACE). +-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +BEGIN; + +-- ── 1. City cohort column (§3.7) ───────────────────────────────────────────── + +ALTER TABLE tracksolid.devices + ADD COLUMN IF NOT EXISTS assigned_city TEXT; + +COMMENT ON COLUMN tracksolid.devices.assigned_city + IS 'Operating territory code: NBO (Nairobi) | MBA (Mombasa) | KLA (Kampala). ' + 'Used for city-cohort analytics and geographic drift detection.'; + +CREATE INDEX IF NOT EXISTS idx_devices_assigned_city + ON tracksolid.devices (assigned_city) + WHERE assigned_city IS NOT NULL; + +-- ── 2. Dispatch log (§4.4, §4.5) ────────────────────────────────────────────── +-- One row per ticket dispatch. Populated by n8n / ops integration at +-- assignment; back-filled by nightly job using trips + live_positions. + +CREATE TABLE IF NOT EXISTS tracksolid.dispatch_log ( + dispatch_id BIGSERIAL PRIMARY KEY, + ticket_id TEXT NOT NULL, + imei TEXT NOT NULL REFERENCES tracksolid.devices(imei), + driver_name TEXT, + job_lat DOUBLE PRECISION NOT NULL, + job_lng DOUBLE PRECISION NOT NULL, + job_geom geometry(Point, 4326), + assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + first_movement_at TIMESTAMPTZ, + on_site_at TIMESTAMPTZ, + resolved_at TIMESTAMPTZ, + cancelled_at TIMESTAMPTZ, + distance_km NUMERIC(8, 2), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_dispatch_log_ticket + ON tracksolid.dispatch_log (ticket_id); +CREATE INDEX IF NOT EXISTS idx_dispatch_log_imei_assigned + ON tracksolid.dispatch_log (imei, assigned_at DESC); +CREATE INDEX IF NOT EXISTS idx_dispatch_log_assigned_at + ON tracksolid.dispatch_log (assigned_at DESC); +CREATE INDEX IF NOT EXISTS idx_dispatch_log_job_geom + ON tracksolid.dispatch_log USING GIST (job_geom); + +COMMENT ON TABLE tracksolid.dispatch_log + IS 'Persistent record of every dispatch decision. Powers SLA metrics: ' + 'dispatch latency, depart delay, time-to-site, wrench time.'; +COMMENT ON COLUMN tracksolid.dispatch_log.first_movement_at + IS 'First trip start after assigned_at. Back-filled nightly from trips.'; +COMMENT ON COLUMN tracksolid.dispatch_log.on_site_at + IS 'Time vehicle entered 150 m radius of job_geom. Back-filled nightly.'; +COMMENT ON COLUMN tracksolid.dispatch_log.resolved_at + IS 'Ticket close time from the ops system (ops.tickets.closed_at).'; + +-- ── 3. ops schema namespace ─────────────────────────────────────────────────── +-- Separates Fireside operations domain (tickets, services, odometers) from +-- the tracksolid telematics namespace so ownership / grants can diverge. + +CREATE SCHEMA IF NOT EXISTS ops; + +COMMENT ON SCHEMA ops + IS 'Fireside operations domain: tickets, service logs, odometer readings. ' + 'Distinct from tracksolid.* which holds telematics data.'; + +-- ── 4. Service log (§10) ────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS ops.service_log ( + service_id BIGSERIAL PRIMARY KEY, + imei TEXT NOT NULL REFERENCES tracksolid.devices(imei), + service_date DATE NOT NULL, + odometer_km INTEGER NOT NULL, + service_type TEXT, + cost_kes INTEGER, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_service_log_imei_date + ON ops.service_log (imei, service_date DESC); + +COMMENT ON TABLE ops.service_log + IS 'Workshop service history. Powers §10 Service-Interval Forecaster.'; +COMMENT ON COLUMN ops.service_log.service_type + IS 'scheduled | repair | tyre | bodywork | inspection | other'; +COMMENT ON COLUMN ops.service_log.odometer_km + IS 'Physical odometer reading at service time (integer km).'; + +-- ── 5. Odometer readings (§3.8) ─────────────────────────────────────────────── +-- Periodic physical odometer captures from service events, fuel card receipts, +-- or manual driver entry. Divergence vs tracker-computed distance flags +-- sensor drift or tamper. + +CREATE TABLE IF NOT EXISTS ops.odometer_readings ( + reading_id BIGSERIAL PRIMARY KEY, + imei TEXT NOT NULL REFERENCES tracksolid.devices(imei), + reading_date DATE NOT NULL, + reading_km INTEGER NOT NULL, + source TEXT, + recorded_by TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (imei, reading_date) +); + +CREATE INDEX IF NOT EXISTS idx_odometer_readings_imei_date + ON ops.odometer_readings (imei, reading_date DESC); + +COMMENT ON TABLE ops.odometer_readings + IS 'Physical odometer captures from service, fuel card, or manual entry. ' + 'Powers §3.8 Odometer Divergence audit.'; +COMMENT ON COLUMN ops.odometer_readings.source + IS 'service | fuel_card | driver_manual | workshop_form'; + +-- ── 6. Tickets skeleton (§2.4) ─────────────────────────────────────────────── +-- MINIMAL skeleton so the Cost-per-Ticket query is runnable. In production, +-- this table is expected to be populated by the Fireside ticketing system +-- (Zoho/Freshdesk/job-management export) via n8n or a direct feed. Schema +-- is intentionally narrow — extend with columns specific to your source. + +CREATE TABLE IF NOT EXISTS ops.tickets ( + ticket_id TEXT PRIMARY KEY, + assigned_imei TEXT REFERENCES tracksolid.devices(imei), + driver_name TEXT, + customer TEXT, + job_type TEXT, + priority TEXT, + status TEXT NOT NULL DEFAULT 'open', + created_at TIMESTAMPTZ NOT NULL, + assigned_at TIMESTAMPTZ, + closed_at TIMESTAMPTZ, + job_lat DOUBLE PRECISION, + job_lng DOUBLE PRECISION, + job_geom geometry(Point, 4326), + ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_tickets_status_created + ON ops.tickets (status, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_tickets_assigned_imei + ON ops.tickets (assigned_imei) + WHERE assigned_imei IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_tickets_closed_at + ON ops.tickets (closed_at DESC NULLS LAST); + +COMMENT ON TABLE ops.tickets + IS 'Skeleton for ticket data sourced from the Fireside ops system. ' + 'Replace or extend to match the actual feed (Zoho Desk, Freshdesk, etc).'; +COMMENT ON COLUMN ops.tickets.status + IS 'open | assigned | in_progress | resolved | cancelled'; + +-- ── 7. Service forecast view (§10) ──────────────────────────────────────────── +-- Wraps the §10 forecaster CTE so the weekly booking query in +-- 01_BusinessAnalytics.md references a stable object. + +CREATE OR REPLACE VIEW ops.vw_service_forecast AS +WITH last_service AS ( + SELECT DISTINCT ON (imei) + imei, + service_date, + odometer_km + FROM ops.service_log + WHERE service_type = 'scheduled' + ORDER BY imei, service_date DESC +), +current_odometer AS ( + SELECT imei, current_mileage_km + FROM tracksolid.devices +), +trailing_rate AS ( + SELECT + imei, + SUM(distance_km) / 30.0 AS km_per_day_30d + FROM tracksolid.trips + WHERE start_time > NOW() - INTERVAL '30 days' + AND end_time IS NOT NULL + GROUP BY imei +) +SELECT + d.imei, + d.driver_name, + d.vehicle_number, + ls.service_date AS last_service_date, + ls.odometer_km AS last_service_odo, + co.current_mileage_km AS current_odo, + (co.current_mileage_km - COALESCE(ls.odometer_km, 0)) AS km_since_service, + GREATEST( + 0, + 10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0)) + ) AS km_to_next_service, + ROUND(tr.km_per_day_30d, 1) AS km_per_day_30d, + CASE + WHEN tr.km_per_day_30d > 0 THEN + CURRENT_DATE + ( + GREATEST(0, 10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0))) + / tr.km_per_day_30d + )::INT + ELSE NULL + END AS projected_service_date +FROM tracksolid.devices d +LEFT JOIN last_service ls ON ls.imei = d.imei +LEFT JOIN current_odometer co ON co.imei = d.imei +LEFT JOIN trailing_rate tr ON tr.imei = d.imei +WHERE d.enabled_flag = 1; + +COMMENT ON VIEW ops.vw_service_forecast + IS 'Projected next-service date per vehicle based on 30-day km rate. ' + 'Service interval default 10,000 km — override at query time if needed.'; + +COMMIT; diff --git a/20260414_FS__Logistics - final_fixed.csv b/20260414_FS__Logistics - final_fixed.csv new file mode 100644 index 0000000..6c3b89c --- /dev/null +++ b/20260414_FS__Logistics - final_fixed.csv @@ -0,0 +1,145 @@ +Account,Customer Name,Device Name,IMEI,Model,Activated Date,Sales Time,SIM,MAC,Subscription Expiration,User Expiration Date,Battery replacement date,Group,ICCID,IMSI,Driver Name,Telephone,License Plate No.,ID Number,Department,VIN,Engine Number,Vehicle Brand,Vehicle Model,Fuel/100km,Installation Time +fireside,Fireside Group HQ,UMA 382EK_UG,865135061569479,X3,2026-02-26,2025-09-08,+256792997079,,2036-02-27,2036-02-27,,Default Group,8925610001837573419F,641101970467667,UG,,UMA 382EK,,MTN,,,,,, +fireside,Fireside Group HQ,UMA 418EK_UG,865135061569131,X3,2026-02-26,2025-09-08,+256792997053,,2036-02-27,2036-02-27,,Default Group,8925610001837573385F,641101970467664,UG,,UMA 418EK,,MTN,,,,,, +fireside,Fireside Group HQ,John Mbugua/OSP-KDW 573B_CAM,862798052707896,JC400P,2026-01-30,2025-06-11,,,2036-01-31,2036-01-31,,Default Group,89254021414206816725,639021410681672,John Mbugua,,KDW 573B,,OSP,,,,Probox,, +fireside,Fireside Group HQ,JOEL NTUMBA/ISP-UMA 826AB_UG,865135061563423,X3,2026-01-28,2025-09-08,0119051036,,2036-01-29,2036-01-29,,Default Group,89254021414206652690,639021410665269,Joel Ntumba,,UMA 826AB,,MTN,,,,Motorbike,, +fireside,Fireside Group HQ,RODIN KIBERU/ISP-UMA 011EK_UG,865135061564280,X3,2026-01-28,2025-09-08,0118081642,,2036-01-29,2036-01-29,,Default Group,89254021414206817244,639021410681724,Rodin Kiberu,,UMA 011EK,,MTN,,,,Motorbike,, +fireside,Fireside Group HQ,Wambua/ROLLOUT-KDV 683Z_CAM,862798052708068,JC400P,2026-01-24,2025-06-11,0758048043,,2036-01-25,2036-01-25,,Default Group,89254021414206816964,639021410681696,Dominic Wambua,,KDV 683Z,,ROLLOUT,,,,Probox,, +fireside,Fireside Group HQ,Levine/OSP-KDV 439_CAM,862798052708167,JC400P,2025-12-13,2025-06-11,0758046738,,2035-12-14,2035-12-14,,Default Group,89254021414206816741,639021410681674,Levine Wasike,,KDV 439W,,FDS,,,,Probox,, +fireside,Fireside Group HQ,Benjamin/PLAN-KDV 438W_Track,865135061563639,X3,2025-12-13,2025-09-08,0758047065,,2035-12-14,2035-12-14,,Default Group,89254021414206816683,639021410681668,Benjamin Ananda,,KDV 438W,,PLANNING,,,,Probox,, +fireside,Fireside Group HQ,Albert/FDS-KDV 437W_Track,865135061569123,X3,2025-12-13,2025-09-08,0758047101,,2035-12-14,2035-12-14,,Default Group,89254021414206816881,639021410681688,Albert Mutwiri,,KDV 437W,,FDS,,,,Probox,, +fireside,Fireside Group HQ,Silvanus/FDS-KDV 064S_Track,865135061564470,X3,2025-11-21,2025-09-08,0113669866,,2035-11-22,2035-11-22,,Default Group,89254021414206378718,639021410637871,Silvanus Kipkorir,,KDV 064S,,AIRTEL,,,,Probox,, +fireside,Fireside Group HQ,Robbert/FDS-KDV 072L_Track,865135061581904,X3,2025-11-21,2025-09-08,0114149576,,2035-11-22,2035-11-22,,Default Group,89254021264261503993,639021266150399,Robert Kipruto,,KDV 072L,,FDS,,,,Probox,, +fireside,Fireside Group HQ,Benard Kimutai/KDN 759G_CAM,862798052713779,JC400P,2025-08-23,2025-06-11,0752143258,,2035-08-24,2035-08-24,,Default Group,89254035061001753860,639035060175386,Benard Kimutai,,KDN 759G,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Geoffrey/Rider-KMGS 239H,865135061043426,X3,2025-08-22,2025-06-11,0768696658,,2035-08-23,2035-08-23,,Default Group,89254021394274518926,639021397451892,Geoffrey Karanja,,KMGS 239H,,OSP-PATROL,,,,Motorbike,, +fireside,Fireside Group HQ,Samuel Kihara/Rider_KMEL 225X,865135061053714,X3,2025-08-02,2025-06-11,0768696832,,2035-08-03,2035-08-03,,Default Group,89254021394274518934,639021397451893,Samuel Kihara,,KMEL 225X,,OSP-PATROL,,,,Motorbike,, +fireside,Fireside Group HQ,Brian Njenga/Rider-KMFF 113Z,865135061036164,X3,2025-07-31,2025-06-11,0768696705,,2035-08-01,2035-08-01,,Default Group,89254021394274518850,639021397451885,Brian Njenga,,KMFF 113Z,,OSP-PATROL,,,,Motorbike,, +fireside,Fireside Group HQ,KMGK 596V,865135061049001,X3,2025-07-31,2025-06-11,0768697064,,2035-08-01,2035-08-01,,Default Group,89254021394274518884,639021397451888,Parked,,KMGK 596V,,DELIVERIES,,,,Motorbike,, +fireside,Fireside Group HQ,Rofas/General-KDT 728R_CAM,862798052715220,JC400P,2025-07-16,2025-06-11,0704573658,,2035-07-17,2035-07-17,,Default Group,89254021334258495873,639021335849587,Rofas Njagi,,KDT 728R,,REGIONAL,,,,Probox,, +fireside,Fireside Group HQ,Emmanuel/Gen-KDS 453Y_Track,865135061037980,X3,2025-07-15,2025-06-11,0790176734,,,2035-07-15,,Default Group,89254021394215205856,639021391520585,Emmanuel Luseno,,KDS 453Y,,GENERAL,,,,Pick-Up,, +fireside,Fireside Group HQ,Kimeria/Crane-KDS 525D_Track,865135061035778,X3,2025-07-11,2025-06-11,0790176738,,2035-07-12,2035-07-12,,Default Group,89254021394215205922,639021391520592,John Kimeria,,KDS 525D,,GENERAL,,,,Crane,, +fireside,Fireside Group HQ,Rashid/ISP-KDM 840V_Track,865135061053748,X3,2025-07-10,2025-06-11,0768445963,,2035-07-11,2035-07-11,,Default Group,89254021334212352574,639021331235257,Rashid Hassan,,KDM 840V,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Wambugu/FDS-KDR 592N_Track,865135061042261,X3,2025-07-10,2025-06-11,0797680464,,2035-07-11,2035-07-11,,Default Group,89254021334258159693,639021335815969,Kelvin Wambugu,,KDR 592N,,FDS,,,,Probox,, +fireside,Fireside Group HQ,James Onyango-KDU 613B__CAM,862798052713811,JC400P,2025-07-09,2025-06-11,0790176542,,2035-07-10,2035-07-10,,Default Group,89254021394215205880,639021391520588,James Onyango,,KDU 613B,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Mazda-KDU 613A_Track,865135061047435,X3,2025-07-09,2025-06-11,0790175971,,2035-07-10,2035-07-10,,Default Group,89254021394215205971,639021391520597,Management_Mazda,,KDU 613A,,MGT,,,,Mazda,, +fireside,Fireside Group HQ,Charles Nyambane/ISP-KCB 711C_CAM,862798050522743,JC400P,2023-12-22,2024-11-08,0768657106,,2033-12-23,2033-12-23,,Default Group,,,Charles Nyambane,,KCB 711C,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Sadique/GEN-KDC 490Q_CAM,862798050525225,JC400P,2023-12-22,2024-11-08,0768652386,,2043-12-22,2043-12-22,,Default Group,,,Sadique Wakayula,,KDC 490Q,,GENERAL,,,,Crane,, +fireside,Fireside Group HQ,Samuel Nganga/ISP-KDE 264M_CAM,862798050525068,JC400P,2023-12-22,2024-11-08,0768658564,,2033-12-23,2033-12-23,,Default Group,,,Samuel Ng'ang'a,,KDE 264M,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Kennedy Ondieki/ISP-KCU 237Z_CAM,862798050525837,JC400P,2023-12-21,,0113669852,,2033-12-22,2033-12-22,,Default Group,,,Kennedy Ondieki,,KCU 237Z,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Geoffrey Too/OSP-KDM 308S_CAM,862798050523618,JC400P,2023-08-15,2023-08-22,0701211625,,2033-08-16,2033-08-16,,Default Group,,,Geoffrey Too,,KDM 308S,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Job Ngare/ISP Coast-KDM309S_CAM,862798050523816,JC400P,2023-08-15,2023-08-22,0707936781,,2033-08-16,2033-08-16,,Default Group,,,Job Ngare,,KDM 309S,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Daudi Jaoko/OSP-KDK 815R_Track,359857082912239,GT06E,2023-06-21,2023-07-27,0706392117,,2033-06-22,2033-06-22,,Default Group,89254021234296021287,639021239602128,Dickson Jaoko,,KDK 815R,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Peter Mbugua/ISP-KDK 728K_Track,359857082897091,GT06E,2022-12-14,2022-12-16,0790262984,,2042-12-15,2042-12-15,,Default Group,89254021234222500396,639021232250039,Peter Mbugua,,KDK 728K,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Peter Mbugua/KDK 728K_CAM,862798050524608,JC400P,2022-12-03,2022-12-15,0706742413,,2042-12-04,2042-12-04,,Default Group,,,Peter Mbugua,,KDK 728K,,ISP,,,,Probox,, +fireside,Fireside Group HQ,JC400P-24368,862798050524368,JC400P,2022-10-29,2022-12-17,,,2042-10-30,2042-10-30,,Default Group,,,Identification,,,,,,,,,, +fireside,Fireside Group HQ,Mutuku/FDS-KDC 739F_CAM,862798050524558,JC400P,2022-01-22,2022-01-25,0100858817,,2042-01-23,2042-01-23,,Default Group,,,Mutuku Joseph,,KDC 739F,,FDS,,,,Probox,, +fireside,Fireside Group HQ,Cornelius/FDS-KCU 938R_CAM,862798050524897,JC400P,2022-01-22,2022-01-25,0114924404,,2042-01-23,2042-01-23,,Default Group,,,Cornelius Kimutai,,KCU 938R,,FDS,,,,Van,, +fireside,Fireside Group HQ,Cassius/OSP-KDB 323M_CAM,862798050522107,JC400P,2022-01-22,2022-01-25,0114149576,,2042-01-23,2042-01-23,,Default Group,,,Cassius Wakiyo,,KDB 323M,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Richardson/ISP/Coast-KDC 207R _CAM,862798050524657,JC400P,2022-01-22,2022-01-25,0758689195,,2042-01-23,2042-01-23,,Default Group,,,Felix Andole,,KDC 207R,,ISP,,,,Probox,, +fireside,Fireside Group HQ,George/OSP KDD 684Y-CAM,862798050523386,JC400P,2022-01-22,2022-01-27,0785586834,,2042-01-23,2042-01-23,,Default Group,,,George Ochieng',,KDD 684Y,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Hamis Pande/ISP-KDD 689Y_CAM,862798050524384,JC400P,2022-01-22,2022-01-27,0701211744,,2042-01-23,2042-01-23,,Default Group,,,Hamisi Pande,,KDD 689Y,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Simon Kamau/ISP-KCE 090R_CAM,862798050525589,JC400P,2022-01-19,2022-01-17,0796276387,,2042-01-20,2042-01-20,,Default Group,,,Simon Kamau,,KCE 090R,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Makori John/PLAN-KDB 585E_CAM,862798050525423,JC400P,2022-01-15,2022-01-17,0701211724,,2042-01-16,2042-01-16,,Default Group,,,Makori John,,KDB 585E,,PLANNING,,,,Probox,, +fireside,Fireside Group HQ,Oseko/OSP-KCG 668W_CAM,862798050525951,JC400P,2022-01-15,2022-01-17,0741943212,,2042-01-16,2042-01-16,,Default Group,,,Wright Oseko,,KCG 668W,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Garage/OSP-KCH 167M_CAM,862798050522859,JC400P,2022-01-15,2022-01-17,0706740252,,2042-01-16,2042-01-16,,Default Group,,,Garage,,KCH 167M,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Garage/ROLL-KCE 699F_CAM,862798050524707,JC400P,2022-01-15,2022-01-17,0110525751,,2042-01-16,2042-01-16,,Default Group,,,Garage,,KCE 699F,,ROLLOUT,,,,Probox,, +fireside,Fireside Group HQ,Dan Watila/ISP-KDE 638J_CAM,862798050522883,JC400P,2022-01-15,2022-01-17,0112615393,,2042-01-16,2042-01-16,,Default Group,,,Dan Watila,,KDE 638J,,ISP,,,,Probox,, +fireside,Fireside Group HQ, Samuel Kamau/ROLL-KCA 542Q_CAM,862798050525605,JC400P,2022-01-15,2022-01-17,0110526783,,2042-01-16,2042-01-16,,Default Group,,,John Ondego,,KCA 542Q,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Brian Ngetich/ISP-KDA 717B_CAM,862798050288360,JC400P,2021-11-05,2021-11-08,0717867861,,2041-11-06,2041-11-06,,Default Group,,,Brian Ngetich,,KDA 717B,,ISP,,,,Probox,, +fireside,Fireside Group HQ,Patric Bet/OSP-KDA 609E_CAM,862798050288261,JC400P,2021-10-23,2021-10-25,0790176509,,2041-10-24,2041-10-24,,Default Group,,,Patric Bett,0112693340,KDA 609E,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Gabriel/ROLL-KCE 690F_Track,359857082042052,GT06E,2020-04-03,2020-04-16,0110094466,,2040-04-04,2040-04-04,,Default Group,89254021164215938024,639021161593802,Gabriel Musumba,,KCE 690F,,OSP,,,,Probox,, +fireside,Fireside Group HQ,Allan Owana/ISP-KDK780K_Track,359857081885410,GT06E,2019-06-19,2019-07-01,0703616117,,2039-06-20,2039-06-20,,Default Group,89254021234222499854,639021232249985,Allan Owana,,KDK 780K,,ISP,,,,Probox,, +fireside,Fireside Group HQ, Garage/OSP-KCH 167M,359857081891798,GT06E,2019-06-16,2019-07-01,0746760102,,2039-06-17,2039-06-17,,Default Group,89254021084186499493,639021088649949,Garage,,KCH 167M,,OSP,,,,Probox,, +fireside,Fireside Group HQ,John Ondego/ISP-KCA 542Q_Track,359857081891632,GT06E,2019-06-15,2019-07-01,0746760038,,2039-06-16,2039-06-16,,Default Group,89254021084186499485,639021088649948,John Ondego,,KCA 542Q,,ISP,,,,Probox,, +fireside,Fireside Group HQ,JC400P-08035,862798052708035,JC400P,Inactive,2025-06-11,,,120Month,——,,Default Group,,,Identification,,,,,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Wambua/ROLLOUT-KDV 683Z_Track,865135061563597,X3,2026-01-30,2026-02-24,0758052405,,2036-01-31,2036-01-31,,Default Group,89254021414206816733,639021410681673,Dominic Wambua,,KDV 683Z,,ROLLOUT,,,,Probox,, +Fireside@HQ,Fireside Telematics ,John Mbugua/OSP-KDW 573B_Track,865135061562722,X3,2026-01-30,2026-02-24,0758052508,,2036-01-31,2036-01-31,,Default Group,89254021414206816832,639021410681683,John Mbugua,,KDW 573B,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Godffrey Nandwa/ISP-KCN 496A_CAM,862798052708282,JC400P,2026-01-25,2026-02-20,0758047934,,2036-01-26,2036-01-26,,Default Group,89254021414206816865,639021410681686,Godffrey Nandwa,,KCN 496A,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Benjamin/PLAN-KDV 438W_CAM,862798052707888,JC400P,2025-12-15,2026-02-20,0758047312,,2035-12-16,2035-12-16,,Default Group,89254021414206816980,639021410681698,Benjamin Ananda,,KDV 438W,,PLANNING,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Albert/FDS-KDV 437W_CAM,862798052708076,JC400P,2025-12-13,2026-02-20,0758047094,,2035-12-14,2035-12-14,,Default Group,89254021414206816782,639021410681678,Albert Mutwiri,,KDV 437W,,FDS,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Levine/OSP-KDV 439W_Track,865135061562847,X3,2025-12-13,2026-02-24,0758047032,,2035-12-14,2035-12-14,,Default Group,89254021414206816840,639021410681684,Levine Wasike,,KDV 439W,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,JC400P-14066,862798052714066,JC400P,2025-11-21,2025-06-11,,,2035-11-22,2035-11-22,,Default Group,89254021414206378684,639021410637868,Identification,,,,,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Kennedy Ondieki/ISP-KCU 237Z_CAM,862798052713837,JC400P,2025-10-08,2026-02-20,0113669852,,2035-10-09,2035-10-09,,Default Group,89254021414206327855,639021410632785,Kennedy Ondieki,,KCU 237Z,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,JC400P-13696,862798052713696,JC400P,2025-09-02,2025-06-11,,,2035-09-03,2035-09-03,,Default Group,89254021394215205906,639021391520590,Identification,,,,,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Gitau/Regional-KDT 916R_CAM,862798052713985,JC400P,2025-08-02,2026-02-20,0768696668,,2035-08-03,2035-08-03,,Default Group,89254021394274518892,639021397451889,Timothy Gitau,,KDT 916R,,REGIONAL,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Richardson Komu-KDT 923R_Track,865135061035653,X3,2025-08-02,2026-02-24,0768697292,,2035-08-03,2035-08-03,,Default Group,89254021394274518942,639021397451894,Richardson Komu,,KDT 923R,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Muriithi/Huawei-KDR 594N_Track,865135061048466,X3,2025-07-24,2026-02-24,0797680395,,2035-07-25,2035-07-25,,Default Group,89254021334258159628,639021335815962,Samuel Muriithy,,KDR 594N,,ROLLOUT,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Rofas/General-KDT 728R_Track,865135061054555,X3,2025-07-16,2026-02-24,0790176726,,2035-07-17,2035-07-17,,Default Group,89254021394215205823,639021391520582,Rofas Njagi,,KDT 728R,,REGIONAL,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Mazda-KDU 613A_CAM,862798052713761,JC400P,2025-07-09,2026-02-20,0790176786,,2035-07-10,2035-07-10,,Default Group,89254021394215205955,639021391520595,Management_Mazda,,KDU 613A,,MGT,,,,Mazda,, +Fireside@HQ,Fireside Telematics ,James Onyango-KDU 613B_Track,865135061054548,X3,2025-07-09,2026-02-24,0790175997,,2035-07-10,2035-07-10,,Default Group,89254021394215205948,639021391520594,James Onyango,,KDU 613B,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Rashid/ISP-KDM 840V_CAM,862798050526231,JC400P,2023-12-22,2026-02-20,0790175526,,2043-12-23,2043-12-23,,Default Group,,,Rashid Hassan,,KDM 840V,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Mike Wanaswa/FDS-KDT 724R_CAM,862798050523139,JC400P,2023-12-22,2026-02-20,0790175045,,2043-12-23,2043-12-23,,Default Group,,,Mike Wanaswa,,KDT 724R,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Wambugu/FDS-KDR 592N_CAM,862798050523063,JC400P,2023-12-22,2026-02-20,0701211876,,2043-12-22,2043-12-22,,Default Group,,,Kelvin Wambugu,,KDR 594N,,FDS,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Major Simiyu/FDS-KDS949Y_CAM,862798050523626,JC400P,2023-12-22,2026-02-20,0701211892,,2033-12-23,2033-12-23,,Default Group,,,Major Simiyu,,KDS 949Y,,FDS,,,,Probox,, +Fireside@HQ,Fireside Telematics , VICTOR/OSP-KDS919Y_CAM ,862798050523337,JC400P,2023-12-22,2026-02-20,0700242527,,2043-12-22,2043-12-22,,Default Group,,,Victor Kimutai,,KDS 919Y,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Emmanuel/Gen-KDS 453Y_CAM,862798050523295,JC400P,2023-12-22,2026-02-20,0700242474,,2033-12-23,2033-12-23,,Default Group,,,Emmanuel Luseno,,KDS 453 Y,,GENERAL,,,,Pick-Up,, +Fireside@HQ,Fireside Telematics ,Muriithi/Huawei-KDR 594N_CAM,862798050523014,JC400P,2023-12-21,2026-02-20,0790175423,,2033-12-22,2033-12-22,,Default Group,,,Samuel Muriithy,,KDR 594N,,ROLLOUT,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Kimeria-General-KDS 525D_CAM,862798050521521,JC400P,2023-11-26,2026-02-20,0752958416,,2033-11-27,2033-11-27,,Default Group,,,John Kimeria,,KDS 525D,,GENERAL,,,,Crane,, +Fireside@HQ,Fireside Telematics ,Leonard/ISP-KDM 306S _CAM,862798050524533,JC400P,2023-08-21,2026-02-20,0703487162,,2033-08-22,2033-08-22,,Default Group,,,Leonard Nzai,,KDM 306S,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Job Ngare/ISP Coast-KDM309S_Track,359857082898016,GT06E,2023-08-15,2026-02-24,0706895756,,2033-08-16,2033-08-16,,Default Group,89254021324273007563,639021327300756,Job Ngare,,KDM 309S,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Dickson Jaoko/OSP-KDK 815R_CAM,862798050525266,JC400P,2023-06-21,2026-02-20,0706665867,,2033-06-22,2033-06-22,,Default Group,,,Dickson Jaoko,,KDK 815R,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Alan Owana/ISP-KDK 780K_CAM,862798050523527,JC400P,2022-12-03,2026-02-20,0792375024,,2042-12-04,2042-12-04,,Default Group,,,Allan Owana,,KDK 780K,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Amani Sulubu/ISP-KCY 090X_CAM,862798050524426,JC400P,2022-01-16,2026-02-20,0113823350,,2042-01-17,2042-01-17,,Default Group,,,Amani Sulubu,,KCY 090X,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Gideon/ISP-KCQ 215F_CAM,862798050522065,JC400P,2022-01-16,2026-02-20,0113343715,,2042-01-17,2042-01-17,,Default Group,,,Gideon Kiprono,,KCQ 215F,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Gabriel/OSP-KCE 690F_CAM,862798050525670,JC400P,2022-01-15,2026-02-20,0701211996,,2042-01-16,2042-01-16,,Default Group,,,Gabriel Musumba,,KCE 690F,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Santoes/OSP-KCZ 181P_CAM D-Max,862798050288345,JC400P,2021-11-06,2026-02-20,0768446105,,2041-11-07,2041-11-07,,Default Group,,,Santoes Omondi,,KCZ 181P,,OSP,,,,Pick-Up,, +Fireside@HQ,Fireside Telematics ,Elias Baya/FDS-KCZ 476E_CAM,862798050288303,JC400P,2021-11-06,2026-02-20,0115870439,,2041-11-07,2041-11-07,,Default Group,,,Elias Baya,,KCZ 476E,,FDS,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Nicholas Erastus /ISP-KCQ 581M_CAM,862798050288212,JC400P,2021-11-02,2026-02-20,0746979531,,2041-11-03,2041-11-03,,Default Group,,,Nicholas Erastus,,KCQ 581M,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Samuel Ng'ang'a/ISP-KDE 264M_Track,359857082898008,GT06E,2021-10-28,2026-02-24,0711731539,,2041-10-29,2041-10-29,,Default Group,89254021264260342245,639021266034224,Samuel Ng'ang'a,,KDE 264M,,ISP ,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Dan Watila/ISP-KDE 638J,359857082898487,GT06E,2021-10-21,2026-02-24,0116242996,,2041-10-22,2041-10-22,,Default Group,89254021334258404214,639021335840421,Dan Watila,,KDE 638J,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Geoffrey Too/ISP-KDM 308S,359857082900358,GT06E,2021-10-21,2026-02-24,0796527601,,2041-10-22,2041-10-22,,Default Group,89254021264260126572,639021266012657,Geoffrey Too,,KDM 308S,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Hamisi/ISP-KDD 689Y,359857082896911,GT06E,2021-09-17,2026-02-24,0112714612,,2041-09-18,2041-09-18,,Default Group,89254021214211314660,639021211131466,Hamisi Pande,,KDD 689Y,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,George/OSP-KDD 684Y_Track,359857082900697,GT06E,2021-09-17,2026-02-24,0114879518,,2041-09-18,2041-09-18,,Default Group,89254021214211314678,639021211131467,George Ochieng',,KDD 684Y,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Cassius/OSP-KDB 323M_Track,359857082897257,GT06E,2021-08-29,2026-02-24,0746428882,,2041-08-29,2041-08-29,,Default Group,89254021234222500818,639021232250081,Cassius Wakiyo,,KDB 323M,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,John Makori/PLAN-KDB 585E,359857082897737,GT06E,2021-08-29,2026-02-24,0114596734,,2041-08-29,2041-08-29,,Default Group,89254021214211145262,639021211114526,John Makori,,KDB 585E,,PLANNING,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Kelvin Gichea/ISP-KDA 717B,359857082911983,GT06E,2021-08-29,2026-02-24,0795188807,,2041-08-29,2041-08-29,,Default Group,89254021214211145288,639021211114528,Brian Ngetich,0795188807,KDA 717B,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Sadique/GEN-KDC 490Q_Track,359857082902461,GT06E,2021-05-22,2026-02-24,0757556468,,2041-05-22,2041-05-22,,Default Group,89254021154296722488,639021159672248,Sadique Wakayula,,KDC 490Q,,GENERAL,,,,Crane,, +Fireside@HQ,Fireside Telematics ,Andrew Makanda/ISP/Coast-KDC 207R ,359857082902503,GT06E,2021-05-15,2026-02-24,0794820817,,2041-05-15,2041-05-15,,Default Group,89254021224270993254,639021227099325,Felix Andole,,KDC 207R,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Mutuku Joseph/FDS-KDC 739F ,359857082897794,GT06E,2021-04-10,2026-02-24,0115019037,,2041-04-10,2041-04-10,,Default Group,89254021224222632356,639021222263235,Mutuku Joseph,0115019037,KDC 739F,,FDS,,,,Probox,, +Fireside@HQ,Fireside Telematics , Patric Bet/OSP-KDA 609E_Track,359857082910589,GT06E,2020-10-26,2026-02-24,0797622637,,2040-10-27,2040-10-27,,Default Group,89254021154296722496,639021159672249,Patric Bett,,KDA 609E,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Charles Nyambane/ISP-KCB 711C_Track,359857082918012,GT06E,2020-09-21,2026-02-24,0793704231,,2040-09-22,2040-09-22,,Default Group,89254021154287138363,639021158713836,Charles Nyambane,,KCB 711C,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Oseko Wright/OSP-KCG 668W_Track,359857081887069,GT06E,2019-06-30,2026-02-24,0746763106,,2039-07-01,2039-07-01,,Default Group,89254021084186499915,639021088649991,Wright Oseko,,KCG 668W,,OSP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,KCE 699F,359857081891590,GT06E,2019-06-16,2026-02-24,0746760215,,2039-06-17,2039-06-17,,Default Group,89254021084186499519,639021088649951,Garage,,KCE 699F,,ROLLOUT,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Simon Kamau/ISP-KCE 090R,359857081891566,GT06E,2019-06-16,2026-02-24,0746760404,,2039-06-17,2039-06-17,,Default Group,89254021084186499527,639021088649952,Simon Kamau,,KCE 090R,,ISP,,,,Probox,, +Fireside@HQ,Fireside Telematics ,Cornelius/FDS-KCU 938R VAN,359857081892101,GT06E,2019-06-12,2026-02-24,0746759919,,2039-06-13,2039-06-13,,Default Group,89254021084186499451,639021088649945,Cornelius Kimutai,,KCU 938R,,FDS,,,,Van,,2019-06-12 +Fireside@HQ,Fireside Telematics ,Nicholas Erastus/ISP-KCQ581M,359857081892309,GT06E,2019-06-09,2026-02-24,0700023776,,2039-06-10,2039-06-10,,Default Group,89254021084178504672,639021087850467,Nicholas Erastus,,KCQ 581M,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Barack_Personal-KDW 781E,865135061563415,X3,2026-01-13,2025-09-08,0758052541,,2036-01-14,2036-01-14,,Default Group,89254021414206816931,639021410681693,Barack Orwa,,KDW 781E,,MGT,,,,Vazel,, +Fireside_MSA,Fireside Group MSA,Major Simiyu-KDS 949Y_Track,865135061035133,X3,2025-08-02,2025-06-11,0768696642,,2035-08-03,2035-08-03,,Default Group,89254021394274518918,639021397451891,Major Simiyu,,KDS 949Y,,FDS,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Harisson/KDT 724R_Track,865135061043079,X3,2025-08-02,2025-06-11,0768696664,,2035-08-03,2035-08-03,,Default Group,89254021394274518959,639021397451895,Mike Wanaswa,,KDT 724R,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Gitau/Regional-KDT 916R_Track,865135061048953,X3,2025-08-02,2025-06-11,0768697056,,2035-08-03,2035-08-03,,Default Group,89254021394274518967,639021397451896,Timothy Gitau,,KDT 916R,,REGIONAL,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Victor/OSP-KDS 919Y_Track,865135061048276,X3,2025-08-02,2025-06-11,0768696755,,2035-08-03,2035-08-03,,Default Group,89254021394274518900,639021397451890,Victor Kimutai,,KDS 919Y,,OSP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Ian Dancan-KDT 923R_CAM,862798050526256,JC400P,2023-12-22,,0794873610,,2043-12-22,2043-12-22,,Default Group,,,Ian Dancun,,KDT 923R,,QEHS,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Wilfred/Gen-KCU 729C_CAM,862798050526165,JC400P,2023-11-26,2024-11-08,0790564929,,2033-11-27,2033-11-27,,Default Group,,,Wilfred Kinyanjui,,KCU 729C,,GENERAL,,,,Crane,, +Fireside_MSA,Fireside Group MSA,Denis Kazungu/KDM 794R_Track,359857082916826,GT06E,2023-08-21,2023-08-22,0705700971,,2033-08-22,2033-08-22,,Default Group,89254021324273006854,639021327300685,Denis Kazungu,,KDM 794R,,FDS,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Mutuku Anthony-KDK 732K_Track,359857082898073,GT06E,2022-12-20,2022-12-20,0793026954,,2042-12-21,2042-12-21,,Default Group,89254021234222387539,639021232238753,Mutuku Antony,,KDK 732K,,FDS,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Anthon/KDK 732K_CAM,862798050524681,JC400P,2022-12-06,2022-12-16,0796275746,,2042-12-07,2042-12-07,,Default Group,,,Mutuku Antony,,KDK 732K,,FDS,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Makanda-KCZ 155P_CAM,862798050524566,JC400P,2022-01-22,2025-02-24,0758781444,,2042-01-23,2042-01-23,,Default Group,,,Makanda Andrew,,KCZ 155P,,OSP,,,,Pick-Up,, +Fireside_MSA,Fireside Group MSA,Dennis Kazungu/-KDM 794R_CAM,862798050521612,JC400P,2022-01-22,2024-11-19,0704113731,,2042-01-23,2042-01-23,,Default Group,,,Denis Kazungu,,KDM 794R,,FDS,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Mbuvi Kioko/OSP-KCZ 199P_CAM,862798050522719,JC400P,2022-01-16,2022-12-16,0768218655,,2042-01-17,2042-01-17,,Default Group,,,Mbuvi Kioko,,KCZ 199P,,OSP,,,,Pick-Up,, +Fireside_MSA,Fireside Group MSA,Felix Muema-KCZ 223P_CAM D-Max,862798050524087,JC400P,2022-01-16,2024-12-30,0113973875,,2042-01-17,2042-01-17,,Default Group,,,Felix Muema,,KCZ 223P,,OSP,,,,Pick-Up,, +Fireside_MSA,Fireside Group MSA,Lawrence Kijogi/ROLL-KCY 080X_CAM,862798050522891,JC400P,2022-01-16,2022-12-16,0113287191,,2042-01-17,2042-01-17,,Default Group,,,Lawrence Kijogi,,KCY 080X,,ROLLOUT,,,,Pick-Up,, +Fireside_MSA,Fireside Group MSA,Ndegwa Duncan/PM-KCG 669W_CAM,862798050524392,JC400P,2022-01-16,2022-12-16, 0113799173,,2042-01-17,2042-01-17,,Default Group,,,Ndegwa Dancun,,KCG 669W,,OSP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Simon Munda-KCZ 154S_CAM,862798050521752,JC400P,2022-01-16,2022-12-16,0113805921,,2042-01-17,2042-01-17,,Default Group,,,Simon Munda,,KCZ 154S,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Moses Wambua-KCZ 751V_CAM,862798050524012,JC400P,2022-01-16,2022-12-16,0113313797,,2042-01-17,2042-01-17,,Default Group,,,Moses Wambua,,KCZ 751V,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Amani Kazungu-KCY 084X_CAM,862798050523204,JC400P,2022-01-16,2022-12-16,0707892547,,2042-01-17,2042-01-17,,Default Group,,,Amani Kazungu,,KCY 084X,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Joseph Kabandi-KCY 076X_CAM,862798050523949,JC400P,2022-01-16,2022-12-16, 0113288492,,2042-01-17,2042-01-17,,Default Group,,,Joseph Kabandi,,KCY 076X,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Kennedy Chege-KCQ 618K_CAM,862798050525613,JC400P,2022-01-16,2022-12-19,0729994247,,2042-01-17,2042-01-17,,Default Group,,,Kennedy Chege,,KCQ 618K,,OSP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Noel/FDS/VOI-KCY 838X_CAM,862798050525753,JC400P,2022-01-15,2023-08-23,,,2042-01-16,2042-01-16,,Default Group,,,Noel Merengeni,,KCY 838X,,FDS,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Noel/VOI-KCY 838X_Track,359857082925330,GT06E,2020-10-26,2023-08-22,0794873610,,2040-10-27,2040-10-27,,Default Group,89254021154296723429,639021159672342,Noel Merengeni,,KCY 838X,,FDS,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Simon Munda-KCZ 154S_Track,359857082900341,GT06E,2020-09-23,2022-12-16,0757236135,,2040-09-24,2040-09-24,,Default Group,89254021154296723312,639021159672331,Simon Munda,,KCZ 154S,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA, Michael Odongo-KCZ 751V,359857082912486,GT06E,2020-09-23,2022-12-16,0792756503,,2040-09-24,2040-09-24,,Default Group,89254021154296723437,639021159672343,Moses Wambua,,KCZ 751V,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Daniel Omondi/Rider_KMFF 099Z,353549090553685,AT4,2020-09-23,2022-12-16,0759336150,,2040-09-24,2040-09-24,,Default Group,89254021334258404099,639021335840409,Daniel Omondi,0112794067,KMFF 099Z,,OSP-PATROL,,,,Motorbike,, +Fireside_MSA,Fireside Group MSA,Daniel Kipkirui/Rider-KMFF 162Z,353549090567685,AT4,2020-09-23,2022-12-16,0742532058,,2040-09-24,2040-09-24,,Default Group,89254021264260388966,639021266038896,Daniel Kipkirui,0112795498,KMFF 162Z,,OSP-PATROL,,,,Motorbike,, +Fireside_MSA,Fireside Group MSA,Makanda/OSP-KCZ155P D-Max,359857082910886,GT06E,2020-08-23,2025-02-24,0745067338,,2040-08-24,2040-08-24,,Default Group,89254021154287138397,639021158713839,Makanda Andrew,,KCZ 155P,,OSP,,,,Pick-Up,, +Fireside_MSA,Fireside Group MSA,Santos/OSP-KCZ 181P D-Max,359857082908500,GT06E,2020-08-23,2022-12-16,0701211974,,2040-08-24,2040-08-24,,Default Group,89254021374215155087,639021371515508,Santoes Omondi,,KCZ 181P,,OSP,,,,Pick-Up,, +Fireside_MSA,Fireside Group MSA,Mbuvi Kioko-KCZ 199P D-Max,359857082918038,GT06E,2020-08-22,2022-12-16,0797318126,,2040-08-23,2040-08-23,,Default Group,89254021154287138389,639021158713838,Mbuvi Kioko,,KCC 199P,,OSP,,,,Pick-Up,, +Fireside_MSA,Fireside Group MSA,Felix Muema-KCZ 223P D-Max,359857082907973,GT06E,2020-08-22,2024-12-30,0757843826,,2040-08-23,2040-08-23,,Default Group,89254021154287138371,639021158713837,Felix Muema,,KCZ 223P,,OSP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Elias KCZ 476E,359857082042854,GT06E,2020-08-09,2022-12-16,0110941187,,2040-08-10,2040-08-10,,Default Group,89254021164224352993,639021162435299,Elias Baya,,KCZ 476E,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Lawrence Kijogi/ROLL-KCY 080X,359857082044280,GT06E,2020-07-13,2022-12-16,0708155933,,2040-07-13,2040-07-13,,Default Group,89254029851005131222,639029850513122,Lawrence Kijogi,,KCY 080X,,ROLLOUT,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Amani Kazungu/ISP-KCY 084X,359857082037185,GT06E,2020-07-13,2022-12-16,0757338522,,2040-07-14,2040-07-14,,Default Group,89254021154287000597,639021158700059,Amani Kazungu,,KCY 084X,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Joseph kabandi-KCY 076X,359857082046145,GT06E,2020-07-13,2022-12-16,0110850007,,2040-07-14,2040-07-14,,Default Group,89254021164223447158,639021162344715,Joseph Kabandi,,KCY 076X,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Rashid Musa-KCY 090X,359857082040981,GT06E,2020-07-13,2022-12-16,0793375853,,2040-07-14,2040-07-14,,Default Group,89254021064168004164,639021066800416,Amani Sulubu,,KCY 090X,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Wilfred/Gen-KCU 729C_Track,359857082038977,GT06E,2020-04-05,2022-12-16,0110094469,,2040-04-06,2040-04-06,,Default Group,89254021164215938057,639021161593805,Wilfred Kinyanjui,,KCU 729C,,GENERAL,,,,Crane,, +Fireside_MSA,Fireside Group MSA,Amani Kazungu/ISP-KCQ 215F_Track,359857081886467,GT06E,2019-06-30,2022-12-16,0746763076,,2039-07-01,2039-07-01,,Default Group,89254021084186499865,639021088649986,Gideon Kiprono,,KCQ 215F,,ISP,,,,Probox,, +Fireside_MSA,Fireside Group MSA, Kennedy Chege/OSP-KCQ 618K,359857081886905,GT06E,2019-06-30,2022-12-16,0746763132,,2039-07-01,2039-07-01,,Default Group,89254021084186499923,639021088649992,Kennedy Chege,,KCQ 618K,,OSP,,,,Probox,, +Fireside_MSA,Fireside Group MSA,Ndegwa Duncan/PM-KCG 669W_Track,359857081887192,GT06E,2019-06-15,2022-12-16,0746760191,,2039-06-16,2039-06-16,,Default Group,89254021084186499501,639021088649950,Ndegwa Dancun,,KCG 669W,,OSP,,,,Probox,, \ No newline at end of file diff --git a/import_drivers_csv.py b/import_drivers_csv.py new file mode 100644 index 0000000..5756730 --- /dev/null +++ b/import_drivers_csv.py @@ -0,0 +1,232 @@ +""" +import_drivers_csv.py — Fireside Communications · Driver & Vehicle CSV Import +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +One-shot script: reads 20260414_FS__Logistics - final_fixed.csv, compares +each row against the current tracksolid.devices values, and updates the DB. + +Usage: + # Dry-run — shows diff, writes nothing + python import_drivers_csv.py + + # Filter to a single IMEI (dry-run) + python import_drivers_csv.py --imei 862798052707896 + + # Apply all changes to DB + python import_drivers_csv.py --apply + + # Only fill fields that are currently NULL in the DB (never overwrite) + python import_drivers_csv.py --only-null --apply + +Pre-requisite: + Migration 06 must be applied first (adds assigned_city / cost_centre columns). +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +""" + +import argparse +import csv +import os +import sys +import time +from datetime import date +from pathlib import Path + +from ts_shared_rev import clean, clean_num, clean_ts, get_conn, get_logger + +log = get_logger("csv_import") + +CSV_PATH = Path(__file__).parent / "20260414_FS__Logistics - final_fixed.csv" + +# Columns fetched from DB for comparison +DB_COLS = [ + "imei", "driver_name", "driver_phone", "vehicle_number", "vehicle_name", + "vehicle_models", "cost_centre", "sim", "iccid", "imsi", "mc_type", + "activation_time", "expiration", "device_name", "assigned_city", +] + +# Driver Name values that are placeholders — skip writing driver_name for these +_DRIVER_SKIP = {"identification", "ug"} + + +def _infer_city(plate: str) -> str | None: + """Derive assigned_city from license plate prefix.""" + p = (plate or "").strip().upper() + if p.startswith("UMA") or p.startswith("UAG"): + return "KLA" + if p.startswith("K"): + return "NBO" + return None + + +def _clean_date(v: str) -> str | None: + """Accept YYYY-MM-DD and return as ISO string suitable for TIMESTAMPTZ cast.""" + s = (v or "").strip() + if not s: + return None + try: + date.fromisoformat(s) + return s + except ValueError: + return None + + +def load_csv() -> dict[str, dict]: + """Load CSV into a dict keyed by IMEI.""" + rows: dict[str, dict] = {} + with open(CSV_PATH, encoding="utf-8-sig", newline="") as f: + for row in csv.DictReader(f): + imei = (row.get("IMEI") or "").strip() + if not imei: + continue + rows[imei] = row + log.info("CSV loaded: %d rows from %s", len(rows), CSV_PATH.name) + return rows + + +def load_db_devices() -> dict[str, dict]: + """Fetch current device rows from DB, keyed by IMEI.""" + devices: dict[str, dict] = {} + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute(f"SELECT {', '.join(DB_COLS)} FROM tracksolid.devices") + col_names = [d[0] for d in cur.description] + for row in cur.fetchall(): + rec = dict(zip(col_names, row)) + devices[rec["imei"]] = rec + log.info("DB loaded: %d devices", len(devices)) + return devices + + +def build_update(csv_row: dict, db_row: dict | None, only_null: bool) -> dict[str, object]: + """ + Return a dict of column→new_value for fields that need updating. + When only_null=True, skip any DB column that already has a value. + The driver_name column is skipped for placeholder-labelled devices. + """ + driver_raw = clean(csv_row.get("Driver Name")) or "" + plate = clean(csv_row.get("License Plate No.")) or "" + is_placeholder = driver_raw.lower() in _DRIVER_SKIP + skip_row = driver_raw.lower() == "identification" + + if skip_row: + return {} + + proposed: dict[str, object] = { + "vehicle_number": clean(plate), + "vehicle_name": clean(plate), + "vehicle_models": clean(csv_row.get("Vehicle Model")), + "cost_centre": clean(csv_row.get("Department")), + "sim": clean(csv_row.get("SIM")), + "iccid": clean(csv_row.get("ICCID")), + "imsi": clean(csv_row.get("IMSI")), + "mc_type": clean(csv_row.get("Model")), + "activation_time": _clean_date(csv_row.get("Activated Date", "")), + "expiration": _clean_date(csv_row.get("Subscription Expiration", "")), + "driver_phone": clean(csv_row.get("Telephone")), + "assigned_city": _infer_city(plate), + } + if not is_placeholder: + proposed["driver_name"] = driver_raw or None + + # Drop None values — no point sending a NULL to overwrite another NULL + proposed = {k: v for k, v in proposed.items() if v is not None} + + if not only_null or db_row is None: + return proposed + + # only_null: drop any column that already has a non-null value in the DB + return { + k: v for k, v in proposed.items() + if db_row.get(k) is None + } + + +def print_diff(imei: str, updates: dict[str, object], db_row: dict | None) -> None: + """Pretty-print what will change for one device.""" + if not updates: + return + db = db_row or {} + print(f"\n IMEI {imei}:") + for col, new_val in sorted(updates.items()): + old_val = db.get(col) + if old_val != new_val: + print(f" {col:<20} {str(old_val):<30} → {new_val}") + + +def run(apply: bool, only_null: bool, filter_imei: str | None) -> None: + csv_rows = load_csv() + db_rows = load_db_devices() + + if filter_imei: + csv_rows = {k: v for k, v in csv_rows.items() if k == filter_imei} + if not csv_rows: + print(f"IMEI {filter_imei} not found in CSV.") + return + + updated = skipped = no_change = not_in_db = 0 + + with get_conn() as conn: + with conn.cursor() as cur: + for imei, csv_row in csv_rows.items(): + db_row = db_rows.get(imei) + + updates = build_update(csv_row, db_row, only_null) + + if not updates: + # Either an "Identification" placeholder or nothing to change + driver_raw = (csv_row.get("Driver Name") or "").strip().lower() + if driver_raw == "identification": + skipped += 1 + else: + no_change += 1 + continue + + if db_row is None: + not_in_db += 1 + log.warning("IMEI %s in CSV but NOT in DB — skipping.", imei) + continue + + print_diff(imei, updates, db_row) + + if apply: + set_clauses = [] + params = [] + for col, val in updates.items(): + if col in ("activation_time", "expiration"): + set_clauses.append(f"{col} = COALESCE(%s::TIMESTAMPTZ, {col})") + else: + set_clauses.append( + f"{col} = COALESCE(NULLIF(%s, ''), {col})" + ) + params.append(str(val) if val is not None else None) + + set_clauses.append("updated_at = NOW()") + params.append(imei) + + cur.execute( + f"UPDATE tracksolid.devices SET {', '.join(set_clauses)} WHERE imei = %s", + params, + ) + updated += 1 + else: + updated += 1 # count as "would update" in dry-run + + mode = "APPLIED" if apply else "DRY-RUN" + print(f"\n{'='*60}") + print(f" {mode} COMPLETE") + print(f"{'='*60}") + print(f" Would update / updated : {updated}") + print(f" No change needed : {no_change}") + print(f" Skipped (Identification): {skipped}") + print(f" IMEI not in DB : {not_in_db}") + if not apply: + print("\n Run with --apply to commit changes.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Import driver/vehicle details from CSV into tracksolid.devices") + parser.add_argument("--apply", action="store_true", help="Write changes to DB (default: dry-run)") + parser.add_argument("--only-null", action="store_true", help="Only update fields currently NULL in the DB") + parser.add_argument("--imei", default=None, help="Limit to a single IMEI") + args = parser.parse_args() + + run(apply=args.apply, only_null=args.only_null, filter_imei=args.imei)