ok([ 'trucks' => $this->tracker->activeTruckPositions(), 'as_of' => now()->toIso8601String(), ]); } /** * Recent breadcrumb trail for a truck — last N pings within the * given window (default 30 minutes, max 4 hours). Used by the live * map to draw the actual path the truck has taken so far. */ public function trail(Request $request, Truck $truck): JsonResponse { $data = $request->validate([ 'minutes' => ['nullable', 'integer', 'min:1', 'max:240'], 'limit' => ['nullable', 'integer', 'min:1', 'max:1000'], ]); $minutes = (int) ($data['minutes'] ?? 30); $limit = (int) ($data['limit'] ?? 500); $rows = TruckLocationHistory::query() ->where('truck_id', $truck->id) ->where('recorded_at', '>=', now()->subMinutes($minutes)) ->orderBy('recorded_at') ->limit($limit) ->get(['recorded_at', 'coordinates', 'heading_degrees', 'speed_kmh', 'trip_id']); return $this->ok([ 'truck_id' => $truck->uuid, 'window_minutes' => $minutes, 'points' => $rows->map(fn ($r) => [ 'lat' => $r->coordinates->latitude, 'lng' => $r->coordinates->longitude, 'heading_degrees' => $r->heading_degrees, 'speed_kmh' => $r->speed_kmh, 'recorded_at' => $r->recorded_at->toIso8601String(), ])->all(), ]); } /** * Find the truck's currently-running trip with full route + stops + * dumpsite + recent timeline. Powers the click-to-detail panel. */ public function activeTrip(Truck $truck): JsonResponse { $trip = Trip::query() ->whereHas('team', fn ($q) => $q->where('truck_id', $truck->id)) ->whereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]) ->orderByDesc('actual_start_time') ->with([ 'route', 'team', 'dumpsite', 'stops.dropOffPoint', 'timelineEvents' => fn ($q) => $q->latest('event_at')->limit(20), 'timelineEvents.recordedBy', ]) ->first(); if (! $trip) { return $this->ok(['active_trip' => null]); } $stops = $trip->stops->map(fn ($s) => [ 'id' => $s->id, 'sequence' => $s->sequence, 'status' => $s->status, 'name' => $s->dropOffPoint?->name, 'lat' => $s->dropOffPoint?->coordinates?->latitude, 'lng' => $s->dropOffPoint?->coordinates?->longitude, 'actual_arrival' => $s->actual_arrival?->toIso8601String(), ]); $dumpsite = $trip->dumpsite ? [ 'name' => $trip->dumpsite->name, 'lat' => $trip->dumpsite->coordinates?->latitude, 'lng' => $trip->dumpsite->coordinates?->longitude, 'boundary' => $this->dumpsiteBoundary($trip->dumpsite), ] : null; return $this->ok([ 'active_trip' => [ 'id' => $trip->uuid, 'trip_number' => $trip->trip_number, 'status' => $trip->status, 'started_at' => $trip->actual_start_time?->toIso8601String(), 'route_name' => $trip->route?->name, 'team_name' => $trip->team?->name, 'total_load_kg' => $trip->total_load_kg, 'stops' => $stops, 'dumpsite' => $dumpsite, 'timeline' => $trip->timelineEvents->map(fn ($e) => [ 'event_type' => $e->event_type, 'event_at' => $e->event_at->toIso8601String(), 'notes' => $e->notes, ])->values(), ], ]); } private function dumpsiteBoundary($dumpsite): ?array { if (! $dumpsite->boundary_polygon) { return null; } $rings = $dumpsite->boundary_polygon->getGeometries(); $ring = $rings->first(); if (! $ring) { return null; } return $ring->getGeometries() ->map(fn ($p) => ['lat' => $p->latitude, 'lng' => $p->longitude]) ->all(); } }