| 0% | Vehicle did not move today | Verify not broken down or abandoned |
> **Note:** The denominator (10 hours) should be adjusted to match your actual contractual shift length.
---
### 2.2 Daily Revenue-Generating Hours vs Fuel-Wasting Idle
Engine-on-but-stationary time is direct cost with no output. At Kenya diesel prices (~KES 180/litre) and typical 8 L/100 km consumption, a stationary diesel engine burns approximately **0.8 L/hour** at idle.
### 3.2 Harsh Driving — Hard Braking and Sudden Acceleration
Requires `track_list` data (POLL-01). Identifies speed changes greater than 30 km/h within a 60-second window — the signature of hard braking or sudden acceleration. Both events cause tyre wear, brake wear, fuel spikes, and increase accident probability.
```sql
WITH ordered 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 '7 days'
AND gps_time <NOW()
)
SELECT
imei,
gps_time AT TIME ZONE 'Africa/Nairobi' AS event_time,
prev_speed AS speed_before,
speed AS speed_after,
ABS(speed - prev_speed) AS delta_kmh,
CASE
WHEN speed > prev_speed THEN 'hard_acceleration'
ELSE 'hard_braking'
END AS event_type
FROM ordered
WHERE ABS(speed - prev_speed) > 30
AND EXTRACT(EPOCH FROM (gps_time - prev_time)) BETWEEN 5 AND 60
ORDER BY event_time DESC;
```
**Driver aggression index** — normalised harsh events per 100 km:
```sql
WITH harsh AS (
SELECT
imei,
COUNT(*) AS harsh_events
FROM (
SELECT
imei,
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,
gps_time
FROM tracksolid.position_history
WHERE source = 'track_list'
AND gps_time > NOW() - INTERVAL '30 days'
) sub
WHERE ABS(speed - prev_speed) > 30
AND EXTRACT(EPOCH FROM (gps_time - prev_time)) BETWEEN 5 AND 60
GROUP BY imei
),
km AS (
SELECT imei, SUM(distance_km) AS total_km
FROM tracksolid.trips
WHERE start_time > NOW() - INTERVAL '30 days'
GROUP BY imei
)
SELECT
h.imei,
d.driver_name,
d.vehicle_number,
h.harsh_events,
ROUND(k.total_km, 0) AS km_driven,
ROUND(h.harsh_events / NULLIF(k.total_km, 0) * 100, 2) AS aggression_index
FROM harsh h
JOIN km k ON k.imei = h.imei
JOIN tracksolid.devices d ON d.imei = h.imei
ORDER BY aggression_index DESC;
```
> An aggression index below **0.5** is good. Above **2.0** warrants a driver coaching conversation. Above **5.0** is a safety concern.
---
### 3.3 Tardiness — Late Starts and Early Knock-Off
**Late starts** (first ignition-on after scheduled shift start):
```sql
SELECT
f.vehicle_key AS imei,
d.driver_name,
d.vehicle_number,
f.day,
f.day_start_time,
CASE
WHEN f.day_start_time > '07:45:00' THEN
EXTRACT(EPOCH FROM (f.day_start_time - '07:30:00'::TIME)) / 60
ELSE 0
END::INT AS minutes_late
FROM dwh_gold.fact_daily_fleet_metrics f
JOIN tracksolid.devices d ON d.imei = f.vehicle_key
WHERE f.day >= CURRENT_DATE - INTERVAL '30 days'
AND f.day_start_time > '07:45:00'
ORDER BY minutes_late DESC;
```
**Early knock-off** (last trip ended before scheduled shift end):
```sql
SELECT
f.vehicle_key AS imei,
d.driver_name,
f.day,
f.day_end_time,
CASE
WHEN f.day_end_time < '17:00:00' THEN
EXTRACT(EPOCH FROM ('17:00:00'::TIME - f.day_end_time)) / 60
ELSE 0
END::INT AS minutes_early
FROM dwh_gold.fact_daily_fleet_metrics f
JOIN tracksolid.devices d ON d.imei = f.vehicle_key
WHERE f.day >= CURRENT_DATE - INTERVAL '30 days'
AND f.day_end_time < '17:00:00'
AND f.total_trips > 0
ORDER BY minutes_early DESC;
```
> Adjust `'07:30:00'` and `'17:00:00'` to match your actual contracted shift times.
**Chronic late starters — monthly pattern:**
```sql
SELECT
f.vehicle_key AS imei,
d.driver_name,
COUNT(*) AS late_days,
ROUND(AVG(
EXTRACT(EPOCH FROM (f.day_start_time - '07:30:00'::TIME)) / 60
), 0) AS avg_minutes_late
FROM dwh_gold.fact_daily_fleet_metrics f
JOIN tracksolid.devices d ON d.imei = f.vehicle_key
WHERE f.day >= DATE_TRUNC('month', CURRENT_DATE)
AND f.day_start_time > '07:45:00'
GROUP BY f.vehicle_key, d.driver_name
HAVING COUNT(*) >= 3
ORDER BY late_days DESC, avg_minutes_late DESC;
```
---
### 3.4 After-Hours Movement
Any trip starting or ending outside contracted hours. Flags unauthorised vehicle use, night deliveries not on schedule, or potential vehicle theft.
```sql
SELECT
t.imei,
d.driver_name,
d.vehicle_number,
t.start_time AT TIME ZONE 'Africa/Nairobi' AS departure_nairobi,
t.end_time AT TIME ZONE 'Africa/Nairobi' AS arrival_nairobi,
ROUND(t.distance_km::numeric, 1) AS distance_km,
CASE
WHEN EXTRACT(HOUR FROM t.start_time AT TIME ZONE 'Africa/Nairobi') <6THEN'pre-dawndeparture'
WHEN EXTRACT(HOUR FROM t.start_time AT TIME ZONE 'Africa/Nairobi') >= 20 THEN 'night departure'
ELSE 'after-hours return'
END AS flag
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE (
EXTRACT(HOUR FROM t.start_time AT TIME ZONE 'Africa/Nairobi') <6
OR EXTRACT(HOUR FROM t.start_time AT TIME ZONE 'Africa/Nairobi') >= 20
OR EXTRACT(HOUR FROM t.end_time AT TIME ZONE 'Africa/Nairobi') >= 21
)
AND t.start_time > NOW() - INTERVAL '30 days'
ORDER BY t.start_time DESC;
```
---
### 3.5 Km Covered per Driver per Day
```sql
SELECT
t.imei,
d.driver_name,
d.vehicle_number,
DATE(t.start_time AT TIME ZONE 'Africa/Nairobi') AS working_day,
ROUND(SUM(t.distance_km)::numeric, 1) AS km_driven,
COUNT(*) AS trips,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
MAX(t.max_speed_kmh) AS peak_speed_kmh,
MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::TIME AS first_departure,
MAX(t.end_time AT TIME ZONE 'Africa/Nairobi')::TIME AS last_return
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE t.start_time >= CURRENT_DATE AT TIME ZONE 'Africa/Nairobi'
AND t.end_time IS NOT NULL
GROUP BY t.imei, d.driver_name, d.vehicle_number, working_day
ORDER BY km_driven DESC;
```
**Expected daily km benchmarks by vehicle type:**
| Vehicle Type | Expected Daily km | Flag: Below | Flag: Above |
|---|---|---|---|
| Urban delivery van | 80–150 km | <40km|> 300 km |
| Long-haul truck | 300–500 km | <150km|> 700 km |
| Field/supervisor vehicle | 50–120 km | <20km|> 250 km |
| Motorcycle courier | 60–120 km | <30km|> 200 km |
A driver consistently covering 250 km/day in an urban van either has a legitimately large route or is running personal errands between jobs. Both scenarios need different responses.
**Weekly km trend per driver:**
```sql
SELECT
t.imei,
d.driver_name,
DATE_TRUNC('week', t.start_time AT TIME ZONE 'Africa/Nairobi') AS week_start,
ROUND(SUM(t.distance_km)::numeric, 1) AS total_km,
COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')) AS days_active,
ROUND(SUM(t.distance_km)::numeric /
NULLIF(COUNT(DISTINCT DATE(t.start_time AT TIME ZONE 'Africa/Nairobi')), 0), 1
) AS avg_km_per_day
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE t.start_time > NOW() - INTERVAL '90 days'
AND t.end_time IS NOT NULL
GROUP BY t.imei, d.driver_name, week_start
ORDER BY t.imei, week_start;
```
---
## 4. Real-Time Dispatch — Nearest Vehicle to Job
### 4.1 Find the 5 Closest Available Vehicles
Given a new job at a known location, this query returns the nearest active vehicles with a fresh GPS fix. Runs in milliseconds against the `live_positions` table with the PostGIS spatial index.
```sql
-- Replace :job_lat and :job_lng with the job coordinates
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_seen
FROM tracksolid.live_positions lp
JOIN tracksolid.devices d ON d.imei = lp.imei
WHERE lp.acc_status = '1'
AND lp.speed <5
AND lp.gps_time > NOW() - INTERVAL '5 minutes'
ORDER BY distance_km ASC
LIMIT 5;
```
**ETA speed assumptions** — adjust the divisor to match the route type:
| Route type | Speed (km/h) | Formula |
|---|---|---|
| Nairobi CBD | 20 km/h | `/ 20.0 * 60` |
| Nairobi urban | 30 km/h | `/ 30.0 * 60` |
| Peri-urban | 50 km/h | `/ 50.0 * 60` |
| Highway | 80 km/h | `/ 80.0 * 60` |
---
### 4.2 Dispatch Logic for n8n or API Integration
The recommended workflow when a new job/ticket arrives:
1.**Trigger:** New job created (webhook from job management system or n8n)
2.**Force-refresh positions:** Call `get_device_locations()` for the top 10 candidate IMEIs to get sub-second fresh positions before committing
3.**Run dispatch query** above with job coordinates
4.**Filter by vehicle type** if the job requires specific capacity (`AND d.vehicle_category = 'van'`)
5.**Exclude vehicles with open alarms:**`AND NOT EXISTS (SELECT 1 FROM tracksolid.alarms a WHERE a.imei = lp.imei AND a.alarm_time > NOW() - INTERVAL '1 hour')`
6.**Present top 3 candidates** to dispatcher (or auto-assign #1 if fully automated)
7.**Log dispatch decision** to a separate `dispatch_log` table for SLA tracking
---
### 4.3 All Active Vehicles Map — Live Fleet View
Returns all vehicles with a position fix in the last 10 minutes, suitable for a Grafana Geomap panel with auto-refresh at 30 seconds.
```sql
SELECT
lp.imei,
COALESCE(d.vehicle_name, d.vehicle_number, lp.imei) AS label,
d.driver_name,
lp.lat,
lp.lng,
lp.speed,
lp.acc_status,
CASE
WHEN lp.speed > 5 THEN 'moving'
WHEN lp.acc_status = '1' THEN 'idle'
ELSE 'parked'
END AS vehicle_state,
lp.gps_time AT TIME ZONE 'Africa/Nairobi' AS last_seen
FROM tracksolid.live_positions lp
JOIN tracksolid.devices d ON d.imei = lp.imei
WHERE lp.gps_time > NOW() - INTERVAL '10 minutes'
ORDER BY lp.imei;
```
---
## 5. Distance per Driver per Day
### 5.1 Today's Summary
```sql
SELECT
t.imei,
COALESCE(d.driver_name, 'Unassigned') AS driver,
COALESCE(d.vehicle_number, t.imei) AS vehicle,
ROUND(SUM(t.distance_km)::numeric, 1) AS km_today,
COUNT(*) AS trips_today,
ROUND(SUM(t.driving_time_s) / 3600.0, 2) AS drive_hours,
ROUND(SUM(t.idle_time_s) / 3600.0, 2) AS idle_hours,
MIN(t.start_time AT TIME ZONE 'Africa/Nairobi')::TIME AS first_departure,
MAX(t.end_time AT TIME ZONE 'Africa/Nairobi')::TIME AS last_return
FROM tracksolid.trips t
JOIN tracksolid.devices d ON d.imei = t.imei
WHERE t.start_time >= CURRENT_DATE AT TIME ZONE 'Africa/Nairobi'
AND t.end_time IS NOT NULL
GROUP BY t.imei, d.driver_name, d.vehicle_number
ORDER BY km_today DESC;
```
### 5.2 30-Day Driver Performance Scorecard
Combines distance, behaviour, and punctuality into a single view per driver.
```sql
WITH km_summary AS (
SELECT
imei,
COUNT(DISTINCT DATE(start_time AT TIME ZONE 'Africa/Nairobi')) AS days_active,
ROUND(SUM(distance_km)::numeric, 1) AS total_km,
ROUND(AVG(distance_km)::numeric, 1) AS avg_km_per_trip,
MAX(max_speed_kmh) AS peak_speed
FROM tracksolid.trips
WHERE start_time > NOW() - INTERVAL '30 days'
AND end_time IS NOT NULL
GROUP BY imei
),
alarm_summary AS (
SELECT imei, COUNT(*) AS alarm_count
FROM tracksolid.alarms
WHERE alarm_time > NOW() - INTERVAL '30 days'
GROUP BY imei
),
late_summary AS (
SELECT vehicle_key AS imei, COUNT(*) AS late_days
FROM dwh_gold.fact_daily_fleet_metrics
WHERE day > CURRENT_DATE - 30
AND day_start_time > '07:45:00'
GROUP BY vehicle_key
)
SELECT
k.imei,
d.driver_name,
d.vehicle_number,
k.days_active,
k.total_km,
ROUND(k.total_km / NULLIF(k.days_active, 0), 1) AS avg_km_per_day,
k.peak_speed AS peak_speed_kmh,
COALESCE(a.alarm_count, 0) AS alarms_30d,
COALESCE(l.late_days, 0) AS late_starts_30d
FROM km_summary k
JOIN tracksolid.devices d ON d.imei = k.imei
LEFT JOIN alarm_summary a ON a.imei = k.imei
LEFT JOIN late_summary l ON l.imei = k.imei
ORDER BY k.total_km DESC;
```
---
## 6. Business Questions Now Answerable
| Business Question | Primary Data Source | Confidence |
|---|---|---|
| Which vehicles are moving right now? | `live_positions` | High |
| Who started work latest today? | `fact_daily_fleet_metrics.day_start_time` | High |
| Who drove the most km this week? | `trips` + `devices` | High |
| Which vehicle spent the most time idling? | `trips.idle_time_s` | High |
| How much fuel was wasted on idle today? | `trips.idle_time_s`× est. rate | Medium (needs `fuel_100km` set) |
| Which driver triggered the most alarms this month? | `alarms` + `devices` | High |
| What is total fleet distance this month? | `trips` | High |
| Which vehicles did not move at all today? | `trips` LEFT JOIN `devices` | High |
| Who is nearest to a new job right now? | `live_positions` + PostGIS | High |
| Did any vehicle leave depot after hours? | `trips` time filter | High |
| What is the speeding rate per driver per week? | `position_history` speed filter | High |
| Which driver has the harshest driving style? | `position_history` delta query | High (needs 1–2 weeks of `track_list` data to accumulate) |
| Are vehicles on approved routes? | `position_history` + `geofences` | Low (pending geofence population) |
| Is cold chain in temperature range? | `temperature_readings` | Low (pending webhook registration) |
| How much fuel is consumed per route? | `fuel_readings` + `trips` | Low (pending fuel sensor webhook) |
| What is the real odometer per vehicle? | `live_positions.current_mileage` | Medium (depends on tracker calibration) |
| How many km to next service interval? | `live_positions.current_mileage` - last service | Open (requires service log) |
| Did any vehicle enter a restricted zone? | `alarms` (geofence type) + `geofences` | Low (pending geofence setup) |
| Which drivers are consistently late on Mondays? | `fact_daily_fleet_metrics` day-of-week filter | High |
| What percentage of the fleet was utilised today? | `trips` + `devices` count | High |