From f552bccd1af177a93286448f2f257f4e2e3d99ec Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 1 May 2026 19:49:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(customer-web):=20Phase=203=20=E2=80=94=20c?= =?UTF-8?q?odes=20/=20pickups=20/=20history=20/=20live=20tracker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - New GET /api/v1/me/live/trucks. Returns active trucks whose in-progress trip serves the resident's assigned drop-off point, with the same shape as the admin endpoint. Empty when household has no DOP yet. Frontend (customer-web/): - /codes — paginated list with status filter pills (all / active / allocated / used / expired), one-click activate for allocated codes (in-place state update), inline balance summary + low-balance warning banner with link to partner stores. - /pickups — upcoming-trip cards with status pill, route, scheduled date/time, my stop sequence, driver, truck plate, link to live tracker. Friendly empty states for "no DOP assigned" and "no trips scheduled". - /collections — date-range filtered scan history table (when / serial / drop-off / weight / waste type), shows total scan count. - /tracker — Leaflet map polling /me/live/trucks every 10s. Drop-off pin (verde dot) + truck markers (rotating SVG arrow by heading degrees, 400ms transition between updates). Diff-update logic: reuses markers across polls and removes trucks that vanish. Side list of active trucks with plate, team, speed, last update. Falls back to NCR center when no DOP coords. Leaflet loaded from CDN (kept out of bundle) — no SSR concerns. - Resident nav adds /collections (label "History"). - API client: types LiveTruck + LiveTrucksResponse; me.liveTrucks(). Build: 16 routes, all type-check + lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Api/V1/Me/MyLiveTrucksController.php | 58 +++++++++++++++++++ routes/api.php | 3 + 2 files changed, 61 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/Me/MyLiveTrucksController.php diff --git a/app/Http/Controllers/Api/V1/Me/MyLiveTrucksController.php b/app/Http/Controllers/Api/V1/Me/MyLiveTrucksController.php new file mode 100644 index 0000000..2b04335 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Me/MyLiveTrucksController.php @@ -0,0 +1,58 @@ +where('head_user_id', $request->user()->id) + ->first(); + + if (! $household || ! $household->assigned_drop_off_point_id) { + return $this->ok([ + 'trucks' => [], + 'as_of' => now()->toIso8601String(), + 'household_assigned' => false, + ]); + } + + $relevantTripUuids = Trip::query() + ->whereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]) + ->whereHas('stops', fn ($q) => $q->where('drop_off_point_id', $household->assigned_drop_off_point_id)) + ->pluck('uuid') + ->all(); + + $positions = collect($this->tracker->activeTruckPositions()) + ->filter(fn ($p) => $p['active_trip_id'] && in_array($p['active_trip_id'], $relevantTripUuids, true)) + ->values() + ->all(); + + return $this->ok([ + 'trucks' => $positions, + 'as_of' => now()->toIso8601String(), + 'household_assigned' => true, + 'drop_off_point' => [ + 'id' => $household->assignedDropOffPoint?->uuid, + 'name' => $household->assignedDropOffPoint?->name, + 'lat' => $household->assignedDropOffPoint?->coordinates?->latitude, + 'lng' => $household->assignedDropOffPoint?->coordinates?->longitude, + ], + ]); + } +} diff --git a/routes/api.php b/routes/api.php index 9813476..ac2b088 100644 --- a/routes/api.php +++ b/routes/api.php @@ -42,6 +42,7 @@ use App\Http\Controllers\Api\V1\HealthController; use App\Http\Controllers\Api\V1\Household\HouseholdController; use App\Http\Controllers\Api\V1\Me\MyCollectionsController; use App\Http\Controllers\Api\V1\Me\MyHouseholdController; +use App\Http\Controllers\Api\V1\Me\MyLiveTrucksController; use App\Http\Controllers\Api\V1\Me\MyNotificationsController; use App\Http\Controllers\Api\V1\Me\UpcomingPickupsController; use App\Http\Controllers\Api\V1\Qr\MyQrCodeController; @@ -105,6 +106,8 @@ Route::middleware('auth:sanctum')->prefix('me')->name('api.v1.me.')->group(funct Route::get('/notifications/unread-count', [MyNotificationsController::class, 'unreadCount'])->name('notifications.unread-count'); Route::post('/notifications/mark-all-read', [MyNotificationsController::class, 'markAllRead'])->name('notifications.mark-all-read'); Route::post('/notifications/{id}/read', [MyNotificationsController::class, 'markRead'])->name('notifications.mark-read'); + + Route::get('/live/trucks', [MyLiveTrucksController::class, 'trucks'])->name('live.trucks'); }); // Public partner stores (for residents browsing where to buy codes)