diff --git a/app/Console/Commands/SimulateTruckMovement.php b/app/Console/Commands/SimulateTruckMovement.php index db886fd..54a7a30 100644 --- a/app/Console/Commands/SimulateTruckMovement.php +++ b/app/Console/Commands/SimulateTruckMovement.php @@ -23,8 +23,9 @@ class SimulateTruckMovement extends Command $delay = (int) $this->option('speed'); $truck = Truck::where('plate_number', $plate)->first(); - if (!$truck) { + if (! $truck) { $this->error("Truck with plate number {$plate} not found."); + return self::FAILURE; } @@ -33,14 +34,16 @@ class SimulateTruckMovement extends Command ->whereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_SCHEDULED, Trip::STATUS_AT_DUMPSITE]) ->first(); - if (!$trip) { + if (! $trip) { $this->error("No active or scheduled trip found for truck {$plate}."); + return self::FAILURE; } $driver = User::find($trip->team->driver_id); - if (!$driver) { - $this->error("No driver assigned to the team for this trip."); + if (! $driver) { + $this->error('No driver assigned to the team for this trip.'); + return self::FAILURE; } @@ -65,14 +68,14 @@ class SimulateTruckMovement extends Command } }); - $this->info("Trip state reset to Scheduled."); + $this->info('Trip state reset to Scheduled.'); // Start the trip $startLat = 13.7900; $startLng = 121.0200; $this->info("Starting trip at: Lat {$startLat}, Lng {$startLng}"); $trip = $executor->start($trip, $driver, $startLat, $startLng); - $this->info("Trip status: IN_PROGRESS"); + $this->info('Trip status: IN_PROGRESS'); $currentLat = $startLat; $currentLng = $startLng; @@ -81,7 +84,7 @@ class SimulateTruckMovement extends Command foreach ($stops as $index => $stop) { $dop = $stop->dropOffPoint; - if (!$dop || !$dop->coordinates) { + if (! $dop || ! $dop->coordinates) { continue; } @@ -102,7 +105,9 @@ class SimulateTruckMovement extends Command if ($destLng != $currentLng) { $rad = atan2($destLat - $currentLat, $destLng - $currentLng); $heading = (int) round(90 - rad2deg($rad)); - if ($heading < 0) $heading += 360; + if ($heading < 0) { + $heading += 360; + } } $tracker->record( @@ -114,7 +119,7 @@ class SimulateTruckMovement extends Command trip: $trip ); - $this->line(" Step {$step}/{$stepsCount} -> Lat: " . round($lat, 6) . ", Lng: " . round($lng, 6) . ", Heading: {$heading}°"); + $this->line(" Step {$step}/{$stepsCount} -> Lat: ".round($lat, 6).', Lng: '.round($lng, 6).", Heading: {$heading}°"); sleep($delay); } @@ -151,7 +156,9 @@ class SimulateTruckMovement extends Command if ($destLng != $currentLng) { $rad = atan2($destLat - $currentLat, $destLng - $currentLng); $heading = (int) round(90 - rad2deg($rad)); - if ($heading < 0) $heading += 360; + if ($heading < 0) { + $heading += 360; + } } // The last step will fall inside the dumpsite geofence and trigger it! @@ -164,10 +171,10 @@ class SimulateTruckMovement extends Command trip: $trip ); - $this->line(" Step {$step}/{$stepsCount} -> Lat: " . round($lat, 6) . ", Lng: " . round($lng, 6)); + $this->line(" Step {$step}/{$stepsCount} -> Lat: ".round($lat, 6).', Lng: '.round($lng, 6)); if ($result->geofenceTriggered) { - $this->warn(" [GEOFENCE] Entered Dumpsite boundary! Trip status transitioned automatically."); + $this->warn(' [GEOFENCE] Entered Dumpsite boundary! Trip status transitioned automatically.'); } sleep($delay); @@ -175,6 +182,7 @@ class SimulateTruckMovement extends Command } $this->info("\nSimulation complete! Truck reached the dumpsite."); + return self::SUCCESS; } } diff --git a/app/Events/TruckLocationBroadcast.php b/app/Events/TruckLocationBroadcast.php index c80f01f..77c572e 100644 --- a/app/Events/TruckLocationBroadcast.php +++ b/app/Events/TruckLocationBroadcast.php @@ -6,7 +6,6 @@ use App\Models\Trip; use App\Models\Truck; use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; diff --git a/app/Http/Controllers/Api/V1/Admin/AdminHouseholdController.php b/app/Http/Controllers/Api/V1/Admin/AdminHouseholdController.php index d244609..9481e1f 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminHouseholdController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminHouseholdController.php @@ -5,10 +5,27 @@ namespace App\Http\Controllers\Api\V1\Admin; use App\Events\HouseholdVerified; use App\Http\Controllers\Api\V1\ApiController; use App\Http\Requests\Admin\RejectProfileRequest; +use App\Http\Requests\Admin\StoreAdminHouseholdMemberRequest; +use App\Http\Requests\Admin\StoreAdminHouseholdRequest; +use App\Http\Requests\Admin\UpdateAdminHouseholdMemberRequest; +use App\Http\Requests\Admin\UpdateAdminHouseholdRequest; use App\Http\Resources\HouseholdResource; +use App\Models\Barangay; use App\Models\Household; +use App\Models\HouseholdMember; +use App\Models\NotificationPreference; +use App\Models\Tenant; +use App\Models\User; +use App\Notifications\HouseholdRejected; +use App\Services\DropOff\DropOffPointFinder; +use App\Tenancy\Tenancy; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Str; +use MatanYadaev\EloquentSpatial\Objects\Point; class AdminHouseholdController extends ApiController { @@ -96,9 +113,9 @@ class AdminHouseholdController extends ApiController $household->markRejected($request->user(), $reason); if ($household->head) { - \Illuminate\Support\Facades\Notification::send( + Notification::send( $household->head, - new \App\Notifications\HouseholdRejected($household, $reason), + new HouseholdRejected($household, $reason), ); } @@ -106,4 +123,242 @@ class AdminHouseholdController extends ApiController return $this->ok(new HouseholdResource($household), 'Household rejected'); } + + public function store( + StoreAdminHouseholdRequest $request, + DropOffPointFinder $dopFinder, + ): JsonResponse { + $data = $request->validated(); + $tenant = Tenancy::current(); + $tenantId = null; + + if ($tenant) { + $tenantId = $tenant->id; + } else { + // Fall back to resolving tenant from selected Barangay (useful for super-admin context) + $barangay = Barangay::find($data['barangay_id']); + if ($barangay) { + $tenantId = Tenant::where('city_municipality_id', $barangay->city_municipality_id)->value('id'); + } + } + + if (! $tenantId) { + return $this->fail('Could not resolve LGU/Tenant context for the selected Barangay.', null, 400); + } + + $headUserId = $data['head_user_id'] ?? null; + + if ($data['resident_type'] === 'existing') { + $existing = Household::where('head_user_id', $headUserId)->exists(); + if ($existing) { + return $this->fail( + 'Selected resident already heads a household.', + ['head_user_id' => ['already_has_household']], + 422 + ); + } + } + + $point = new Point((float) $data['lat'], (float) $data['lng'], 4326); + $nearestDop = $dopFinder->nearest((float) $data['lat'], (float) $data['lng']); + + $household = DB::transaction(function () use ($data, $tenantId, $point, $nearestDop, &$headUserId) { + if ($data['resident_type'] === 'new') { + $user = User::create([ + 'tenant_id' => $tenantId, + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'email' => $data['email'], + 'phone' => $data['phone'], + 'password' => Hash::make($data['password'] ?? Str::random(12)), + 'role' => User::ROLE_RESIDENT, + 'status' => User::STATUS_PENDING, + 'preferred_language' => 'en', + ]); + + $user->assignRole(User::ROLE_RESIDENT); + + $profileClass = User::profileModelForRole(User::ROLE_RESIDENT); + if ($profileClass) { + $profileClass::create(['user_id' => $user->id]); + } + + NotificationPreference::create([ + 'user_id' => $user->id, + 'language' => 'en', + ]); + + $headUserId = $user->id; + } + + $h = Household::create([ + 'tenant_id' => $tenantId, + 'head_user_id' => $headUserId, + 'barangay_id' => $data['barangay_id'], + 'address_line' => $data['address_line'], + 'coordinates' => $point, + 'household_size' => $data['household_size'], + 'assigned_drop_off_point_id' => $nearestDop?->id, + 'verification_status' => Household::VERIFICATION_PENDING, + ]); + + HouseholdMember::create([ + 'household_id' => $h->id, + 'user_id' => $headUserId, + 'relationship' => HouseholdMember::RELATIONSHIP_HEAD, + 'full_name' => User::find($headUserId)->full_name, + ]); + + return $h; + }); + + $household->load(['head', 'barangay', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); + + return $this->created( + new HouseholdResource($household), + 'Household created successfully.' + ); + } + + public function update( + UpdateAdminHouseholdRequest $request, + Household $household, + DropOffPointFinder $dopFinder, + ): JsonResponse { + $data = $request->validated(); + $point = new Point((float) $data['lat'], (float) $data['lng'], 4326); + + $barangay = Barangay::findOrFail($data['barangay_id']); + if ($barangay && $barangay->boundary) { + $isContained = DB::selectOne( + 'SELECT ST_Contains(boundary, ST_GeomFromText(?, 4326, \'axis-order=long-lat\')) as contained FROM barangays WHERE id = ?', + [$point->toWkt(), $barangay->id] + ); + + if (! $isContained || ! $isContained->contained) { + return $this->fail( + 'The pinned household coordinates must lie completely within the Barangay boundary.', + ['barangay_id' => ['The location pin is outside the selected Barangay boundary.']], + 422 + ); + } + } + + $nearestDop = $dopFinder->nearest((float) $data['lat'], (float) $data['lng']); + + $household->update([ + 'address_line' => $data['address_line'], + 'coordinates' => $point, + 'barangay_id' => $data['barangay_id'], + 'household_size' => $data['household_size'], + 'assigned_drop_off_point_id' => $nearestDop?->id, + ]); + + $household->load(['head', 'barangay', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); + + return $this->ok(new HouseholdResource($household), 'Household updated successfully.'); + } + + public function addMember(StoreAdminHouseholdMemberRequest $request, Household $household): JsonResponse + { + $data = $request->validated(); + + if (! empty($data['user_id'])) { + $isMember = HouseholdMember::where('user_id', $data['user_id'])->exists(); + if ($isMember) { + return $this->fail('The selected resident already belongs to a household.', ['user_id' => ['already_member']], 422); + } + + $user = User::findOrFail($data['user_id']); + $fullName = $user->full_name; + } else { + $fullName = $data['full_name']; + } + + if ($data['relationship'] === HouseholdMember::RELATIONSHIP_HEAD) { + $hasHead = $household->members()->where('relationship', HouseholdMember::RELATIONSHIP_HEAD)->exists(); + if ($hasHead) { + return $this->fail('Household already has a designated head.', ['relationship' => ['head_already_exists']], 422); + } + } + + $member = HouseholdMember::create([ + 'household_id' => $household->id, + 'user_id' => $data['user_id'] ?? null, + 'relationship' => $data['relationship'], + 'full_name' => $fullName, + 'date_of_birth' => $data['date_of_birth'] ?? null, + ]); + + if ($data['relationship'] === HouseholdMember::RELATIONSHIP_HEAD && ! empty($data['user_id'])) { + $household->update(['head_user_id' => $data['user_id']]); + } + + $currentMemberCount = $household->members()->count(); + if ($household->household_size < $currentMemberCount) { + $household->update(['household_size' => $currentMemberCount]); + } + + $household->load(['head', 'barangay', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); + + return $this->created(new HouseholdResource($household), 'Household member added successfully.'); + } + + public function updateMember( + UpdateAdminHouseholdMemberRequest $request, + Household $household, + HouseholdMember $member + ): JsonResponse { + if ($member->household_id !== $household->id) { + return $this->fail('Member does not belong to this household.', null, 404); + } + + $data = $request->validated(); + + if ($data['relationship'] === HouseholdMember::RELATIONSHIP_HEAD && $member->relationship !== HouseholdMember::RELATIONSHIP_HEAD) { + $otherHead = $household->members() + ->where('relationship', HouseholdMember::RELATIONSHIP_HEAD) + ->where('id', '!=', $member->id) + ->first(); + + if ($otherHead) { + return $this->fail('Household already has another designated head.', ['relationship' => ['head_already_exists']], 422); + } + + if ($member->user_id) { + $household->update(['head_user_id' => $member->user_id]); + } + } + + if ($member->relationship === HouseholdMember::RELATIONSHIP_HEAD && $data['relationship'] !== HouseholdMember::RELATIONSHIP_HEAD) { + return $this->fail('Cannot demote the household head. Designate another member as head first.', ['relationship' => ['cannot_demote_head']], 422); + } + + $member->update([ + 'relationship' => $data['relationship'], + 'full_name' => $data['full_name'], + 'date_of_birth' => $data['date_of_birth'] ?? null, + ]); + + $household->load(['head', 'barangay', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); + + return $this->ok(new HouseholdResource($household), 'Household member updated successfully.'); + } + + public function removeMember(Request $request, Household $household, HouseholdMember $member): JsonResponse + { + if ($member->household_id !== $household->id) { + return $this->fail('Member does not belong to this household.', null, 404); + } + + if ($member->relationship === HouseholdMember::RELATIONSHIP_HEAD) { + return $this->fail('Cannot remove the household head. Assign a new head first.', null, 422); + } + + $member->delete(); + + $household->load(['head', 'barangay', 'members.user', 'assignedDropOffPoint'])->loadCount('members'); + + return $this->ok(new HouseholdResource($household), 'Household member removed successfully.'); + } } diff --git a/app/Http/Controllers/Api/V1/Admin/AdminLiveTrackingController.php b/app/Http/Controllers/Api/V1/Admin/AdminLiveTrackingController.php index e8a7852..44ee793 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminLiveTrackingController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminLiveTrackingController.php @@ -118,11 +118,15 @@ class AdminLiveTrackingController extends ApiController private function dumpsiteBoundary($dumpsite): ?array { - if (! $dumpsite->boundary_polygon) return null; + if (! $dumpsite->boundary_polygon) { + return null; + } $rings = $dumpsite->boundary_polygon->getGeometries(); $ring = $rings->first(); - if (! $ring) return null; + if (! $ring) { + return null; + } return $ring->getGeometries() ->map(fn ($p) => ['lat' => $p->latitude, 'lng' => $p->longitude]) diff --git a/app/Http/Controllers/Api/V1/Admin/AdminPartnerStoreController.php b/app/Http/Controllers/Api/V1/Admin/AdminPartnerStoreController.php index d061323..2ef45d0 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminPartnerStoreController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminPartnerStoreController.php @@ -6,7 +6,6 @@ use App\Http\Controllers\Api\V1\ApiController; use App\Http\Resources\PartnerStoreResource; use App\Models\Household; use App\Models\PartnerStore; -use App\Models\User; use App\Services\Store\StoreOperations; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; diff --git a/app/Http/Controllers/Api/V1/Admin/AdminQrBatchController.php b/app/Http/Controllers/Api/V1/Admin/AdminQrBatchController.php index 25ce9be..0e107fa 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminQrBatchController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminQrBatchController.php @@ -6,16 +6,20 @@ use App\Http\Controllers\Api\V1\ApiController; use App\Http\Requests\Qr\GenerateBatchRequest; use App\Http\Resources\QrCodeBatchResource; use App\Http\Resources\QrCodeResource; +use App\Models\QrCode; use App\Models\QrCodeBatch; use App\Models\ServiceArea; use App\Services\Qr\BatchGenerator; use Barryvdh\DomPDF\Facade\Pdf; +use Carbon\Carbon; use Endroid\QrCode\Builder\Builder; use Endroid\QrCode\ErrorCorrectionLevel; use Endroid\QrCode\Writer\PngWriter; +use Endroid\QrCode\Writer\SvgWriter; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Picqer\Barcode\BarcodeGeneratorPNG; +use Picqer\Barcode\BarcodeGeneratorSVG; use Symfony\Component\HttpFoundation\Response; class AdminQrBatchController extends ApiController @@ -71,7 +75,7 @@ class AdminQrBatchController extends ApiController targetArea: $area, targetStoreId: $data['target_store_id'] ?? null, createdBy: $request->user(), - expiresAt: isset($data['expires_at']) ? \Carbon\Carbon::parse($data['expires_at']) : null, + expiresAt: isset($data['expires_at']) ? Carbon::parse($data['expires_at']) : null, notes: $data['notes'] ?? null, ); @@ -103,7 +107,7 @@ class AdminQrBatchController extends ApiController public function showCode(string $serial): JsonResponse { - $code = \App\Models\QrCode::query() + $code = QrCode::query() ->with(['batch.targetArea', 'household.head', 'usedAtDropOff', 'scannedBy']) ->where('serial', $serial) ->firstOrFail(); @@ -140,7 +144,7 @@ class AdminQrBatchController extends ApiController { try { $result = (new Builder( - writer: new PngWriter(), + writer: new PngWriter, data: $serial, errorCorrectionLevel: ErrorCorrectionLevel::Medium, size: 140, @@ -149,14 +153,15 @@ class AdminQrBatchController extends ApiController return $result->getDataUri(); } catch (\Throwable $e) { - if (class_exists(\Endroid\QrCode\Writer\SvgWriter::class)) { + if (class_exists(SvgWriter::class)) { $result = (new Builder( - writer: new \Endroid\QrCode\Writer\SvgWriter(), + writer: new SvgWriter, data: $serial, errorCorrectionLevel: ErrorCorrectionLevel::Medium, size: 140, margin: 4, ))->build(); + return 'data:image/svg+xml;base64,'.base64_encode($result->getString()); } throw $e; @@ -166,14 +171,15 @@ class AdminQrBatchController extends ApiController private function barcodeDataUri(string $serial): string { try { - $generator = new BarcodeGeneratorPNG(); + $generator = new BarcodeGeneratorPNG; $png = $generator->getBarcode($serial, BarcodeGeneratorPNG::TYPE_CODE_128, 1, 28); return 'data:image/png;base64,'.base64_encode($png); } catch (\Throwable $e) { - if (class_exists(\Picqer\Barcode\BarcodeGeneratorSVG::class)) { - $generator = new \Picqer\Barcode\BarcodeGeneratorSVG(); - $svg = $generator->getBarcode($serial, \Picqer\Barcode\BarcodeGeneratorSVG::TYPE_CODE_128, 1, 28); + if (class_exists(BarcodeGeneratorSVG::class)) { + $generator = new BarcodeGeneratorSVG; + $svg = $generator->getBarcode($serial, BarcodeGeneratorSVG::TYPE_CODE_128, 1, 28); + return 'data:image/svg+xml;base64,'.base64_encode($svg); } throw $e; diff --git a/app/Http/Controllers/Api/V1/Admin/AdminReportController.php b/app/Http/Controllers/Api/V1/Admin/AdminReportController.php index 60791ea..525e767 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminReportController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminReportController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin; use App\Http\Controllers\Api\V1\ApiController; use App\Models\DailyCollectionStat; +use App\Models\DumpsiteRelease; use App\Models\MonthlyStoreSale; use App\Models\WeeklyRoutePerformance; use App\Services\Report\Aggregator; @@ -133,7 +134,7 @@ class AdminReportController extends ApiController $out = fopen('php://output', 'w'); fputcsv($out, ['released_at', 'trip_number', 'dumpsite', 'permit_number', 'weight_kg', 'gate_pass', 'attendant']); - \App\Models\DumpsiteRelease::with(['dumpsite', 'trip']) + DumpsiteRelease::with(['dumpsite', 'trip']) ->whereBetween('released_at', [$from, $to]) ->orderBy('released_at') ->chunk(500, function ($rows) use ($out) { diff --git a/app/Http/Controllers/Api/V1/Admin/AdminUserController.php b/app/Http/Controllers/Api/V1/Admin/AdminUserController.php index 4935d1f..0683d15 100644 --- a/app/Http/Controllers/Api/V1/Admin/AdminUserController.php +++ b/app/Http/Controllers/Api/V1/Admin/AdminUserController.php @@ -17,7 +17,7 @@ class AdminUserController extends ApiController { $request->validate([ 'role' => [ - 'nullable', + 'nullable', Rule::in([ User::ROLE_ADMIN, User::ROLE_RESIDENT, @@ -25,7 +25,7 @@ class AdminUserController extends ApiController User::ROLE_HELPER, User::ROLE_SCANNER, User::ROLE_STORE_PARTNER, - ]) + ]), ], 'status' => [ 'nullable', @@ -33,9 +33,10 @@ class AdminUserController extends ApiController User::STATUS_ACTIVE, User::STATUS_SUSPENDED, User::STATUS_PENDING, - ]) + ]), ], 'verification_status' => ['nullable', 'in:pending,approved,rejected'], + 'without_household' => ['nullable', 'boolean'], 'q' => ['nullable', 'string', 'max:100'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:500'], ]); @@ -45,6 +46,7 @@ class AdminUserController extends ApiController $users = User::query() ->when($request->filled('role'), fn ($q) => $q->where('role', $request->string('role'))) ->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status'))) + ->when($request->boolean('without_household'), fn ($q) => $q->whereDoesntHave('headedHousehold')) ->when($request->filled('q'), function ($q) use ($request) { $term = '%'.$request->string('q').'%'; $q->where(function ($qq) use ($term) { diff --git a/app/Http/Controllers/Api/V1/Admin/BulkActionController.php b/app/Http/Controllers/Api/V1/Admin/BulkActionController.php index 4a943f3..8948d26 100644 --- a/app/Http/Controllers/Api/V1/Admin/BulkActionController.php +++ b/app/Http/Controllers/Api/V1/Admin/BulkActionController.php @@ -32,14 +32,17 @@ class BulkActionController extends ApiController $h = Household::where('uuid', $uuid)->first(); if (! $h) { $results[] = ['id' => $uuid, 'ok' => false, 'reason' => 'not_found']; + continue; } if ($h->verification_status === Household::VERIFICATION_APPROVED) { $results[] = ['id' => $uuid, 'ok' => false, 'reason' => 'already_approved']; + continue; } if (! $h->proof_of_residency_path) { $results[] = ['id' => $uuid, 'ok' => false, 'reason' => 'no_proof']; + continue; } @@ -70,9 +73,14 @@ class BulkActionController extends ApiController $results = []; foreach ($data['user_ids'] as $uuid) { $u = User::where('uuid', $uuid)->first(); - if (! $u) { $results[] = ['id' => $uuid, 'ok' => false, 'reason' => 'not_found']; continue; } + if (! $u) { + $results[] = ['id' => $uuid, 'ok' => false, 'reason' => 'not_found']; + + continue; + } if ($u->role === User::ROLE_ADMIN) { $results[] = ['id' => $uuid, 'ok' => false, 'reason' => 'admin_protected']; + continue; } $u->forceFill(['status' => User::STATUS_SUSPENDED])->save(); @@ -94,7 +102,11 @@ class BulkActionController extends ApiController $results = []; foreach ($data['user_ids'] as $uuid) { $u = User::where('uuid', $uuid)->first(); - if (! $u) { $results[] = ['id' => $uuid, 'ok' => false, 'reason' => 'not_found']; continue; } + if (! $u) { + $results[] = ['id' => $uuid, 'ok' => false, 'reason' => 'not_found']; + + continue; + } $u->forceFill(['status' => User::STATUS_ACTIVE])->save(); $results[] = ['id' => $uuid, 'ok' => true]; } @@ -116,9 +128,14 @@ class BulkActionController extends ApiController $results = []; foreach ($data['serials'] as $serial) { $code = QrCode::where('serial', $serial)->first(); - if (! $code) { $results[] = ['serial' => $serial, 'ok' => false, 'reason' => 'not_found']; continue; } + if (! $code) { + $results[] = ['serial' => $serial, 'ok' => false, 'reason' => 'not_found']; + + continue; + } if (! $code->status->canTransitionTo(Voided::class)) { $results[] = ['serial' => $serial, 'ok' => false, 'reason' => 'cannot_void_from_'.(string) $code->status]; + continue; } $code->status->transitionTo(Voided::class); diff --git a/app/Http/Controllers/Api/V1/Auth/LoginController.php b/app/Http/Controllers/Api/V1/Auth/LoginController.php index 1cbd925..3841e4a 100644 --- a/app/Http/Controllers/Api/V1/Auth/LoginController.php +++ b/app/Http/Controllers/Api/V1/Auth/LoginController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\V1\Auth; use App\Http\Controllers\Api\V1\ApiController; use App\Http\Requests\Auth\LoginRequest; use App\Http\Resources\UserResource; +use App\Models\Tenant; use App\Models\User; use App\Tenancy\Tenancy; use Illuminate\Http\JsonResponse; @@ -41,6 +42,12 @@ class LoginController extends ApiController // into their own LGU. Prevents cross-tenant credential reuse. if (! $user->isSuperAdmin()) { $tenant = Tenancy::current(); + if (! $tenant && $user->tenant_id) { + $tenant = Tenant::find($user->tenant_id); + if ($tenant) { + Tenancy::setCurrent($tenant); + } + } if (! $tenant) { return $this->fail( 'Pick your LGU first. Send X-Tenant-Code header.', diff --git a/app/Http/Controllers/Api/V1/Auth/SelfProfileController.php b/app/Http/Controllers/Api/V1/Auth/SelfProfileController.php index 5262f37..68318bd 100644 --- a/app/Http/Controllers/Api/V1/Auth/SelfProfileController.php +++ b/app/Http/Controllers/Api/V1/Auth/SelfProfileController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Api\V1\Auth; use App\Http\Controllers\Api\V1\ApiController; -use App\Http\Resources\UserDetailResource; use App\Models\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; diff --git a/app/Http/Controllers/Api/V1/Driver/DriverLocationController.php b/app/Http/Controllers/Api/V1/Driver/DriverLocationController.php index bef79b0..4e6b83b 100644 --- a/app/Http/Controllers/Api/V1/Driver/DriverLocationController.php +++ b/app/Http/Controllers/Api/V1/Driver/DriverLocationController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Api\V1\ApiController; use App\Models\Trip; use App\Models\Truck; use App\Services\LiveTracking\TruckTracker; +use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -39,7 +40,7 @@ class DriverLocationController extends ApiController heading: isset($data['heading_degrees']) ? (int) $data['heading_degrees'] : null, speedKmh: isset($data['speed_kmh']) ? (float) $data['speed_kmh'] : null, trip: $trip, - recordedAt: isset($data['recorded_at']) ? \Carbon\Carbon::parse($data['recorded_at']) : null, + recordedAt: isset($data['recorded_at']) ? Carbon::parse($data['recorded_at']) : null, ); return $this->ok([ diff --git a/app/Http/Controllers/Api/V1/Driver/DriverTripController.php b/app/Http/Controllers/Api/V1/Driver/DriverTripController.php index da90c06..58ece8f 100644 --- a/app/Http/Controllers/Api/V1/Driver/DriverTripController.php +++ b/app/Http/Controllers/Api/V1/Driver/DriverTripController.php @@ -203,6 +203,8 @@ class DriverTripController extends ApiController private function ensureStopBelongs(TripStop $stop, Trip $trip): void { - if ($stop->trip_id !== $trip->id) abort(404, 'Stop not found in this trip'); + if ($stop->trip_id !== $trip->id) { + abort(404, 'Stop not found in this trip'); + } } } diff --git a/app/Http/Controllers/Api/V1/Geo/BarangayController.php b/app/Http/Controllers/Api/V1/Geo/BarangayController.php index abf6a6c..cd4ba28 100644 --- a/app/Http/Controllers/Api/V1/Geo/BarangayController.php +++ b/app/Http/Controllers/Api/V1/Geo/BarangayController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\V1\Geo; use App\Http\Controllers\Api\V1\ApiController; use App\Http\Resources\BarangayResource; use App\Models\Barangay; +use App\Models\Tenant; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -21,7 +22,7 @@ class BarangayController extends ApiController $user = $request->user(); $barangays = Barangay::query() ->when($user && $user->tenant_id, function ($q) use ($user) { - $tenant = $user->tenant ?: \App\Models\Tenant::find($user->tenant_id); + $tenant = $user->tenant ?: Tenant::find($user->tenant_id); if ($tenant) { $q->where('city_municipality_id', $tenant->city_municipality_id); } diff --git a/app/Http/Controllers/Api/V1/Household/HouseholdController.php b/app/Http/Controllers/Api/V1/Household/HouseholdController.php index 29c2d4d..3603684 100644 --- a/app/Http/Controllers/Api/V1/Household/HouseholdController.php +++ b/app/Http/Controllers/Api/V1/Household/HouseholdController.php @@ -16,7 +16,6 @@ use App\Services\Geo\GeoLocationService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Storage; use MatanYadaev\EloquentSpatial\Objects\Point; class HouseholdController extends ApiController diff --git a/app/Http/Controllers/Api/V1/Me/MyCollectionsController.php b/app/Http/Controllers/Api/V1/Me/MyCollectionsController.php index 308177c..fc9b961 100644 --- a/app/Http/Controllers/Api/V1/Me/MyCollectionsController.php +++ b/app/Http/Controllers/Api/V1/Me/MyCollectionsController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\V1\Me; use App\Http\Controllers\Api\V1\ApiController; use App\Models\CollectionLog; use App\Models\Household; +use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -32,8 +33,8 @@ class MyCollectionsController extends ApiController ->with(['qrCode:id,serial', 'dropOffPoint:id,name,uuid']) ->where('household_id', $household->id) ->where('verification_status', CollectionLog::STATUS_VALID) - ->when($data['from'] ?? null, fn ($q, $from) => $q->where('scanned_at', '>=', \Carbon\Carbon::parse($from)->startOfDay())) - ->when($data['to'] ?? null, fn ($q, $to) => $q->where('scanned_at', '<=', \Carbon\Carbon::parse($to)->endOfDay())) + ->when($data['from'] ?? null, fn ($q, $from) => $q->where('scanned_at', '>=', Carbon::parse($from)->startOfDay())) + ->when($data['to'] ?? null, fn ($q, $to) => $q->where('scanned_at', '<=', Carbon::parse($to)->endOfDay())) ->orderByDesc('scanned_at') ->paginate($perPage); diff --git a/app/Http/Controllers/Api/V1/Me/UpcomingPickupsController.php b/app/Http/Controllers/Api/V1/Me/UpcomingPickupsController.php index 3b96694..c046905 100644 --- a/app/Http/Controllers/Api/V1/Me/UpcomingPickupsController.php +++ b/app/Http/Controllers/Api/V1/Me/UpcomingPickupsController.php @@ -36,7 +36,7 @@ class UpcomingPickupsController extends ApiController ->whereHas('route.stops', fn ($q) => $q->where('drop_off_point_id', $dopId)) ->where(function ($q) { $q->where('scheduled_date', '>=', now()->toDateString()) - ->orWhereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]); + ->orWhereIn('status', [Trip::STATUS_IN_PROGRESS, Trip::STATUS_AT_DUMPSITE]); }) ->whereNotIn('status', [Trip::STATUS_CANCELLED, Trip::STATUS_COMPLETED]) ->orderBy('scheduled_date') diff --git a/app/Http/Controllers/Api/V1/Payment/PaymentController.php b/app/Http/Controllers/Api/V1/Payment/PaymentController.php index c1da9f2..ec0c768 100644 --- a/app/Http/Controllers/Api/V1/Payment/PaymentController.php +++ b/app/Http/Controllers/Api/V1/Payment/PaymentController.php @@ -4,8 +4,8 @@ namespace App\Http\Controllers\Api\V1\Payment; use App\Http\Controllers\Api\V1\ApiController; use App\Models\Household; -use App\Models\Payment; use App\Models\PartnerStore; +use App\Models\Payment; use App\Services\Payment\PaymentDriver; use App\Services\Store\StoreOperations; use Illuminate\Http\JsonResponse; @@ -176,7 +176,9 @@ class PaymentController extends ApiController */ private function fulfill(Payment $payment, StoreOperations $stores): void { - if ($payment->purpose !== Payment::PURPOSE_RESIDENT) return; + if ($payment->purpose !== Payment::PURPOSE_RESIDENT) { + return; + } $meta = $payment->metadata ?? []; $store = isset($meta['store_id']) ? PartnerStore::find($meta['store_id']) : null; @@ -184,7 +186,9 @@ class PaymentController extends ApiController $qty = (int) ($meta['quantity'] ?? 0); $price = (int) ($meta['retail_price_per_code_centavos'] ?? 0); - if (! $store || ! $household || $qty <= 0) return; + if (! $store || ! $household || $qty <= 0) { + return; + } try { $stores->sellToHousehold($store, $household, $qty, $price); diff --git a/app/Http/Controllers/Api/V1/Scanner/ScannerController.php b/app/Http/Controllers/Api/V1/Scanner/ScannerController.php index 8ce57df..4ac899f 100644 --- a/app/Http/Controllers/Api/V1/Scanner/ScannerController.php +++ b/app/Http/Controllers/Api/V1/Scanner/ScannerController.php @@ -7,6 +7,7 @@ use App\Models\DropOffPoint; use App\Models\Trip; use App\Models\TripStop; use App\Services\Scan\ScanService; +use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -46,7 +47,7 @@ class ScannerController extends ApiController wasteType: $data['waste_type'] ?? null, photoPath: $data['photo_path'] ?? null, notes: $data['notes'] ?? null, - scannedAt: isset($data['scanned_at']) ? \Carbon\Carbon::parse($data['scanned_at']) : null, + scannedAt: isset($data['scanned_at']) ? Carbon::parse($data['scanned_at']) : null, ); if (! $result->accepted) { @@ -87,6 +88,7 @@ class ScannerController extends ApiController $dop = DropOffPoint::find($s['drop_off_point_id']); if (! $dop) { $results[] = ['index' => $i, 'serial' => $s['serial'], 'accepted' => false, 'reason' => 'dop_not_found']; + continue; } $trip = isset($s['trip_id']) ? Trip::where('uuid', $s['trip_id'])->first() : null; @@ -100,7 +102,7 @@ class ScannerController extends ApiController lng: (float) $s['lng'], trip: $trip, tripStop: $stop, - scannedAt: isset($s['scanned_at']) ? \Carbon\Carbon::parse($s['scanned_at']) : null, + scannedAt: isset($s['scanned_at']) ? Carbon::parse($s['scanned_at']) : null, ); $results[] = [ 'index' => $i, diff --git a/app/Http/Controllers/Api/V1/SuperAdmin/SuperAdminBarangayController.php b/app/Http/Controllers/Api/V1/SuperAdmin/SuperAdminBarangayController.php new file mode 100644 index 0000000..7a75627 --- /dev/null +++ b/app/Http/Controllers/Api/V1/SuperAdmin/SuperAdminBarangayController.php @@ -0,0 +1,226 @@ +validate([ + 'city_municipality_id' => ['nullable', 'integer'], + 'q' => ['nullable', 'string', 'max:100'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:100'], + ]); + + $perPage = (int) $request->input('per_page', 25); + + $barangays = Barangay::query() + ->with(['cityMunicipality.province']) + ->when($request->filled('city_municipality_id'), fn ($q) => $q->where('city_municipality_id', $request->integer('city_municipality_id'))) + ->when($request->filled('q'), function ($q) use ($request) { + $term = '%'.$request->string('q').'%'; + $q->where(function ($qq) use ($term) { + $qq->where('name', 'like', $term) + ->orWhere('code', 'like', $term) + ->orWhere('psgc_code', 'like', $term); + }); + }) + ->orderBy('name') + ->paginate($perPage); + + return $this->ok(BarangayResource::collection($barangays)); + } + + public function store(StoreBarangayRequest $request): JsonResponse + { + $data = $request->validated(); + + $boundary = $this->buildPolygon($data['boundary']); + $centroid = $this->calculateCentroid($boundary); + + // Spatial validation check + $cityId = $data['city_municipality_id']; + $tenant = Tenant::where('city_municipality_id', $cityId)->first(); + + if ($tenant && $tenant->boundary_polygon) { + $isContained = DB::selectOne( + 'SELECT ST_Contains(boundary_polygon, ST_GeomFromText(?, 4326, \'axis-order=long-lat\')) as contained FROM tenants WHERE id = ?', + [$boundary->toWkt(), $tenant->id] + ); + + if (! $isContained || ! $isContained->contained) { + return $this->fail( + 'The drawn Barangay boundary must lie completely within the LGU/Municipality boundary.', + ['boundary' => ['The Barangay boundary is outside the LGU border.']], + 422 + ); + } + } + + // Code auto-generation if empty + $city = CityMunicipality::findOrFail($cityId); + if (empty($data['psgc_code'])) { + $prefix = substr($city->psgc_code, 0, 9); + $maxLocal = Barangay::where('psgc_code', 'like', $prefix.'%') + ->whereRaw('LENGTH(psgc_code) = 12') + ->orderBy('psgc_code', 'desc') + ->first(); + $suffix = $maxLocal ? ((int) substr($maxLocal->psgc_code, -3) + 1) : 1; + $data['psgc_code'] = $prefix.str_pad($suffix, 3, '0', STR_PAD_LEFT); + } + + if (empty($data['code'])) { + $prefix = substr($city->code, 0, 12); + $maxLocal = Barangay::where('code', 'like', $prefix.'%') + ->orderBy('code', 'desc') + ->first(); + $suffix = $maxLocal ? ((int) substr(strrchr($maxLocal->code, '-'), 1) + 1) : 1; + $data['code'] = $prefix.'-'.str_pad($suffix, 3, '0', STR_PAD_LEFT); + } + + $barangay = Barangay::create([ + 'name' => $data['name'], + 'city_municipality_id' => $cityId, + 'psgc_code' => $data['psgc_code'], + 'code' => $data['code'], + 'urban_rural' => $data['urban_rural'], + 'population' => $data['population'], + 'boundary' => $boundary, + 'centroid' => $centroid, + ]); + + return $this->created(new BarangayResource($barangay), 'Barangay created successfully'); + } + + public function show(Barangay $barangay): JsonResponse + { + $barangay->load(['cityMunicipality.province']); + + return $this->ok(new BarangayResource($barangay)); + } + + public function update(UpdateBarangayRequest $request, Barangay $barangay): JsonResponse + { + $data = $request->validated(); + + $boundary = $this->buildPolygon($data['boundary']); + $centroid = $this->calculateCentroid($boundary); + + // Spatial validation check + $cityId = $data['city_municipality_id']; + $tenant = Tenant::where('city_municipality_id', $cityId)->first(); + + if ($tenant && $tenant->boundary_polygon) { + $isContained = DB::selectOne( + 'SELECT ST_Contains(boundary_polygon, ST_GeomFromText(?, 4326, \'axis-order=long-lat\')) as contained FROM tenants WHERE id = ?', + [$boundary->toWkt(), $tenant->id] + ); + + if (! $isContained || ! $isContained->contained) { + return $this->fail( + 'The drawn Barangay boundary must lie completely within the LGU/Municipality boundary.', + ['boundary' => ['The Barangay boundary is outside the LGU border.']], + 422 + ); + } + } + + // Code auto-generation if empty + $city = CityMunicipality::findOrFail($cityId); + if (empty($data['psgc_code'])) { + $prefix = substr($city->psgc_code, 0, 9); + $maxLocal = Barangay::where('psgc_code', 'like', $prefix.'%') + ->whereRaw('LENGTH(psgc_code) = 12') + ->orderBy('psgc_code', 'desc') + ->first(); + $suffix = $maxLocal ? ((int) substr($maxLocal->psgc_code, -3) + 1) : 1; + $data['psgc_code'] = $prefix.str_pad($suffix, 3, '0', STR_PAD_LEFT); + } + + if (empty($data['code'])) { + $prefix = substr($city->code, 0, 12); + $maxLocal = Barangay::where('code', 'like', $prefix.'%') + ->orderBy('code', 'desc') + ->first(); + $suffix = $maxLocal ? ((int) substr(strrchr($maxLocal->code, '-'), 1) + 1) : 1; + $data['code'] = $prefix.'-'.str_pad($suffix, 3, '0', STR_PAD_LEFT); + } + + $barangay->update([ + 'name' => $data['name'], + 'city_municipality_id' => $cityId, + 'psgc_code' => $data['psgc_code'], + 'code' => $data['code'], + 'urban_rural' => $data['urban_rural'], + 'population' => $data['population'], + 'boundary' => $boundary, + 'centroid' => $centroid, + ]); + + return $this->ok(new BarangayResource($barangay), 'Barangay updated successfully'); + } + + public function destroy(Barangay $barangay): JsonResponse + { + $barangay->delete(); + + return $this->ok(null, 'Barangay deleted successfully'); + } + + private function buildPolygon(array $points): Polygon + { + $ring = array_map( + fn ($p) => new Point((float) $p['lat'], (float) $p['lng'], 4326), + $points, + ); + + $first = $ring[0]; + $last = $ring[count($ring) - 1]; + if ($first->latitude !== $last->latitude || $first->longitude !== $last->longitude) { + $ring[] = new Point($first->latitude, $first->longitude, 4326); + } + + return new Polygon([new LineString($ring)], 4326); + } + + private function calculateCentroid(Polygon $polygon): Point + { + $res = DB::selectOne( + 'SELECT ST_AsText(ST_SRID(ST_Centroid(ST_GeomFromText(?, 0)), 4326)) as wkt', + [$polygon->toWkt()] + ); + + if ($res && preg_match('/POINT\(([^ ]+) ([^ ]+)\)/', $res->wkt, $matches)) { + $lat = (float) $matches[1]; + $lng = (float) $matches[2]; + + return new Point($lat, $lng, 4326); + } + + $rings = $polygon->getGeometries(); + $ring = $rings->first(); + if ($ring) { + $coords = $ring->getGeometries(); + $lats = $coords->map(fn ($p) => $p->latitude)->all(); + $lngs = $coords->map(fn ($p) => $p->longitude)->all(); + + return new Point(array_sum($lats) / count($lats), array_sum($lngs) / count($lngs), 4326); + } + + return new Point(0, 0, 4326); + } +} diff --git a/app/Http/Controllers/Api/V1/SuperAdmin/SuperAdminUserController.php b/app/Http/Controllers/Api/V1/SuperAdmin/SuperAdminUserController.php index 3a71c4b..49f73d2 100644 --- a/app/Http/Controllers/Api/V1/SuperAdmin/SuperAdminUserController.php +++ b/app/Http/Controllers/Api/V1/SuperAdmin/SuperAdminUserController.php @@ -116,7 +116,7 @@ class SuperAdminUserController extends ApiController 'tenant_id' => $data['tenant_id'] ?? null, ]; - if (!empty($data['password'])) { + if (! empty($data['password'])) { $updateData['password'] = Hash::make($data['password']); } diff --git a/app/Http/Middleware/EnsureUserHasRole.php b/app/Http/Middleware/EnsureUserHasRole.php index 6fb4700..01372db 100644 --- a/app/Http/Middleware/EnsureUserHasRole.php +++ b/app/Http/Middleware/EnsureUserHasRole.php @@ -3,6 +3,7 @@ namespace App\Http\Middleware; use App\Http\Responses\ApiResponse; +use App\Models\User; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; @@ -17,7 +18,7 @@ class EnsureUserHasRole return ApiResponse::error('Unauthenticated', null, Response::HTTP_UNAUTHORIZED); } - if ($user->role === \App\Models\User::ROLE_SUPER_ADMIN) { + if ($user->role === User::ROLE_SUPER_ADMIN) { return $next($request); } diff --git a/app/Http/Middleware/QueryTokenAuth.php b/app/Http/Middleware/QueryTokenAuth.php index df3e90a..e116881 100644 --- a/app/Http/Middleware/QueryTokenAuth.php +++ b/app/Http/Middleware/QueryTokenAuth.php @@ -9,8 +9,8 @@ class QueryTokenAuth { public function handle(Request $request, Closure $next) { - if ($request->has('token') && !$request->headers->has('Authorization')) { - $request->headers->set('Authorization', 'Bearer ' . $request->query('token')); + if ($request->has('token') && ! $request->headers->has('Authorization')) { + $request->headers->set('Authorization', 'Bearer '.$request->query('token')); } return $next($request); diff --git a/app/Http/Requests/Admin/StoreAdminHouseholdMemberRequest.php b/app/Http/Requests/Admin/StoreAdminHouseholdMemberRequest.php new file mode 100644 index 0000000..dcf88a4 --- /dev/null +++ b/app/Http/Requests/Admin/StoreAdminHouseholdMemberRequest.php @@ -0,0 +1,49 @@ + [ + 'required', + 'string', + Rule::in([ + HouseholdMember::RELATIONSHIP_HEAD, + HouseholdMember::RELATIONSHIP_SPOUSE, + HouseholdMember::RELATIONSHIP_CHILD, + HouseholdMember::RELATIONSHIP_PARENT, + HouseholdMember::RELATIONSHIP_SIBLING, + HouseholdMember::RELATIONSHIP_OTHER, + ]), + ], + 'user_id' => [ + 'nullable', + 'integer', + 'exists:users,id', + ], + 'full_name' => [ + 'required_without:user_id', + 'nullable', + 'string', + 'max:191', + ], + 'date_of_birth' => [ + 'nullable', + 'date', + 'before_or_equal:today', + ], + ]; + } +} diff --git a/app/Http/Requests/Admin/StoreAdminHouseholdRequest.php b/app/Http/Requests/Admin/StoreAdminHouseholdRequest.php new file mode 100644 index 0000000..54568a0 --- /dev/null +++ b/app/Http/Requests/Admin/StoreAdminHouseholdRequest.php @@ -0,0 +1,68 @@ + ['required', 'string', 'in:new,existing'], + + // Required if resident_type is existing + 'head_user_id' => [ + 'required_if:resident_type,existing', + 'nullable', + 'integer', + 'exists:users,id', + ], + + // Required if resident_type is new + 'first_name' => [ + 'required_if:resident_type,new', + 'nullable', + 'string', + 'max:100', + ], + 'last_name' => [ + 'required_if:resident_type,new', + 'nullable', + 'string', + 'max:100', + ], + 'email' => [ + 'required_if:resident_type,new', + 'nullable', + 'email', + 'max:255', + 'unique:users,email', + ], + 'phone' => [ + 'required_if:resident_type,new', + 'nullable', + 'string', + 'max:20', + 'unique:users,phone', + ], + 'password' => [ + 'nullable', + 'string', + 'min:8', + ], + + // Household fields (always required) + 'address_line' => ['required', 'string', 'max:255'], + 'lat' => ['required', 'numeric', 'between:-90,90'], + 'lng' => ['required', 'numeric', 'between:-180,180'], + 'barangay_id' => ['required', 'integer', 'exists:barangays,id'], + 'household_size' => ['required', 'integer', 'min:1', 'max:50'], + ]; + } +} diff --git a/app/Http/Requests/Admin/UpdateAdminHouseholdMemberRequest.php b/app/Http/Requests/Admin/UpdateAdminHouseholdMemberRequest.php new file mode 100644 index 0000000..0f8b356 --- /dev/null +++ b/app/Http/Requests/Admin/UpdateAdminHouseholdMemberRequest.php @@ -0,0 +1,43 @@ + [ + 'required', + 'string', + Rule::in([ + HouseholdMember::RELATIONSHIP_HEAD, + HouseholdMember::RELATIONSHIP_SPOUSE, + HouseholdMember::RELATIONSHIP_CHILD, + HouseholdMember::RELATIONSHIP_PARENT, + HouseholdMember::RELATIONSHIP_SIBLING, + HouseholdMember::RELATIONSHIP_OTHER, + ]), + ], + 'full_name' => [ + 'required', + 'string', + 'max:191', + ], + 'date_of_birth' => [ + 'nullable', + 'date', + 'before_or_equal:today', + ], + ]; + } +} diff --git a/app/Http/Requests/Admin/UpdateAdminHouseholdRequest.php b/app/Http/Requests/Admin/UpdateAdminHouseholdRequest.php new file mode 100644 index 0000000..fe74017 --- /dev/null +++ b/app/Http/Requests/Admin/UpdateAdminHouseholdRequest.php @@ -0,0 +1,24 @@ + ['required', 'string', 'max:255'], + 'lat' => ['required', 'numeric', 'between:-90,90'], + 'lng' => ['required', 'numeric', 'between:-180,180'], + 'barangay_id' => ['required', 'integer', 'exists:barangays,id'], + 'household_size' => ['required', 'integer', 'min:1', 'max:50'], + ]; + } +} diff --git a/app/Http/Requests/Auth/ChangePasswordRequest.php b/app/Http/Requests/Auth/ChangePasswordRequest.php index c39c800..bac22cd 100644 --- a/app/Http/Requests/Auth/ChangePasswordRequest.php +++ b/app/Http/Requests/Auth/ChangePasswordRequest.php @@ -7,7 +7,10 @@ use Illuminate\Validation\Rules\Password; class ChangePasswordRequest extends FormRequest { - public function authorize(): bool { return true; } + public function authorize(): bool + { + return true; + } public function rules(): array { diff --git a/app/Http/Requests/SuperAdmin/StoreBarangayRequest.php b/app/Http/Requests/SuperAdmin/StoreBarangayRequest.php new file mode 100644 index 0000000..942457f --- /dev/null +++ b/app/Http/Requests/SuperAdmin/StoreBarangayRequest.php @@ -0,0 +1,28 @@ + ['required', 'string', 'max:191'], + 'city_municipality_id' => ['required', 'integer', 'exists:cities_municipalities,id'], + 'psgc_code' => ['nullable', 'string', 'max:12', 'unique:barangays,psgc_code'], + 'code' => ['nullable', 'string', 'max:16', 'unique:barangays,code'], + 'urban_rural' => ['required', 'string', 'in:urban,rural,unknown'], + 'population' => ['nullable', 'integer', 'min:0'], + 'boundary' => ['required', 'array', 'min:3'], + 'boundary.*.lat' => ['required', 'numeric', 'between:-90,90'], + 'boundary.*.lng' => ['required', 'numeric', 'between:-180,180'], + ]; + } +} diff --git a/app/Http/Requests/SuperAdmin/StoreTenantRequest.php b/app/Http/Requests/SuperAdmin/StoreTenantRequest.php index ea55892..c9dd637 100644 --- a/app/Http/Requests/SuperAdmin/StoreTenantRequest.php +++ b/app/Http/Requests/SuperAdmin/StoreTenantRequest.php @@ -30,7 +30,7 @@ class StoreTenantRequest extends FormRequest 'contact_phone' => ['required', 'string', 'max:20'], 'status' => ['nullable', Rule::in([Tenant::STATUS_ONBOARDING, Tenant::STATUS_ACTIVE, Tenant::STATUS_SUSPENDED])], 'timezone' => ['nullable', 'string', 'max:100'], - + // Default Admin Account Info (Only required during creation) 'admin_email' => $isCreate ? ['required', 'email', 'max:191', 'unique:users,email'] : ['nullable'], 'admin_password' => $isCreate ? ['required', 'string', 'min:8'] : ['nullable'], diff --git a/app/Http/Requests/SuperAdmin/UpdateBarangayRequest.php b/app/Http/Requests/SuperAdmin/UpdateBarangayRequest.php new file mode 100644 index 0000000..84c6a4f --- /dev/null +++ b/app/Http/Requests/SuperAdmin/UpdateBarangayRequest.php @@ -0,0 +1,41 @@ +route('barangay'); + + return [ + 'name' => ['required', 'string', 'max:191'], + 'city_municipality_id' => ['required', 'integer', 'exists:cities_municipalities,id'], + 'psgc_code' => [ + 'nullable', + 'string', + 'max:12', + Rule::unique('barangays', 'psgc_code')->ignore($barangayId), + ], + 'code' => [ + 'nullable', + 'string', + 'max:16', + Rule::unique('barangays', 'code')->ignore($barangayId), + ], + 'urban_rural' => ['required', 'string', 'in:urban,rural,unknown'], + 'population' => ['nullable', 'integer', 'min:0'], + 'boundary' => ['required', 'array', 'min:3'], + 'boundary.*.lat' => ['required', 'numeric', 'between:-90,90'], + 'boundary.*.lng' => ['required', 'numeric', 'between:-180,180'], + ]; + } +} diff --git a/app/Http/Requests/Team/StoreCollectionTeamRequest.php b/app/Http/Requests/Team/StoreCollectionTeamRequest.php index 870a841..bf80e9b 100644 --- a/app/Http/Requests/Team/StoreCollectionTeamRequest.php +++ b/app/Http/Requests/Team/StoreCollectionTeamRequest.php @@ -8,7 +8,10 @@ use Illuminate\Validation\Rule; class StoreCollectionTeamRequest extends FormRequest { - public function authorize(): bool { return true; } + public function authorize(): bool + { + return true; + } public function rules(): array { diff --git a/app/Http/Requests/Team/StoreTruckRequest.php b/app/Http/Requests/Team/StoreTruckRequest.php index 6ff4e44..72de1f9 100644 --- a/app/Http/Requests/Team/StoreTruckRequest.php +++ b/app/Http/Requests/Team/StoreTruckRequest.php @@ -8,7 +8,10 @@ use Illuminate\Validation\Rule; class StoreTruckRequest extends FormRequest { - public function authorize(): bool { return true; } + public function authorize(): bool + { + return true; + } public function rules(): array { diff --git a/app/Http/Requests/Trip/StoreTripRequest.php b/app/Http/Requests/Trip/StoreTripRequest.php index fac84be..2329cbf 100644 --- a/app/Http/Requests/Trip/StoreTripRequest.php +++ b/app/Http/Requests/Trip/StoreTripRequest.php @@ -6,7 +6,10 @@ use Illuminate\Foundation\Http\FormRequest; class StoreTripRequest extends FormRequest { - public function authorize(): bool { return true; } + public function authorize(): bool + { + return true; + } public function rules(): array { diff --git a/app/Http/Resources/BarangayResource.php b/app/Http/Resources/BarangayResource.php index 3cafcb6..4714ac4 100644 --- a/app/Http/Resources/BarangayResource.php +++ b/app/Http/Resources/BarangayResource.php @@ -26,6 +26,7 @@ class BarangayResource extends JsonResource 'code' => $this->code, 'name' => $this->name, 'urban_rural' => $this->urban_rural, + 'population' => $this->population, 'city_municipality_id' => $this->city_municipality_id, 'centroid' => $this->centroid ? [ 'lat' => $this->centroid->latitude, diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 28d6dd9..e455d2f 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -29,6 +29,7 @@ class UserResource extends JsonResource 'boundary_polygon' => $this->tenant->boundary_polygon ? (function ($poly) { $rings = $poly->getGeometries(); $ring = $rings->first(); + return $ring ? $ring->getGeometries()->map(fn ($p) => ['lat' => $p->latitude, 'lng' => $p->longitude])->all() : null; })($this->tenant->boundary_polygon) : null, ] : null, diff --git a/app/Listeners/SendHouseholdApprovedNotification.php b/app/Listeners/SendHouseholdApprovedNotification.php index 245fc4e..e098e81 100644 --- a/app/Listeners/SendHouseholdApprovedNotification.php +++ b/app/Listeners/SendHouseholdApprovedNotification.php @@ -5,7 +5,6 @@ namespace App\Listeners; use App\Events\HouseholdVerified; use App\Models\QrCode; use App\Notifications\HouseholdApproved; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Notification; class SendHouseholdApprovedNotification @@ -14,7 +13,9 @@ class SendHouseholdApprovedNotification { $household = $event->household; $head = $household->head; - if (! $head) return; + if (! $head) { + return; + } // The free-allocation listener runs in parallel; query for the // count of active codes so the user sees the actual number. diff --git a/app/Listeners/SendQrBalanceLowNotification.php b/app/Listeners/SendQrBalanceLowNotification.php index 259eba7..859ec6e 100644 --- a/app/Listeners/SendQrBalanceLowNotification.php +++ b/app/Listeners/SendQrBalanceLowNotification.php @@ -11,7 +11,9 @@ class SendQrBalanceLowNotification public function handle(QrBalanceLow $event): void { $head = $event->household->head; - if (! $head) return; + if (! $head) { + return; + } Notification::send($head, new QrBalanceLowNotification( $event->household, $event->activeCount, $event->threshold, diff --git a/app/Models/Barangay.php b/app/Models/Barangay.php index 2ba20e0..d61ce1b 100644 --- a/app/Models/Barangay.php +++ b/app/Models/Barangay.php @@ -16,7 +16,9 @@ class Barangay extends Model use HasFactory, HasSpatial, SoftDeletes; public const URBAN = 'urban'; + public const RURAL = 'rural'; + public const URBAN_RURAL_UNKNOWN = 'unknown'; protected $fillable = [ diff --git a/app/Models/CityMunicipality.php b/app/Models/CityMunicipality.php index 1734feb..eb72927 100644 --- a/app/Models/CityMunicipality.php +++ b/app/Models/CityMunicipality.php @@ -15,7 +15,9 @@ class CityMunicipality extends Model protected $table = 'cities_municipalities'; public const TYPE_CITY = 'city'; + public const TYPE_MUNICIPALITY = 'municipality'; + public const TYPE_SUB_MUNICIPALITY = 'sub_municipality'; protected $fillable = [ diff --git a/app/Models/CollectionLog.php b/app/Models/CollectionLog.php index 52eba98..629faf4 100644 --- a/app/Models/CollectionLog.php +++ b/app/Models/CollectionLog.php @@ -14,8 +14,11 @@ class CollectionLog extends Model use HasFactory, HasSpatial, HasTenant; public const STATUS_VALID = 'valid'; + public const STATUS_INVALID = 'invalid'; + public const STATUS_DUPLICATE = 'duplicate'; + public const STATUS_EXPIRED = 'expired'; protected $fillable = [ diff --git a/app/Models/CollectionTeam.php b/app/Models/CollectionTeam.php index 59728ba..9084f48 100644 --- a/app/Models/CollectionTeam.php +++ b/app/Models/CollectionTeam.php @@ -15,6 +15,7 @@ class CollectionTeam extends Model use HasFactory, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; + public const STATUS_INACTIVE = 'inactive'; protected $fillable = [ @@ -30,7 +31,9 @@ class CollectionTeam extends Model protected static function booted(): void { static::creating(function (self $t): void { - if (empty($t->uuid)) $t->uuid = (string) Str::uuid(); + if (empty($t->uuid)) { + $t->uuid = (string) Str::uuid(); + } }); } diff --git a/app/Models/Concerns/HasProfileVerification.php b/app/Models/Concerns/HasProfileVerification.php index c028eb7..6bde8eb 100644 --- a/app/Models/Concerns/HasProfileVerification.php +++ b/app/Models/Concerns/HasProfileVerification.php @@ -12,7 +12,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; trait HasProfileVerification { public const VERIFICATION_PENDING = 'pending'; + public const VERIFICATION_APPROVED = 'approved'; + public const VERIFICATION_REJECTED = 'rejected'; public function verifiedByAdmin(): BelongsTo diff --git a/app/Models/DropOffPoint.php b/app/Models/DropOffPoint.php index 02f9127..f3b643a 100644 --- a/app/Models/DropOffPoint.php +++ b/app/Models/DropOffPoint.php @@ -17,7 +17,9 @@ class DropOffPoint extends Model use HasFactory, HasSpatial, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; + public const STATUS_MAINTENANCE = 'maintenance'; + public const STATUS_CLOSED = 'closed'; protected $fillable = [ diff --git a/app/Models/Dumpsite.php b/app/Models/Dumpsite.php index 1bcb5c6..ac547ec 100644 --- a/app/Models/Dumpsite.php +++ b/app/Models/Dumpsite.php @@ -19,7 +19,9 @@ class Dumpsite extends Model use HasFactory, HasSpatial, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; + public const STATUS_MAINTENANCE = 'maintenance'; + public const STATUS_CLOSED = 'closed'; protected $fillable = [ diff --git a/app/Models/HouseholdMember.php b/app/Models/HouseholdMember.php index 2822af7..d5a904e 100644 --- a/app/Models/HouseholdMember.php +++ b/app/Models/HouseholdMember.php @@ -12,10 +12,15 @@ class HouseholdMember extends Model use HasFactory, SoftDeletes; public const RELATIONSHIP_HEAD = 'head'; + public const RELATIONSHIP_SPOUSE = 'spouse'; + public const RELATIONSHIP_CHILD = 'child'; + public const RELATIONSHIP_PARENT = 'parent'; + public const RELATIONSHIP_SIBLING = 'sibling'; + public const RELATIONSHIP_OTHER = 'other'; protected $fillable = [ diff --git a/app/Models/OtpCode.php b/app/Models/OtpCode.php index 5063533..75dc648 100644 --- a/app/Models/OtpCode.php +++ b/app/Models/OtpCode.php @@ -11,11 +11,15 @@ class OtpCode extends Model use HasFactory; public const CHANNEL_SMS = 'sms'; + public const CHANNEL_EMAIL = 'email'; public const PURPOSE_REGISTER = 'register'; + public const PURPOSE_LOGIN = 'login'; + public const PURPOSE_PHONE_VERIFY = 'phone_verify'; + public const PURPOSE_PASSWORD_RESET = 'password_reset'; public const MAX_ATTEMPTS = 5; diff --git a/app/Models/PartnerStore.php b/app/Models/PartnerStore.php index a203004..1689f6a 100644 --- a/app/Models/PartnerStore.php +++ b/app/Models/PartnerStore.php @@ -18,7 +18,9 @@ class PartnerStore extends Model use HasFactory, HasSpatial, HasTenant, SoftDeletes; public const STATUS_PENDING_KYC = 'pending_kyc'; + public const STATUS_ACTIVE = 'active'; + public const STATUS_SUSPENDED = 'suspended'; protected $fillable = [ @@ -44,7 +46,9 @@ class PartnerStore extends Model protected static function booted(): void { static::creating(function (self $s): void { - if (empty($s->uuid)) $s->uuid = (string) Str::uuid(); + if (empty($s->uuid)) { + $s->uuid = (string) Str::uuid(); + } }); } diff --git a/app/Models/Payment.php b/app/Models/Payment.php index cf823cf..382b41e 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -15,15 +15,21 @@ class Payment extends Model use HasFactory, HasTenant, LogsActivity; public const PURPOSE_RESIDENT = 'resident_code_purchase'; + public const PURPOSE_STORE_WHOLESALE = 'store_inventory_purchase'; public const STATUS_PENDING = 'pending'; + public const STATUS_PROCESSING = 'processing'; + public const STATUS_PAID = 'paid'; + public const STATUS_FAILED = 'failed'; + public const STATUS_REFUNDED = 'refunded'; public const PROVIDER_PAYMONGO = 'paymongo'; + public const PROVIDER_MANUAL = 'manual'; protected $fillable = [ @@ -59,7 +65,9 @@ class Payment extends Model protected static function booted(): void { static::creating(function (self $p): void { - if (empty($p->uuid)) $p->uuid = (string) Str::uuid(); + if (empty($p->uuid)) { + $p->uuid = (string) Str::uuid(); + } }); } diff --git a/app/Models/QrCodeBatch.php b/app/Models/QrCodeBatch.php index cb6c312..6acea3a 100644 --- a/app/Models/QrCodeBatch.php +++ b/app/Models/QrCodeBatch.php @@ -14,7 +14,9 @@ class QrCodeBatch extends Model use HasFactory, HasTenant, SoftDeletes; public const PURPOSE_FREE = 'free_allocation'; + public const PURPOSE_STORE = 'store_inventory'; + public const PURPOSE_PROMO = 'promotional'; protected $table = 'qr_code_batches'; diff --git a/app/Models/Route.php b/app/Models/Route.php index 9315dc1..811deeb 100644 --- a/app/Models/Route.php +++ b/app/Models/Route.php @@ -15,6 +15,7 @@ class Route extends Model use HasFactory, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; + public const STATUS_INACTIVE = 'inactive'; protected $fillable = [ diff --git a/app/Models/ServiceArea.php b/app/Models/ServiceArea.php index 117503a..11b5513 100644 --- a/app/Models/ServiceArea.php +++ b/app/Models/ServiceArea.php @@ -14,6 +14,7 @@ class ServiceArea extends Model use HasFactory, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; + public const STATUS_INACTIVE = 'inactive'; protected $fillable = [ diff --git a/app/Models/TeamMember.php b/app/Models/TeamMember.php index c680301..5e907b8 100644 --- a/app/Models/TeamMember.php +++ b/app/Models/TeamMember.php @@ -11,8 +11,11 @@ class TeamMember extends Model use HasFactory; public const ROLE_DRIVER = 'driver'; + public const ROLE_HELPER = 'helper'; + public const ROLE_SCANNER = 'scanner'; + public const ROLE_LEAD = 'lead'; protected $fillable = [ diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php index dbac5ba..4fde07f 100644 --- a/app/Models/Tenant.php +++ b/app/Models/Tenant.php @@ -2,10 +2,10 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Str; use MatanYadaev\EloquentSpatial\Objects\Polygon; @@ -13,10 +13,12 @@ use MatanYadaev\EloquentSpatial\Traits\HasSpatial; class Tenant extends Model { - use HasFactory, SoftDeletes, HasSpatial; + use HasFactory, HasSpatial, SoftDeletes; public const STATUS_ONBOARDING = 'onboarding'; + public const STATUS_ACTIVE = 'active'; + public const STATUS_SUSPENDED = 'suspended'; protected $fillable = [ @@ -56,6 +58,7 @@ class Tenant extends Model if ($city?->code) { return strtoupper($city->code); } + return strtoupper(Str::slug($name)); } @@ -64,17 +67,17 @@ class Tenant extends Model return $this->belongsTo(CityMunicipality::class); } - public function users(): \Illuminate\Database\Eloquent\Relations\HasMany + public function users(): HasMany { return $this->hasMany(User::class); } - public function households(): \Illuminate\Database\Eloquent\Relations\HasMany + public function households(): HasMany { return $this->hasMany(Household::class); } - public function trucks(): \Illuminate\Database\Eloquent\Relations\HasMany + public function trucks(): HasMany { return $this->hasMany(Truck::class); } diff --git a/app/Models/Trip.php b/app/Models/Trip.php index ca2fc96..ebdbbc2 100644 --- a/app/Models/Trip.php +++ b/app/Models/Trip.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Tenancy\HasTenant; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -17,9 +18,13 @@ class Trip extends Model use HasFactory, HasTenant, LogsActivity, SoftDeletes; public const STATUS_SCHEDULED = 'scheduled'; + public const STATUS_IN_PROGRESS = 'in_progress'; + public const STATUS_AT_DUMPSITE = 'at_dumpsite'; + public const STATUS_COMPLETED = 'completed'; + public const STATUS_CANCELLED = 'cancelled'; protected $fillable = [ @@ -58,9 +63,11 @@ class Trip extends Model protected static function booted(): void { static::creating(function (self $t): void { - if (empty($t->uuid)) $t->uuid = (string) Str::uuid(); + if (empty($t->uuid)) { + $t->uuid = (string) Str::uuid(); + } if (empty($t->trip_number)) { - $date = ($t->scheduled_date ? \Carbon\Carbon::parse($t->scheduled_date) : now())->format('Ymd'); + $date = ($t->scheduled_date ? Carbon::parse($t->scheduled_date) : now())->format('Ymd'); $seq = self::where('trip_number', 'like', "TRIP-{$date}-%")->count() + 1; $t->trip_number = sprintf('TRIP-%s-%03d', $date, $seq); } @@ -69,7 +76,7 @@ class Trip extends Model public function route(): BelongsTo { - return $this->belongsTo(\App\Models\Route::class); + return $this->belongsTo(Route::class); } public function team(): BelongsTo diff --git a/app/Models/TripStop.php b/app/Models/TripStop.php index 7bb2160..2f0a512 100644 --- a/app/Models/TripStop.php +++ b/app/Models/TripStop.php @@ -13,8 +13,11 @@ class TripStop extends Model use HasFactory, HasSpatial; public const STATUS_PENDING = 'pending'; + public const STATUS_ARRIVED = 'arrived'; + public const STATUS_COMPLETED = 'completed'; + public const STATUS_SKIPPED = 'skipped'; protected $fillable = [ diff --git a/app/Models/TripTimelineEvent.php b/app/Models/TripTimelineEvent.php index f9412fe..dea024a 100644 --- a/app/Models/TripTimelineEvent.php +++ b/app/Models/TripTimelineEvent.php @@ -14,21 +14,35 @@ class TripTimelineEvent extends Model use HasFactory, HasSpatial; public const TYPE_TRIP_STARTED = 'trip_started'; + public const TYPE_ARRIVED_AT_STOP = 'arrived_at_stop'; + public const TYPE_COLLECTION_STARTED = 'collection_started'; + public const TYPE_QR_SCANNED = 'qr_scanned'; + public const TYPE_COLLECTION_COMPLETED = 'collection_completed'; + public const TYPE_DEPARTED_STOP = 'departed_stop'; + public const TYPE_STOP_SKIPPED = 'stop_skipped'; + public const TYPE_TRUCK_FULL_WARNING = 'truck_full_warning'; + public const TYPE_ARRIVED_AT_DUMPSITE = 'arrived_at_dumpsite'; + public const TYPE_LOAD_RELEASED = 'load_released'; + public const TYPE_DEPARTED_DUMPSITE = 'departed_dumpsite'; + public const TYPE_TRIP_COMPLETED = 'trip_completed'; + public const TYPE_INCIDENT_REPORTED = 'incident_reported'; + public const TYPE_BREAKDOWN = 'breakdown'; public $timestamps = false; + protected $dateFormat = 'Y-m-d H:i:s'; protected $fillable = [ diff --git a/app/Models/Truck.php b/app/Models/Truck.php index 1e882fa..3f9d2a6 100644 --- a/app/Models/Truck.php +++ b/app/Models/Truck.php @@ -16,7 +16,9 @@ class Truck extends Model use HasFactory, HasSpatial, HasTenant, SoftDeletes; public const STATUS_ACTIVE = 'active'; + public const STATUS_MAINTENANCE = 'maintenance'; + public const STATUS_RETIRED = 'retired'; protected $fillable = [ @@ -42,7 +44,9 @@ class Truck extends Model protected static function booted(): void { static::creating(function (self $t): void { - if (empty($t->uuid)) $t->uuid = (string) Str::uuid(); + if (empty($t->uuid)) { + $t->uuid = (string) Str::uuid(); + } }); } diff --git a/app/Models/User.php b/app/Models/User.php index c9e2ba7..1541155 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,9 +2,12 @@ namespace App\Models; +use App\Notifications\VerifyEmailNotification; +use Database\Factories\UserFactory; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -17,19 +20,27 @@ use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable implements MustVerifyEmail { - /** @use HasFactory<\Database\Factories\UserFactory> */ + /** @use HasFactory */ use HasApiTokens, HasFactory, HasRoles, LogsActivity, Notifiable, SoftDeletes; public const ROLE_SUPER_ADMIN = 'super_admin'; + public const ROLE_ADMIN = 'admin'; + public const ROLE_RESIDENT = 'resident'; + public const ROLE_DRIVER = 'driver'; + public const ROLE_HELPER = 'helper'; + public const ROLE_SCANNER = 'scanner'; + public const ROLE_STORE_PARTNER = 'store_partner'; public const STATUS_ACTIVE = 'active'; + public const STATUS_SUSPENDED = 'suspended'; + public const STATUS_PENDING = 'pending'; protected $fillable = [ @@ -91,7 +102,7 @@ class User extends Authenticatable implements MustVerifyEmail public function sendEmailVerificationNotification(): void { - $this->notify(new \App\Notifications\VerifyEmailNotification()); + $this->notify(new VerifyEmailNotification); } public function headedHousehold(): HasOne @@ -129,7 +140,7 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasOne(StorePartnerProfile::class); } - public function tenant(): \Illuminate\Database\Eloquent\Relations\BelongsTo + public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class); } diff --git a/app/Models/WeeklyRoutePerformance.php b/app/Models/WeeklyRoutePerformance.php index 520b0ea..01564ec 100644 --- a/app/Models/WeeklyRoutePerformance.php +++ b/app/Models/WeeklyRoutePerformance.php @@ -26,6 +26,6 @@ class WeeklyRoutePerformance extends Model public function route(): BelongsTo { - return $this->belongsTo(\App\Models\Route::class); + return $this->belongsTo(Route::class); } } diff --git a/app/Notifications/Channels/PushChannel.php b/app/Notifications/Channels/PushChannel.php index 823935e..bd1015f 100644 --- a/app/Notifications/Channels/PushChannel.php +++ b/app/Notifications/Channels/PushChannel.php @@ -12,12 +12,20 @@ class PushChannel public function send(mixed $notifiable, Notification $notification): void { - if (! $notifiable instanceof User) return; - if (! $notifiable->fcm_token) return; - if (! method_exists($notification, 'toPush')) return; + if (! $notifiable instanceof User) { + return; + } + if (! $notifiable->fcm_token) { + return; + } + if (! method_exists($notification, 'toPush')) { + return; + } $payload = $notification->toPush($notifiable); - if (empty($payload['title']) || empty($payload['body'])) return; + if (empty($payload['title']) || empty($payload['body'])) { + return; + } $this->driver->send( token: $notifiable->fcm_token, diff --git a/app/Notifications/Concerns/RoutesByPreferences.php b/app/Notifications/Concerns/RoutesByPreferences.php index 9029cf2..23eb5fe 100644 --- a/app/Notifications/Concerns/RoutesByPreferences.php +++ b/app/Notifications/Concerns/RoutesByPreferences.php @@ -29,10 +29,14 @@ trait RoutesByPreferences $channels = ['database']; if ($prefs?->sms_enabled ?? true) { - if ($notifiable->phone) $channels[] = SmsChannel::class; + if ($notifiable->phone) { + $channels[] = SmsChannel::class; + } } if ($prefs?->push_enabled ?? true) { - if ($notifiable->fcm_token) $channels[] = PushChannel::class; + if ($notifiable->fcm_token) { + $channels[] = PushChannel::class; + } } return $channels; diff --git a/app/Notifications/HouseholdApproved.php b/app/Notifications/HouseholdApproved.php index da7908a..75781e5 100644 --- a/app/Notifications/HouseholdApproved.php +++ b/app/Notifications/HouseholdApproved.php @@ -30,7 +30,7 @@ class HouseholdApproved extends Notification { return [ 'to' => $notifiable->phone, - 'message' => "Verde: Your household is verified." + 'message' => 'Verde: Your household is verified.' .($this->codesAllocated > 0 ? " {$this->codesAllocated} QR codes are ready in your wallet." : '') .' Open the app to see details.', ]; diff --git a/app/Notifications/VerifyEmailNotification.php b/app/Notifications/VerifyEmailNotification.php index 29a11c8..6512601 100644 --- a/app/Notifications/VerifyEmailNotification.php +++ b/app/Notifications/VerifyEmailNotification.php @@ -31,7 +31,7 @@ class VerifyEmailNotification extends BaseVerifyEmail protected function buildMailMessage($url): MailMessage { - return (new MailMessage()) + return (new MailMessage) ->subject('Verify your Verde email') ->greeting('Hello,') ->line('Tap the button below to confirm your Verde account email address.') diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8e5c88d..83ec342 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,8 +3,8 @@ namespace App\Providers; use App\Services\Payment\ManualPaymentDriver; -use App\Services\Payment\PayMongoDriver; use App\Services\Payment\PaymentDriver; +use App\Services\Payment\PayMongoDriver; use App\Services\Push\FcmPushDriver; use App\Services\Push\LogPushDriver; use App\Services\Push\PushDriver; @@ -32,7 +32,7 @@ class AppServiceProvider extends ServiceProvider ); } - return new ManualPaymentDriver(); + return new ManualPaymentDriver; }); $this->app->singleton(PushDriver::class, function ($app) { @@ -43,7 +43,7 @@ class AppServiceProvider extends ServiceProvider return new FcmPushDriver($key); } - return new LogPushDriver(); + return new LogPushDriver; }); $this->app->singleton(SmsService::class, function ($app) { @@ -54,8 +54,8 @@ class AppServiceProvider extends ServiceProvider apiKey: (string) config('services.semaphore.api_key'), senderName: (string) config('services.semaphore.sender_name', 'VERDE'), ), - 'fake' => new FakeSmsService(), - default => new LogSmsService(), + 'fake' => new FakeSmsService, + default => new LogSmsService, }; }); } diff --git a/app/Services/Otp/OtpVerifyResult.php b/app/Services/Otp/OtpVerifyResult.php index c72cefc..973bacc 100644 --- a/app/Services/Otp/OtpVerifyResult.php +++ b/app/Services/Otp/OtpVerifyResult.php @@ -7,9 +7,13 @@ use App\Models\OtpCode; class OtpVerifyResult { public const STATUS_OK = 'ok'; + public const STATUS_INVALID = 'invalid'; + public const STATUS_EXPIRED = 'expired'; + public const STATUS_EXHAUSTED = 'exhausted'; + public const STATUS_NOT_FOUND = 'not_found'; public function __construct( diff --git a/app/Services/Payment/PayMongoDriver.php b/app/Services/Payment/PayMongoDriver.php index 07af3d5..7ad9504 100644 --- a/app/Services/Payment/PayMongoDriver.php +++ b/app/Services/Payment/PayMongoDriver.php @@ -98,10 +98,14 @@ class PayMongoDriver implements PaymentDriver { $eventType = $payload['data']['attributes']['type'] ?? null; $paymentUuid = $payload['data']['attributes']['data']['attributes']['metadata']['payment_uuid'] ?? null; - if (! $paymentUuid) return null; + if (! $paymentUuid) { + return null; + } $payment = Payment::where('uuid', $paymentUuid)->first(); - if (! $payment) return null; + if (! $payment) { + return null; + } if (in_array($eventType, ['checkout_session.payment.paid', 'payment.paid'], true)) { $payment->forceFill([ diff --git a/app/Services/Push/PushResult.php b/app/Services/Push/PushResult.php index 756f40c..d31ce7e 100644 --- a/app/Services/Push/PushResult.php +++ b/app/Services/Push/PushResult.php @@ -10,6 +10,13 @@ class PushResult public readonly ?string $error = null, ) {} - public static function success(?string $id = null): self { return new self(true, $id); } - public static function failure(string $error): self { return new self(false, null, $error); } + public static function success(?string $id = null): self + { + return new self(true, $id); + } + + public static function failure(string $error): self + { + return new self(false, null, $error); + } } diff --git a/app/Services/Route/RouteCalculator.php b/app/Services/Route/RouteCalculator.php index 21fc887..accf5d0 100644 --- a/app/Services/Route/RouteCalculator.php +++ b/app/Services/Route/RouteCalculator.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\DB; class RouteCalculator { public const DEFAULT_AVG_SPEED_KMH = 25.0; + public const DUMPSITE_DWELL_MINUTES = 20; /** diff --git a/app/Services/Scan/ScanService.php b/app/Services/Scan/ScanService.php index f611774..cddadc7 100644 --- a/app/Services/Scan/ScanService.php +++ b/app/Services/Scan/ScanService.php @@ -7,6 +7,7 @@ use App\Models\DropOffPoint; use App\Models\QrCode; use App\Models\Trip; use App\Models\TripStop; +use App\Models\TripTimelineEvent; use App\Models\User; use App\Services\Qr\QrAllocator; use App\Services\Trip\TripExecutor; @@ -108,7 +109,7 @@ class ScanService if ($trip) { $this->tripExecutor->log( $trip, - \App\Models\TripTimelineEvent::TYPE_QR_SCANNED, + TripTimelineEvent::TYPE_QR_SCANNED, $scanner, $lat, $lng, relatedType: CollectionLog::class, diff --git a/app/Services/Store/StoreOperations.php b/app/Services/Store/StoreOperations.php index 13b8d0b..56aa17f 100644 --- a/app/Services/Store/StoreOperations.php +++ b/app/Services/Store/StoreOperations.php @@ -9,11 +9,13 @@ use App\Models\QrCodeBatch; use App\Models\StoreInventory; use App\Models\StorePurchase; use App\Models\StoreSale; +use App\Notifications\CodesPurchased; use App\Services\Qr\BatchGenerator; use App\States\QrCode\Active; use App\States\QrCode\Allocated; use App\States\QrCode\Unassigned; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Notification; class StoreOperations { @@ -122,9 +124,9 @@ class StoreOperations // Notify the household head — fire after commit so the receiver // sees the persisted state. if ($household->head) { - \Illuminate\Support\Facades\Notification::send( + Notification::send( $household->head, - new \App\Notifications\CodesPurchased($quantity, $totalRetail, $store->business_name), + new CodesPurchased($quantity, $totalRetail, $store->business_name), ); } diff --git a/app/Services/Trip/TripExecutor.php b/app/Services/Trip/TripExecutor.php index 67e3caf..f634b35 100644 --- a/app/Services/Trip/TripExecutor.php +++ b/app/Services/Trip/TripExecutor.php @@ -3,12 +3,15 @@ namespace App\Services\Trip; use App\Models\DumpsiteRelease; +use App\Models\Household; use App\Models\Trip; use App\Models\TripStop; use App\Models\TripTimelineEvent; use App\Models\User; +use App\Notifications\PickupImminent; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Notification; use MatanYadaev\EloquentSpatial\Objects\Point; class TripExecutor @@ -71,19 +74,23 @@ class TripExecutor { $stop->loadMissing('dropOffPoint'); $dop = $stop->dropOffPoint; - if (! $dop) return; + if (! $dop) { + return; + } - $heads = \App\Models\User::query() - ->whereIn('id', \App\Models\Household::query() + $heads = User::query() + ->whereIn('id', Household::query() ->where('assigned_drop_off_point_id', $dop->id) ->pluck('head_user_id')) ->get(); - if ($heads->isEmpty()) return; + if ($heads->isEmpty()) { + return; + } - \Illuminate\Support\Facades\Notification::send( + Notification::send( $heads, - new \App\Notifications\PickupImminent($dop), + new PickupImminent($dop), ); } diff --git a/app/States/QrCode/QrCodeState.php b/app/States/QrCode/QrCodeState.php index 4cbb992..58f1bde 100644 --- a/app/States/QrCode/QrCodeState.php +++ b/app/States/QrCode/QrCodeState.php @@ -8,10 +8,15 @@ use Spatie\ModelStates\StateConfig; abstract class QrCodeState extends State { public const UNASSIGNED = 'unassigned'; + public const ALLOCATED = 'allocated'; + public const ACTIVE = 'active'; + public const USED = 'used'; + public const EXPIRED = 'expired'; + public const VOIDED = 'voided'; public static function config(): StateConfig diff --git a/app/Tenancy/HasTenant.php b/app/Tenancy/HasTenant.php index 37d17cf..e4d2fa5 100644 --- a/app/Tenancy/HasTenant.php +++ b/app/Tenancy/HasTenant.php @@ -2,6 +2,7 @@ namespace App\Tenancy; +use App\Models\Tenant; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -16,7 +17,7 @@ trait HasTenant { public static function bootHasTenant(): void { - static::addGlobalScope(new TenantScope()); + static::addGlobalScope(new TenantScope); static::creating(function (Model $model): void { if (empty($model->tenant_id) && Tenancy::current()) { @@ -27,6 +28,6 @@ trait HasTenant public function tenant(): BelongsTo { - return $this->belongsTo(\App\Models\Tenant::class); + return $this->belongsTo(Tenant::class); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 538324b..92112e4 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,8 @@ statefulApi(); $middleware->alias([ - 'role' => \App\Http\Middleware\EnsureUserHasRole::class, - 'tenant' => \App\Http\Middleware\ResolveTenant::class, + 'role' => EnsureUserHasRole::class, + 'tenant' => ResolveTenant::class, ]); // Run tenant resolution on every API request — public lookups // need it too so the bookkeeping is consistent. - $middleware->prependToGroup('api', \App\Http\Middleware\QueryTokenAuth::class); - $middleware->appendToGroup('api', \App\Http\Middleware\ResolveTenant::class); + $middleware->prependToGroup('api', QueryTokenAuth::class); + $middleware->appendToGroup('api', ResolveTenant::class); $middleware->redirectGuestsTo(function (Request $request) { return $request->is('api/*') ? null : null; @@ -40,7 +44,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withExceptions(function (Exceptions $exceptions) { // Report unhandled exceptions to Sentry. No-op when SENTRY_LARAVEL_DSN // isn't set (e.g., local/testing) — keeps the dev loop quiet. - \Sentry\Laravel\Integration::handles($exceptions); + Integration::handles($exceptions); $exceptions->shouldRenderJsonWhen(function (Request $request) { return $request->is('api/*') || $request->expectsJson(); diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 38b258d..fc94ae6 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,5 +1,7 @@ \Spatie\Activitylog\Models\Activity::class, + 'activity_model' => Activity::class, /* * This is the name of the table that will be created by the migration and diff --git a/config/auth.php b/config/auth.php index 0ba5d5d..9daae00 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,5 +1,7 @@ [ 'users' => [ 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', App\Models\User::class), + 'model' => env('AUTH_MODEL', User::class), ], // 'users' => [ diff --git a/config/sanctum.php b/config/sanctum.php index 44527d6..cde73cf 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -1,5 +1,8 @@ [ - 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, - 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + 'authenticate_session' => AuthenticateSession::class, + 'encrypt_cookies' => EncryptCookies::class, + 'validate_csrf_token' => ValidateCsrfToken::class, ], ]; diff --git a/database/factories/DropOffPointFactory.php b/database/factories/DropOffPointFactory.php index 5ff0293..612aa56 100644 --- a/database/factories/DropOffPointFactory.php +++ b/database/factories/DropOffPointFactory.php @@ -8,7 +8,7 @@ use Illuminate\Support\Str; use MatanYadaev\EloquentSpatial\Objects\Point; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\DropOffPoint> + * @extends Factory */ class DropOffPointFactory extends Factory { diff --git a/database/factories/DumpsiteFactory.php b/database/factories/DumpsiteFactory.php index 88f13f9..3b50000 100644 --- a/database/factories/DumpsiteFactory.php +++ b/database/factories/DumpsiteFactory.php @@ -10,7 +10,7 @@ use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Dumpsite> + * @extends Factory */ class DumpsiteFactory extends Factory { diff --git a/database/factories/HouseholdFactory.php b/database/factories/HouseholdFactory.php index f085b83..d582d52 100644 --- a/database/factories/HouseholdFactory.php +++ b/database/factories/HouseholdFactory.php @@ -9,7 +9,7 @@ use Illuminate\Support\Str; use MatanYadaev\EloquentSpatial\Objects\Point; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Household> + * @extends Factory */ class HouseholdFactory extends Factory { diff --git a/database/factories/PartnerStoreFactory.php b/database/factories/PartnerStoreFactory.php index a2fbc2c..ebec011 100644 --- a/database/factories/PartnerStoreFactory.php +++ b/database/factories/PartnerStoreFactory.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\PartnerStore> + * @extends Factory */ class PartnerStoreFactory extends Factory { diff --git a/database/factories/ServiceAreaFactory.php b/database/factories/ServiceAreaFactory.php index 188b609..1511a7b 100644 --- a/database/factories/ServiceAreaFactory.php +++ b/database/factories/ServiceAreaFactory.php @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ServiceArea> + * @extends Factory */ class ServiceAreaFactory extends Factory { diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index eb61aa3..470a883 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -2,12 +2,14 @@ namespace Database\Factories; +use App\Models\User; +use App\Tenancy\Tenancy; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> + * @extends Factory */ class UserFactory extends Factory { @@ -25,15 +27,15 @@ class UserFactory extends Factory { return [ 'uuid' => (string) Str::uuid(), - 'tenant_id' => \App\Tenancy\Tenancy::current()?->id, + 'tenant_id' => Tenancy::current()?->id, 'first_name' => fake()->firstName(), 'last_name' => fake()->lastName(), 'email' => fake()->unique()->safeEmail(), 'phone' => fake()->unique()->numerify('+639#########'), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), - 'role' => \App\Models\User::ROLE_RESIDENT, - 'status' => \App\Models\User::STATUS_ACTIVE, + 'role' => User::ROLE_RESIDENT, + 'status' => User::STATUS_ACTIVE, 'preferred_language' => 'en', 'remember_token' => Str::random(10), ]; diff --git a/database/migrations/2026_04_29_035508_create_activity_log_table.php b/database/migrations/2026_04_29_035508_create_activity_log_table.php index 7c05bc8..b788f65 100644 --- a/database/migrations/2026_04_29_035508_create_activity_log_table.php +++ b/database/migrations/2026_04_29_035508_create_activity_log_table.php @@ -1,8 +1,8 @@ whereNull('boundary')->count(); if ($missing > 0) { @@ -32,7 +34,9 @@ return new class extends Migration $exists = collect(DB::select('SHOW INDEX FROM barangays')) ->contains(fn ($i) => $i->Key_name === 'barangays_boundary_spx'); - if ($exists) return; + if ($exists) { + return; + } // Tighten column to NOT NULL + add the spatial index. DB::statement('ALTER TABLE barangays MODIFY boundary GEOMETRY NOT NULL SRID 4326'); @@ -41,7 +45,9 @@ return new class extends Migration public function down(): void { - if (! Schema::hasTable('barangays')) return; + if (! Schema::hasTable('barangays')) { + return; + } $exists = collect(DB::select('SHOW INDEX FROM barangays')) ->contains(fn ($i) => $i->Key_name === 'barangays_boundary_spx'); diff --git a/database/seeders/DemoResidentSeeder.php b/database/seeders/DemoResidentSeeder.php index 564f871..98247c5 100644 --- a/database/seeders/DemoResidentSeeder.php +++ b/database/seeders/DemoResidentSeeder.php @@ -31,6 +31,7 @@ class DemoResidentSeeder extends Seeder $barangay = Barangay::first(); if (! $barangay) { $this->command->warn('No barangay seeded — run SamplePsgcSeeder first.'); + return; } diff --git a/database/seeders/DemoScenarioSeeder.php b/database/seeders/DemoScenarioSeeder.php index bfa32d1..5b884a3 100644 --- a/database/seeders/DemoScenarioSeeder.php +++ b/database/seeders/DemoScenarioSeeder.php @@ -24,6 +24,7 @@ use App\Models\Truck; use App\Models\User; use App\Services\LiveTracking\TruckTracker; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; @@ -54,6 +55,7 @@ class DemoScenarioSeeder extends Seeder $juanHousehold = $juan ? Household::where('head_user_id', $juan->id)->first() : null; if (! $juan || ! $juanHousehold) { $this->command->warn('Run DemoResidentSeeder first.'); + return; } @@ -61,6 +63,7 @@ class DemoScenarioSeeder extends Seeder $dops = DropOffPoint::orderBy('id')->get(); if ($dops->count() < 1 || ! $dumpsite) { $this->command->warn('Need DOPs and a dumpsite seeded first.'); + return; } @@ -69,12 +72,12 @@ class DemoScenarioSeeder extends Seeder $this->command->info(" Partner stores: {$stores->count()} active"); // 2) Staff users + profiles - $driver = $this->createStaff('driver1@verde.local', '+639180000001', 'Carlos', 'Reyes', User::ROLE_DRIVER); - $helper = $this->createStaff('helper1@verde.local', '+639180000002', 'Mario', 'Cruz', User::ROLE_HELPER); - $scanner = $this->createStaff('scanner1@verde.local', '+639180000003', 'Imelda', 'Lopez', User::ROLE_SCANNER); + $driver = $this->createStaff('driver1@verde.local', '+639180000001', 'Carlos', 'Reyes', User::ROLE_DRIVER); + $helper = $this->createStaff('helper1@verde.local', '+639180000002', 'Mario', 'Cruz', User::ROLE_HELPER); + $scanner = $this->createStaff('scanner1@verde.local', '+639180000003', 'Imelda', 'Lopez', User::ROLE_SCANNER); - DriverProfile::updateOrCreate(['user_id' => $driver->id], ['license_number' => 'D-12345', 'verification_status' => 'approved', 'verified_at' => now()]); - HelperProfile::updateOrCreate(['user_id' => $helper->id], ['verification_status' => 'approved', 'verified_at' => now()]); + DriverProfile::updateOrCreate(['user_id' => $driver->id], ['license_number' => 'D-12345', 'verification_status' => 'approved', 'verified_at' => now()]); + HelperProfile::updateOrCreate(['user_id' => $helper->id], ['verification_status' => 'approved', 'verified_at' => now()]); ScannerProfile::updateOrCreate(['user_id' => $scanner->id], ['verification_status' => 'approved', 'verified_at' => now()]); // 3) Truck @@ -131,9 +134,9 @@ class DemoScenarioSeeder extends Seeder } // 6) Trips — today (in_progress), tomorrow, +3 days - $todayTrip = $this->seedTrip($route, $team, $truck, $dumpsite, today(), Trip::STATUS_IN_PROGRESS, 'TRIP-DEMO-TODAY'); - $tomorrowTrip = $this->seedTrip($route, $team, $truck, $dumpsite, today()->addDay(), Trip::STATUS_SCHEDULED, 'TRIP-DEMO-TMRW'); - $futureTrip = $this->seedTrip($route, $team, $truck, $dumpsite, today()->addDays(3), Trip::STATUS_SCHEDULED, 'TRIP-DEMO-FUTURE'); + $todayTrip = $this->seedTrip($route, $team, $truck, $dumpsite, today(), Trip::STATUS_IN_PROGRESS, 'TRIP-DEMO-TODAY'); + $tomorrowTrip = $this->seedTrip($route, $team, $truck, $dumpsite, today()->addDay(), Trip::STATUS_SCHEDULED, 'TRIP-DEMO-TMRW'); + $futureTrip = $this->seedTrip($route, $team, $truck, $dumpsite, today()->addDays(3), Trip::STATUS_SCHEDULED, 'TRIP-DEMO-FUTURE'); // 7) Past collection logs for Juan (use a few of his "used" or active codes) $this->seedCollectionsForJuan($juanHousehold, $todayTrip, $scanner); @@ -158,10 +161,12 @@ class DemoScenarioSeeder extends Seeder $this->command->info(' Trips: today (in_progress), tomorrow (scheduled), +3 days'); } - private function seedStores(Household $juanHousehold): \Illuminate\Database\Eloquent\Collection + private function seedStores(Household $juanHousehold): Collection { $center = $juanHousehold->coordinates; - if (! $center) return PartnerStore::query()->get(); + if (! $center) { + return PartnerStore::query()->get(); + } $samples = [ ['name' => 'Aling Nena Sari-Sari', 'lat_offset' => 0.001, 'lng_offset' => 0.0008, 'addr' => '24 Sampaguita St'], @@ -231,6 +236,7 @@ class DemoScenarioSeeder extends Seeder ); $u->syncRoles([$role]); NotificationPreference::firstOrCreate(['user_id' => $u->id]); + return $u; } @@ -277,10 +283,14 @@ class DemoScenarioSeeder extends Seeder ->where('status', 'active') ->limit(5) ->get(); - if ($codes->isEmpty()) return; + if ($codes->isEmpty()) { + return; + } $dop = $juanHousehold->assignedDropOffPoint; - if (! $dop) return; + if (! $dop) { + return; + } $tripStop = $trip->stops()->where('drop_off_point_id', $dop->id)->first(); $weights = [3.4, 2.8, 4.1, 3.6, 2.2]; diff --git a/database/seeders/SamplePsgcSeeder.php b/database/seeders/SamplePsgcSeeder.php index a58fdab..798d215 100644 --- a/database/seeders/SamplePsgcSeeder.php +++ b/database/seeders/SamplePsgcSeeder.php @@ -48,7 +48,7 @@ class SamplePsgcSeeder extends Seeder ['psgc_code' => '110000000', 'code' => 'R11', 'name' => 'Davao Region', 'island_group' => 'Mindanao'], ['psgc_code' => '120000000', 'code' => 'R12', 'name' => 'SOCCSKSARGEN', 'island_group' => 'Mindanao'], ['psgc_code' => '130000000', 'code' => 'NCR', 'name' => 'National Capital Region', 'island_group' => 'Luzon'], - ['psgc_code' => '140000000', 'code' => 'CAR', 'name' => 'Cordillera Administrative Region','island_group' => 'Luzon'], + ['psgc_code' => '140000000', 'code' => 'CAR', 'name' => 'Cordillera Administrative Region', 'island_group' => 'Luzon'], ['psgc_code' => '160000000', 'code' => 'R13', 'name' => 'Caraga', 'island_group' => 'Mindanao'], ['psgc_code' => '190000000', 'code' => 'BARMM', 'name' => 'Bangsamoro Autonomous Region in Muslim Mindanao', 'island_group' => 'Mindanao'], ]; @@ -74,12 +74,12 @@ class SamplePsgcSeeder extends Seeder $manilaDist = Province::where('code', 'NCR-1')->first()->id; $secondDist = Province::where('code', 'NCR-2')->first()->id; - $thirdDist = Province::where('code', 'NCR-3')->first()->id; + $thirdDist = Province::where('code', 'NCR-3')->first()->id; $fourthDist = Province::where('code', 'NCR-4')->first()->id; $cities = [ ['psgc_code' => '133900000', 'code' => 'MNL', 'name' => 'City of Manila', 'province_id' => $manilaDist, 'is_capital' => true], - ['psgc_code' => '137401000', 'code' => 'MND', 'name' => 'City of Mandaluyong','province_id' => $secondDist], + ['psgc_code' => '137401000', 'code' => 'MND', 'name' => 'City of Mandaluyong', 'province_id' => $secondDist], ['psgc_code' => '137402000', 'code' => 'MRK', 'name' => 'City of Marikina', 'province_id' => $secondDist], ['psgc_code' => '137403000', 'code' => 'PSG', 'name' => 'Pasig City', 'province_id' => $secondDist], ['psgc_code' => '137404000', 'code' => 'QC', 'name' => 'Quezon City', 'province_id' => $secondDist], diff --git a/database/seeders/SanPascualTenantSeeder.php b/database/seeders/SanPascualTenantSeeder.php index 83b8703..5c85814 100644 --- a/database/seeders/SanPascualTenantSeeder.php +++ b/database/seeders/SanPascualTenantSeeder.php @@ -9,6 +9,7 @@ use App\Models\Region; use App\Models\Tenant; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use MatanYadaev\EloquentSpatial\Objects\LineString; use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; @@ -100,7 +101,9 @@ class SanPascualTenantSeeder extends Seeder $totals = []; foreach ($tables as $table) { - if (! \Illuminate\Support\Facades\Schema::hasColumn($table, 'tenant_id')) continue; + if (! Schema::hasColumn($table, 'tenant_id')) { + continue; + } // Don't backfill super_admins — they have no tenant. if ($table === 'users') { diff --git a/database/seeders/SuperAdminSeeder.php b/database/seeders/SuperAdminSeeder.php index d085fa2..b1d5e51 100644 --- a/database/seeders/SuperAdminSeeder.php +++ b/database/seeders/SuperAdminSeeder.php @@ -30,6 +30,6 @@ class SuperAdminSeeder extends Seeder $user->syncRoles([User::ROLE_SUPER_ADMIN]); NotificationPreference::firstOrCreate(['user_id' => $user->id]); - $this->command->info("Super admin: {$user->email} (password: ".env('SUPER_ADMIN_PASSWORD', 'password').")"); + $this->command->info("Super admin: {$user->email} (password: ".env('SUPER_ADMIN_PASSWORD', 'password').')'); } } diff --git a/resources/views/admin/barangays.blade.php b/resources/views/admin/barangays.blade.php new file mode 100644 index 0000000..20563f1 --- /dev/null +++ b/resources/views/admin/barangays.blade.php @@ -0,0 +1,468 @@ +@extends('admin.layouts.app', ['pageTitle' => 'Barangay Config']) + +@section('page') + + + + + + + +
+
+
+

Barangays

+

Configure boundaries, PSGC codes, and demographics of active Barangays.

+
+ +
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + +
NameCity / MunicipalityPSGC CodeCodeTypePopulationCentroidActions
Loading…
+
+ + +
+ +{{-- Slide-over create/edit form --}} + + + +@endsection diff --git a/resources/views/admin/households.blade.php b/resources/views/admin/households.blade.php index b577d00..40d3bd8 100644 --- a/resources/views/admin/households.blade.php +++ b/resources/views/admin/households.blade.php @@ -10,6 +10,7 @@

Households

Verify resident households and view assigned drop-offs.

+
@@ -56,6 +57,164 @@
+{{-- Add household modal --}} + + {{-- Reject modal --}}