211 lines
7.7 KiB
PHP
211 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1\Driver;
|
|
|
|
use App\Http\Controllers\Api\V1\ApiController;
|
|
use App\Http\Resources\TripResource;
|
|
use App\Models\Trip;
|
|
use App\Models\TripStop;
|
|
use App\Services\Trip\TripExecutor;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
|
|
class DriverTripController extends ApiController
|
|
{
|
|
public function __construct(private readonly TripExecutor $executor) {}
|
|
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$user = $request->user();
|
|
$trips = Trip::query()
|
|
->with(['route', 'team', 'truck', 'dumpsite'])
|
|
->whereIn('status', [Trip::STATUS_SCHEDULED, Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE])
|
|
->whereHas('team', fn ($q) => $q->where('driver_id', $user->id))
|
|
->orderBy('scheduled_date')
|
|
->get();
|
|
|
|
return $this->ok(TripResource::collection($trips));
|
|
}
|
|
|
|
public function show(Request $request, Trip $trip): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$trip->load(['route', 'team', 'truck', 'dumpsite', 'stops.dropOffPoint', 'timelineEvents.recordedBy']);
|
|
|
|
return $this->ok(new TripResource($trip));
|
|
}
|
|
|
|
public function start(Request $request, Trip $trip): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$data = $request->validate([
|
|
'lat' => ['nullable', 'numeric', 'between:-90,90'],
|
|
'lng' => ['nullable', 'numeric', 'between:-180,180'],
|
|
]);
|
|
|
|
try {
|
|
$trip = $this->executor->start($trip, $request->user(), $data['lat'] ?? null, $data['lng'] ?? null);
|
|
} catch (\DomainException $e) {
|
|
return $this->fail($e->getMessage(), null, 422);
|
|
}
|
|
|
|
return $this->ok(new TripResource($trip->load(['route', 'team', 'stops.dropOffPoint'])), 'Trip started');
|
|
}
|
|
|
|
public function arrive(Request $request, Trip $trip, TripStop $stop): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$this->ensureStopBelongs($stop, $trip);
|
|
$data = $request->validate([
|
|
'lat' => ['required', 'numeric', 'between:-90,90'],
|
|
'lng' => ['required', 'numeric', 'between:-180,180'],
|
|
]);
|
|
|
|
try {
|
|
$stop = $this->executor->arriveAtStop($stop, $request->user(), (float) $data['lat'], (float) $data['lng']);
|
|
} catch (\DomainException $e) {
|
|
return $this->fail($e->getMessage(), null, 422);
|
|
}
|
|
|
|
return $this->ok(['stop' => $stop], 'Arrived at stop');
|
|
}
|
|
|
|
public function depart(Request $request, Trip $trip, TripStop $stop): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$this->ensureStopBelongs($stop, $trip);
|
|
$data = $request->validate([
|
|
'lat' => ['required', 'numeric'],
|
|
'lng' => ['required', 'numeric'],
|
|
]);
|
|
|
|
try {
|
|
$stop = $this->executor->departStop($stop, $request->user(), (float) $data['lat'], (float) $data['lng']);
|
|
} catch (\DomainException $e) {
|
|
return $this->fail($e->getMessage(), null, 422);
|
|
}
|
|
|
|
return $this->ok(['stop' => $stop], 'Departed stop');
|
|
}
|
|
|
|
public function skip(Request $request, Trip $trip, TripStop $stop): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$this->ensureStopBelongs($stop, $trip);
|
|
$data = $request->validate([
|
|
'reason' => ['required', 'string', 'min:3', 'max:255'],
|
|
]);
|
|
|
|
$stop = $this->executor->skipStop($stop, $request->user(), $data['reason']);
|
|
|
|
return $this->ok(['stop' => $stop], 'Stop skipped');
|
|
}
|
|
|
|
public function reportIncident(Request $request, Trip $trip): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$data = $request->validate([
|
|
'kind' => ['required', 'in:incident,breakdown'],
|
|
'notes' => ['required', 'string', 'min:3', 'max:1000'],
|
|
'lat' => ['nullable', 'numeric'],
|
|
'lng' => ['nullable', 'numeric'],
|
|
]);
|
|
|
|
$event = $this->executor->reportIncident(
|
|
$trip, $request->user(), $data['kind'], $data['notes'],
|
|
$data['lat'] ?? null, $data['lng'] ?? null,
|
|
);
|
|
|
|
return $this->created(['event' => $event], 'Incident logged');
|
|
}
|
|
|
|
public function arriveDumpsite(Request $request, Trip $trip): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$data = $request->validate([
|
|
'lat' => ['required', 'numeric', 'between:-90,90'],
|
|
'lng' => ['required', 'numeric', 'between:-180,180'],
|
|
'override_geofence' => ['nullable', 'boolean'],
|
|
]);
|
|
|
|
// If the trip's dumpsite has a boundary configured, enforce that
|
|
// the driver's GPS is inside it — unless they explicitly override
|
|
// (e.g., GPS is reading wrong; admin can audit later).
|
|
$trip->loadMissing('dumpsite');
|
|
$dumpsite = $trip->dumpsite;
|
|
if ($dumpsite && $dumpsite->boundary_polygon && empty($data['override_geofence'])) {
|
|
$inside = $dumpsite->containsPoint((float) $data['lat'], (float) $data['lng']);
|
|
if (! $inside) {
|
|
return $this->fail(
|
|
"GPS is outside the {$dumpsite->name} geofence. Pass override_geofence: true if the reading is wrong.",
|
|
['geofence' => ['outside']],
|
|
422,
|
|
);
|
|
}
|
|
}
|
|
|
|
try {
|
|
$trip = $this->executor->arriveAtDumpsite($trip, $request->user(), (float) $data['lat'], (float) $data['lng']);
|
|
} catch (\DomainException $e) {
|
|
return $this->fail($e->getMessage(), null, 422);
|
|
}
|
|
|
|
return $this->ok(new TripResource($trip), 'Arrived at dumpsite');
|
|
}
|
|
|
|
public function releaseLoad(Request $request, Trip $trip): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$data = $request->validate([
|
|
'weight_kg' => ['required', 'integer', 'min:0'],
|
|
'gate_pass_number' => ['nullable', 'string', 'max:100'],
|
|
'dumpsite_attendant_name' => ['nullable', 'string', 'max:191'],
|
|
'photo_evidence_path' => ['nullable', 'string', 'max:255'],
|
|
'waste_type_breakdown' => ['nullable', 'array'],
|
|
'lat' => ['nullable', 'numeric'],
|
|
'lng' => ['nullable', 'numeric'],
|
|
'notes' => ['nullable', 'string', 'max:1000'],
|
|
]);
|
|
|
|
try {
|
|
$release = $this->executor->releaseLoad($trip, $request->user(), $data);
|
|
} catch (\DomainException $e) {
|
|
return $this->fail($e->getMessage(), null, 422);
|
|
}
|
|
|
|
return $this->created(['release' => $release], 'Load released');
|
|
}
|
|
|
|
public function complete(Request $request, Trip $trip): JsonResponse
|
|
{
|
|
$this->authorizeDriver($request, $trip);
|
|
$data = $request->validate([
|
|
'lat' => ['nullable', 'numeric'],
|
|
'lng' => ['nullable', 'numeric'],
|
|
]);
|
|
|
|
try {
|
|
$trip = $this->executor->complete($trip, $request->user(), $data['lat'] ?? null, $data['lng'] ?? null);
|
|
} catch (\DomainException $e) {
|
|
return $this->fail($e->getMessage(), null, 422);
|
|
}
|
|
|
|
return $this->ok(new TripResource($trip->load(['route', 'team', 'stops'])), 'Trip completed');
|
|
}
|
|
|
|
private function authorizeDriver(Request $request, Trip $trip): void
|
|
{
|
|
$trip->loadMissing('team');
|
|
if ($trip->team?->driver_id !== $request->user()->id) {
|
|
abort(403, 'You are not the driver for this trip');
|
|
}
|
|
}
|
|
|
|
private function ensureStopBelongs(TripStop $stop, Trip $trip): void
|
|
{
|
|
if ($stop->trip_id !== $trip->id) {
|
|
abort(404, 'Stop not found in this trip');
|
|
}
|
|
}
|
|
}
|