From aa8fdaa5aaa3ae06262c14ab8215b231dd545a11 Mon Sep 17 00:00:00 2001 From: Developer Date: Mon, 6 Jul 2026 15:59:35 +0800 Subject: [PATCH] fix(scanner): add team_id to collection_logs for robust stats tracking - Created a database migration to add `team_id` to `collection_logs`. - Created a database migration to backfill `team_id` for past scans using `trip_id` and scanner's active team. - Updated `ScanService` to lookup the scanner's active team and store the `team_id` directly on the `CollectionLog`. - Modified the `CollectionTeam` model's `collectionLogs()` relation to be a direct `HasMany` using the new `team_id` instead of routing through `Trip`. - Updated `AdminTeamReportController` to query metrics directly from `collection_logs.team_id`, ensuring all past and future scans (including those done without an active trip) correctly increment team stats and daily charts. --- .../V1/Admin/AdminTeamReportController.php | 22 +++---- app/Models/CollectionLog.php | 7 +- app/Models/CollectionTeam.php | 4 +- app/Services/Scan/ScanService.php | 6 ++ commit_message.txt | 11 ++-- ..._074707_add_team_id_to_collection_logs.php | 29 +++++++++ ...12_backfill_team_id_on_collection_logs.php | 43 +++++++++++++ tests/Feature/Api/V1/Scan/ScanTeamIdTest.php | 64 +++++++++++++++++++ 8 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 database/migrations/2026_07_06_074707_add_team_id_to_collection_logs.php create mode 100644 database/migrations/2026_07_06_075812_backfill_team_id_on_collection_logs.php create mode 100644 tests/Feature/Api/V1/Scan/ScanTeamIdTest.php diff --git a/app/Http/Controllers/Api/V1/Admin/AdminTeamReportController.php b/app/Http/Controllers/Api/V1/Admin/AdminTeamReportController.php index e9e0ffd..e53f019 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminTeamReportController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminTeamReportController.php @@ -75,12 +75,10 @@ class AdminTeamReportController extends ApiController $scanStats = Tenancy::withoutScope(function () use ($teamIds, $fromStr, $toStr) { return DB::table('collection_logs') - ->join('trips', 'trips.id', '=', 'collection_logs.trip_id') - ->select('trips.team_id', DB::raw('COUNT(collection_logs.id) as total_scans')) - ->whereIn('trips.team_id', $teamIds) - ->whereBetween('collection_logs.scanned_at', [$fromStr, $toStr]) - ->whereNull('trips.deleted_at') - ->groupBy('trips.team_id') + ->select('team_id', DB::raw('COUNT(id) as total_scans')) + ->whereIn('team_id', $teamIds) + ->whereBetween('scanned_at', [$fromStr, $toStr]) + ->groupBy('team_id') ->get() ->keyBy('team_id'); }); @@ -179,10 +177,10 @@ class AdminTeamReportController extends ApiController $kpis = $this->buildKpis($team->id, $tripIds, $fromDate, $toDate, $fromDT, $toDT); // --- Daily scans (group by DATE) --- - $dailyScans = Tenancy::withoutScope(function () use ($tripIds, $fromDT, $toDT) { + $dailyScans = Tenancy::withoutScope(function () use ($team, $fromDT, $toDT) { return DB::table('collection_logs') ->selectRaw('DATE(scanned_at) as date, COUNT(*) as scans') - ->whereIn('trip_id', $tripIds) + ->where('team_id', $team->id) ->whereBetween('scanned_at', [$fromDT, $toDT]) ->groupByRaw('DATE(scanned_at)') ->orderBy('date') @@ -191,10 +189,10 @@ class AdminTeamReportController extends ApiController }); // --- Weekly scans (group by YEARWEEK) --- - $weeklyScans = Tenancy::withoutScope(function () use ($tripIds, $fromDT, $toDT) { + $weeklyScans = Tenancy::withoutScope(function () use ($team, $fromDT, $toDT) { return DB::table('collection_logs') ->selectRaw("DATE(DATE_SUB(scanned_at, INTERVAL WEEKDAY(scanned_at) DAY)) as week_start, COUNT(*) as scans") - ->whereIn('trip_id', $tripIds) + ->where('team_id', $team->id) ->whereBetween('scanned_at', [$fromDT, $toDT]) ->groupByRaw("DATE(DATE_SUB(scanned_at, INTERVAL WEEKDAY(scanned_at) DAY))") ->orderBy('week_start') @@ -314,10 +312,10 @@ class AdminTeamReportController extends ApiController ->first(); }); - $scanStats = Tenancy::withoutScope(function () use ($tripIds, $fromDT, $toDT) { + $scanStats = Tenancy::withoutScope(function () use ($teamId, $fromDT, $toDT) { return DB::table('collection_logs') ->selectRaw('COUNT(*) as total_scans, SUM(COALESCE(weight_kg,0)) as total_scan_weight') - ->whereIn('trip_id', $tripIds) + ->where('team_id', $teamId) ->whereBetween('scanned_at', [$fromDT, $toDT]) ->first(); }); diff --git a/app/Models/CollectionLog.php b/app/Models/CollectionLog.php index 629faf4..cb80d04 100644 --- a/app/Models/CollectionLog.php +++ b/app/Models/CollectionLog.php @@ -22,7 +22,7 @@ class CollectionLog extends Model public const STATUS_EXPIRED = 'expired'; protected $fillable = [ - 'tenant_id', 'qr_code_id', 'household_id', 'drop_off_point_id', + 'tenant_id', 'qr_code_id', 'team_id', 'household_id', 'drop_off_point_id', 'scanned_by_user_id', 'trip_id', 'trip_stop_id', 'scanned_at', 'coordinates_at_scan', 'weight_kg', 'waste_type', 'photo_path', 'notes', @@ -62,4 +62,9 @@ class CollectionLog extends Model { return $this->belongsTo(Trip::class); } + + public function team(): BelongsTo + { + return $this->belongsTo(CollectionTeam::class, 'team_id'); + } } diff --git a/app/Models/CollectionTeam.php b/app/Models/CollectionTeam.php index a3b16ba..aad7764 100644 --- a/app/Models/CollectionTeam.php +++ b/app/Models/CollectionTeam.php @@ -82,9 +82,9 @@ class CollectionTeam extends Model ->whereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]); } - public function collectionLogs(): HasManyThrough + public function collectionLogs(): HasMany { - return $this->hasManyThrough(CollectionLog::class, Trip::class, 'team_id', 'trip_id'); + return $this->hasMany(CollectionLog::class, 'team_id'); } public function getIsFullAttribute(): bool diff --git a/app/Services/Scan/ScanService.php b/app/Services/Scan/ScanService.php index 37086f1..544391f 100644 --- a/app/Services/Scan/ScanService.php +++ b/app/Services/Scan/ScanService.php @@ -3,6 +3,7 @@ namespace App\Services\Scan; use App\Models\CollectionLog; +use App\Models\CollectionTeam; use App\Models\DropOffPoint; use App\Models\QrCode; use App\Models\Trip; @@ -86,8 +87,13 @@ class ScanService 'scanned_by_user_id' => $scanner->id, ])->save(); + $team = CollectionTeam::where('scanner_id', $scanner->id) + ->where('status', CollectionTeam::STATUS_ACTIVE) + ->first(); + $log = CollectionLog::create([ 'qr_code_id' => $code->id, + 'team_id' => $team?->id, 'household_id' => $code->assigned_to_household_id, 'drop_off_point_id' => $dropOff->id, 'scanned_by_user_id' => $scanner->id, diff --git a/commit_message.txt b/commit_message.txt index cf83fe0..d15e166 100644 --- a/commit_message.txt +++ b/commit_message.txt @@ -1,6 +1,7 @@ -feat: implement trip incident dashboard, drop-off geofence validation, and calendar updates +fix(scanner): add team_id to collection_logs for robust stats tracking -- **Trip Incidents**: Created the `AdminTripController@incidents` API endpoint, built the `incidents.blade.php` view, enabled the incidents sidebar link, and added test coverage (`TripIncidentDashboardTest.php`). -- **Drop-off Points (LGU Geofence)**: Added a Barangay (LGU) dropdown to the drop-off point form. Integrated `Turf.js` to render the LGU boundary polygon on Leaflet and restrict pin placement (clicks, drags, and address search) to within the selected boundary. -- **Trip Calendar**: Changed the shortcut for opening the Daily Digest from Ctrl+Click to Shift+Click on calendar dates. -- **API fixes**: Corrected the Barangay fetch endpoint to `/api/v1/geo/barangays` in the Drop-off points view. +- Created a database migration to add `team_id` to `collection_logs`. +- Created a database migration to backfill `team_id` for past scans using `trip_id` and scanner's active team. +- Updated `ScanService` to lookup the scanner's active team and store the `team_id` directly on the `CollectionLog`. +- Modified the `CollectionTeam` model's `collectionLogs()` relation to be a direct `HasMany` using the new `team_id` instead of routing through `Trip`. +- Updated `AdminTeamReportController` to query metrics directly from `collection_logs.team_id`, ensuring all past and future scans (including those done without an active trip) correctly increment team stats and daily charts. diff --git a/database/migrations/2026_07_06_074707_add_team_id_to_collection_logs.php b/database/migrations/2026_07_06_074707_add_team_id_to_collection_logs.php new file mode 100644 index 0000000..547196d --- /dev/null +++ b/database/migrations/2026_07_06_074707_add_team_id_to_collection_logs.php @@ -0,0 +1,29 @@ +foreignId('team_id')->nullable()->after('qr_code_id')->constrained('collection_teams')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('collection_logs', function (Blueprint $table) { + $table->dropForeign(['team_id']); + $table->dropColumn('team_id'); + }); + } +}; diff --git a/database/migrations/2026_07_06_075812_backfill_team_id_on_collection_logs.php b/database/migrations/2026_07_06_075812_backfill_team_id_on_collection_logs.php new file mode 100644 index 0000000..37da802 --- /dev/null +++ b/database/migrations/2026_07_06_075812_backfill_team_id_on_collection_logs.php @@ -0,0 +1,43 @@ + scanned_by_user_id -> CollectionTeam + DB::statement(' + UPDATE collection_logs + JOIN qr_codes ON qr_codes.id = collection_logs.qr_code_id + JOIN collection_teams ON collection_teams.scanner_id = qr_codes.scanned_by_user_id + SET collection_logs.team_id = collection_teams.id + WHERE collection_logs.team_id IS NULL + AND collection_teams.status = "active" + '); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/tests/Feature/Api/V1/Scan/ScanTeamIdTest.php b/tests/Feature/Api/V1/Scan/ScanTeamIdTest.php new file mode 100644 index 0000000..b63d941 --- /dev/null +++ b/tests/Feature/Api/V1/Scan/ScanTeamIdTest.php @@ -0,0 +1,64 @@ +seed([RoleSeeder::class, SamplePsgcSeeder::class, SampleDropOffPointsSeeder::class]); + } + + public function test_scan_without_trip_assigns_team_id() + { + $scanner = User::factory()->create(['role' => User::ROLE_SCANNER, 'status' => 'active']); + $team = CollectionTeam::create([ + 'tenant_id' => 1, + 'name' => 'Test Team', + 'scanner_id' => $scanner->id, + 'status' => CollectionTeam::STATUS_ACTIVE, + ]); + + $dop = DropOffPoint::first(); + $batch = app(BatchGenerator::class)->generate(1, QrCodeBatch::PURPOSE_FREE); + $code = $batch->codes->first(); + $code->forceFill([ + 'assigned_to_household_id' => \App\Models\Household::factory()->create(['barangay_id' => $dop->barangay_id])->id, + 'status' => 'active', + ])->save(); + + Sanctum::actingAs($scanner); + + $response = $this->postJson('/api/v1/scanner/scan', [ + 'serial' => $code->serial, + 'drop_off_point_id' => $dop->id, + 'lat' => $dop->coordinates->latitude, + 'lng' => $dop->coordinates->longitude, + 'weight_kg' => 5, + ]); + + $response->assertOk(); + + $this->assertDatabaseHas('collection_logs', [ + 'qr_code_id' => $code->id, + 'team_id' => $team->id, + 'trip_id' => null, + ]); + } +}