From 1ee4a049dc68bb7fdbe3964deb176d787cdba55a Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 2 Jul 2026 15:01:44 +0800 Subject: [PATCH] feat(trips): live mid-trip truck load input with Pusher broadcast - Migration: add current_load_kg (nullable unsigned int) to trips table - Trip model: add current_load_kg to fillable + integer cast - New TruckLoadBroadcast event (ShouldBroadcastNow) - Channel: private admin.live - Event name: truck.load - Payload: trip_id, team_id, truck_id, current_load_kg, capacity_kg, is_full - DriverTripController::updateLoad() - PATCH /api/v1/driver/trips/{trip}/update-load - Validates status is in_progress, saves current_load_kg, fires broadcast - Route: PATCH driver/trips/{trip}/update-load registered - CollectionTeamResource: truck_is_full uses current_load_kg (live estimate) with fallback to total_load_kg (authoritative dumpsite weight) - TripResource: expose current_load_kg field - Admin Teams card: - data-team-id attribute added to each card for Echo targeting - Load bar uses current_load_kg when available - Echo listener for truck.load event patches load bar + badge in real-time - truck-status-badge class added to Collecting/Truck Full spans --- app/Events/TruckLoadBroadcast.php | 44 +++++++++++++++++ .../Api/V1/Driver/DriverTripController.php | 22 +++++++++ app/Http/Resources/CollectionTeamResource.php | 14 +++--- app/Models/Trip.php | 3 +- ...118_add_current_load_kg_to_trips_table.php | 23 +++++++++ resources/views/admin/teams.blade.php | 47 +++++++++++++++++-- routes/api.php | 1 + 7 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 app/Events/TruckLoadBroadcast.php create mode 100644 database/migrations/2026_07_02_064118_add_current_load_kg_to_trips_table.php diff --git a/app/Events/TruckLoadBroadcast.php b/app/Events/TruckLoadBroadcast.php new file mode 100644 index 0000000..a73b065 --- /dev/null +++ b/app/Events/TruckLoadBroadcast.php @@ -0,0 +1,44 @@ +trip; + $capacityKg = $trip->truck?->capacity_kg; + $currentLoad = $trip->current_load_kg ?? 0; + + return [ + 'trip_id' => $trip->uuid, + 'team_id' => $trip->team_id, + 'truck_id' => $trip->truck?->uuid, + 'current_load_kg' => $trip->current_load_kg, + 'capacity_kg' => $capacityKg, + 'is_full' => $capacityKg && $currentLoad >= $capacityKg, + 'updated_at' => now()->toIso8601String(), + ]; + } +} diff --git a/app/Http/Controllers/Api/V1/Driver/DriverTripController.php b/app/Http/Controllers/Api/V1/Driver/DriverTripController.php index 504cc0c..e289b09 100644 --- a/app/Http/Controllers/Api/V1/Driver/DriverTripController.php +++ b/app/Http/Controllers/Api/V1/Driver/DriverTripController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\V1\Driver; use App\Http\Controllers\Api\V1\ApiController; use App\Http\Resources\TripResource; +use App\Events\TruckLoadBroadcast; use App\Models\Trip; use App\Models\TripStop; use App\Services\Trip\TripExecutor; @@ -153,6 +154,27 @@ class DriverTripController extends ApiController 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); diff --git a/app/Http/Resources/CollectionTeamResource.php b/app/Http/Resources/CollectionTeamResource.php index 5e3afc5..25ff228 100644 --- a/app/Http/Resources/CollectionTeamResource.php +++ b/app/Http/Resources/CollectionTeamResource.php @@ -30,14 +30,12 @@ class CollectionTeamResource extends JsonResource 'is_full' => $this->is_full, 'truck_is_full' => $this->whenLoaded('currentTrip', function () { $trip = $this->currentTrip; - if (! $trip) { - return false; - } - $atDumpsite = $trip->status === Trip::STATUS_AT_DUMPSITE; - $atCapacity = $this->truck?->capacity_kg > 0 - && $trip->total_load_kg >= $this->truck->capacity_kg; - - return $atDumpsite || $atCapacity; + if (! $trip) return false; + $capacity = $this->truck?->capacity_kg; + // Prefer driver's live estimate; fall back to dumpsite-scale total + $effectiveLoad = $trip->current_load_kg ?? $trip->total_load_kg ?? 0; + return ($capacity && $effectiveLoad >= $capacity) + || $trip->status === 'at_dumpsite'; }, false), 'performance_stats' => $this->getPerformanceStats(), 'current_trip' => TripResource::make($this->whenLoaded('currentTrip')), diff --git a/app/Models/Trip.php b/app/Models/Trip.php index e2f5d25..b2747ad 100644 --- a/app/Models/Trip.php +++ b/app/Models/Trip.php @@ -32,7 +32,7 @@ class Trip extends Model 'scheduled_date', 'scheduled_start_time', 'actual_start_time', 'actual_end_time', 'dumpsite_arrival_time', 'dumpsite_departure_time', - 'status', 'is_detouring', 'total_load_kg', 'notes', 'created_by_admin_id', + 'status', 'is_detouring', 'total_load_kg', 'current_load_kg', 'notes', 'created_by_admin_id', ]; protected function casts(): array @@ -44,6 +44,7 @@ class Trip extends Model 'dumpsite_arrival_time' => 'datetime', 'dumpsite_departure_time' => 'datetime', 'is_detouring' => 'boolean', + 'current_load_kg' => 'integer', ]; } diff --git a/database/migrations/2026_07_02_064118_add_current_load_kg_to_trips_table.php b/database/migrations/2026_07_02_064118_add_current_load_kg_to_trips_table.php new file mode 100644 index 0000000..c1dee91 --- /dev/null +++ b/database/migrations/2026_07_02_064118_add_current_load_kg_to_trips_table.php @@ -0,0 +1,23 @@ +unsignedInteger('current_load_kg')->nullable()->after('total_load_kg'); + }); + } + + public function down(): void + { + Schema::table('trips', function (Blueprint $table) { + $table->dropColumn('current_load_kg'); + }); + } +}; + diff --git a/resources/views/admin/teams.blade.php b/resources/views/admin/teams.blade.php index e5cdab6..975f493 100644 --- a/resources/views/admin/teams.blade.php +++ b/resources/views/admin/teams.blade.php @@ -217,7 +217,7 @@ : 0; return ` -
+

${window.Verde.escapeHtml(t.name)}

@@ -231,10 +231,10 @@
Truck Load - ${stats.current_trip_kg || 0} / ${truck.capacity_kg || 0} KG + ${t.current_trip?.current_load_kg ?? stats.current_trip_kg ?? 0} / ${truck.capacity_kg || 0} KG
-
+
@@ -276,8 +276,8 @@ ? 'Idle' : t.current_trip ? (t.truck_is_full - ? 'Truck Full' - : 'Collecting') + ? 'Truck Full' + : 'Collecting') : 'Ready' }
@@ -433,5 +433,42 @@ loadHelpers(), ]); load(); + + // Real-time truck load updates via Pusher + const echo = window.Verde.getEcho?.(); + if (echo) { + try { + echo.private('admin.live').listen('.truck.load', (payload) => { + const card = cardsGrid.querySelector(`[data-team-id="${payload.team_id}"]`); + if (!card) return; + + const capacityKg = payload.capacity_kg || 0; + const currentKg = payload.current_load_kg ?? 0; + const pct = capacityKg > 0 ? Math.min(Math.round((currentKg / capacityKg) * 100), 100) : 0; + + // Update load bar width + const bar = card.querySelector('.team-load-bar'); + if (bar) bar.style.width = pct + '%'; + + // Update kg label + const label = card.querySelector('.team-load-label'); + if (label) label.textContent = `${currentKg} / ${capacityKg} KG`; + + // Swap truck badge + const truckBadge = card.querySelector('.truck-status-badge'); + if (truckBadge) { + if (payload.is_full) { + truckBadge.className = 'truck-status-badge text-[10px] font-bold text-red-600 bg-red-50 px-1.5 py-0.5 rounded border border-red-200 uppercase'; + truckBadge.textContent = 'Truck Full'; + } else { + truckBadge.className = 'truck-status-badge text-[10px] font-bold text-green-600 bg-green-50 px-1.5 py-0.5 rounded border border-green-200 uppercase'; + truckBadge.textContent = 'Collecting'; + } + } + }); + } catch (e) { + console.warn('Teams Echo listener failed', e); + } + } @endsection diff --git a/routes/api.php b/routes/api.php index 9b3544b..2415ee8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -381,6 +381,7 @@ Route::prefix('driver/trips') Route::post('/{trip}/stops/{stop}/skip', [DriverTripController::class, 'skip'])->name('skip'); Route::post('/{trip}/incident', [DriverTripController::class, 'reportIncident'])->name('incident'); Route::post('/{trip}/arrive-dumpsite', [DriverTripController::class, 'arriveDumpsite'])->name('arrive-dumpsite'); + Route::patch('/{trip}/update-load', [DriverTripController::class, 'updateLoad'])->name('update-load'); Route::post('/{trip}/release-load', [DriverTripController::class, 'releaseLoad'])->name('release-load'); Route::post('/{trip}/complete', [DriverTripController::class, 'complete'])->name('complete'); });