trips, trip_stops, trip_timeline_events. Admin: schedule/show/cancel. Driver: start, arrive/depart/skip stops, report incident, arrive at dumpsite, release load (creates dumpsite_release row + tallies trip load), complete. Every action writes a typed timeline event with GPS. Trip number auto-generated TRIP-YYYYMMDD-NNN. Promotes dumpsite_releases.trip_id to a real FK now that trips exists. 148 feature tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.4 KiB
PHP
100 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1\Admin;
|
|
|
|
use App\Http\Controllers\Api\V1\ApiController;
|
|
use App\Http\Requests\Trip\StoreTripRequest;
|
|
use App\Http\Resources\TripResource;
|
|
use App\Models\Route;
|
|
use App\Models\Trip;
|
|
use App\Models\TripStop;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class AdminTripController extends ApiController
|
|
{
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'status' => ['nullable', 'in:scheduled,in_progress,at_dumpsite,completed,cancelled'],
|
|
'date' => ['nullable', 'date'],
|
|
'team_id' => ['nullable', 'integer'],
|
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
|
]);
|
|
$perPage = (int) $request->input('per_page', 25);
|
|
|
|
$trips = Trip::query()
|
|
->with(['route', 'team', 'truck', 'dumpsite'])
|
|
->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
|
|
->when($request->filled('date'), fn ($q) => $q->whereDate('scheduled_date', $request->string('date')))
|
|
->when($request->filled('team_id'), fn ($q) => $q->where('team_id', $request->integer('team_id')))
|
|
->orderByDesc('scheduled_date')
|
|
->orderBy('scheduled_start_time')
|
|
->paginate($perPage);
|
|
|
|
return $this->ok(
|
|
TripResource::collection($trips),
|
|
null,
|
|
[
|
|
'page' => $trips->currentPage(),
|
|
'per_page' => $trips->perPage(),
|
|
'total' => $trips->total(),
|
|
'last_page' => $trips->lastPage(),
|
|
],
|
|
);
|
|
}
|
|
|
|
public function store(StoreTripRequest $request): JsonResponse
|
|
{
|
|
$data = $request->validated();
|
|
$route = Route::with('stops.dropOffPoint')->findOrFail($data['route_id']);
|
|
$data['dumpsite_id'] = $data['dumpsite_id'] ?? $route->default_dumpsite_id;
|
|
$data['truck_id'] = $data['truck_id'] ?? null;
|
|
$data['created_by_admin_id'] = $request->user()->id;
|
|
|
|
$trip = DB::transaction(function () use ($data, $route) {
|
|
$trip = Trip::create($data);
|
|
|
|
foreach ($route->stops as $rs) {
|
|
TripStop::create([
|
|
'trip_id' => $trip->id,
|
|
'drop_off_point_id' => $rs->drop_off_point_id,
|
|
'sequence' => $rs->sequence,
|
|
'status' => TripStop::STATUS_PENDING,
|
|
]);
|
|
}
|
|
|
|
return $trip;
|
|
});
|
|
|
|
return $this->created(
|
|
new TripResource(
|
|
$trip->fresh()->load(['route', 'team', 'truck', 'dumpsite', 'stops.dropOffPoint']),
|
|
),
|
|
'Trip scheduled',
|
|
);
|
|
}
|
|
|
|
public function show(Trip $trip): JsonResponse
|
|
{
|
|
$trip->load([
|
|
'route', 'team.driver', 'team.scanner', 'truck', 'dumpsite',
|
|
'stops.dropOffPoint', 'timelineEvents.recordedBy',
|
|
]);
|
|
|
|
return $this->ok(new TripResource($trip));
|
|
}
|
|
|
|
public function cancel(Request $request, Trip $trip): JsonResponse
|
|
{
|
|
if (in_array($trip->status, [Trip::STATUS_COMPLETED, Trip::STATUS_CANCELLED], true)) {
|
|
return $this->fail('Trip is already finished', null, 422);
|
|
}
|
|
|
|
$trip->forceFill(['status' => Trip::STATUS_CANCELLED])->save();
|
|
|
|
return $this->ok(new TripResource($trip->fresh()->load(['route', 'team'])), 'Trip cancelled');
|
|
}
|
|
}
|