user(); $allowedStatuses = [ Trip::STATUS_SCHEDULED, Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE, Trip::STATUS_COMPLETED, Trip::STATUS_CANCELLED, Trip::STATUS_HANDED_OFF, ]; // When ?status= is provided, filter by those statuses; otherwise default to active if ($request->filled('status')) { $requested = array_filter( explode(',', $request->string('status')->toString()), fn ($s) => in_array($s, $allowedStatuses, true), ); $statuses = count($requested) > 0 ? array_values($requested) : [Trip::STATUS_SCHEDULED, Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]; } else { $statuses = [Trip::STATUS_SCHEDULED, Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]; } $trips = Trip::query() ->with(['route', 'team', 'truck', 'dumpsite', 'stops', 'parentTrip']) ->whereIn('status', $statuses) ->whereHas('team', fn ($q) => $q->where('driver_id', $user->id)) ->orderBy('scheduled_date', 'desc') ->paginate((int) $request->get('per_page', 20)); return $this->ok(TripResource::collection($trips)); } public function show(Request $request, Trip $trip): JsonResponse { $this->authorizeDriver($request, $trip); $trip->load(['route', 'team.driver', 'team.scanner', 'team.helpers.user', '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 updateLoad(Request $request, Trip $trip): JsonResponse { $this->authorizeDriver($request, $trip); $data = $request->validate([ 'current_load_kg' => ['required', 'integer', 'min:0', 'max:100000'], ]); if ($trip->status !== Trip::STATUS_IN_PROGRESS) { return $this->fail('Load can only be updated while a trip is in progress', null, 422); } $trip->forceFill(['current_load_kg' => $data['current_load_kg']])->save(); $trip->loadMissing('truck'); TruckLoadBroadcast::dispatch($trip); return $this->ok([ 'current_load_kg' => $trip->current_load_kg, ], 'Truck load updated'); } 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'); } } }