Files
Verde-Web/app/Http/Controllers/Api/V1/Admin/AdminLiveTrackingController.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();
}
}