136 lines
4.7 KiB
PHP
136 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1\Admin;
|
|
|
|
use App\Http\Controllers\Api\V1\ApiController;
|
|
use App\Models\Trip;
|
|
use App\Models\Truck;
|
|
use App\Models\TruckLocationHistory;
|
|
use App\Services\LiveTracking\TruckTracker;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
|
|
class AdminLiveTrackingController extends ApiController
|
|
{
|
|
public function __construct(private readonly TruckTracker $tracker) {}
|
|
|
|
public function trucks(): JsonResponse
|
|
{
|
|
return $this->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();
|
|
}
|
|
}
|