318 lines
13 KiB
PHP
318 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Database\Seeders;
|
|
|
|
use App\Models\CollectionLog;
|
|
use App\Models\CollectionTeam;
|
|
use App\Models\DriverProfile;
|
|
use App\Models\DropOffPoint;
|
|
use App\Models\Dumpsite;
|
|
use App\Models\HelperProfile;
|
|
use App\Models\Household;
|
|
use App\Models\NotificationPreference;
|
|
use App\Models\PartnerStore;
|
|
use App\Models\QrCode;
|
|
use App\Models\Route;
|
|
use App\Models\RouteStop;
|
|
use App\Models\ScannerProfile;
|
|
use App\Models\StoreInventory;
|
|
use App\Models\StorePartnerProfile;
|
|
use App\Models\TeamMember;
|
|
use App\Models\Trip;
|
|
use App\Models\TripStop;
|
|
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;
|
|
use MatanYadaev\EloquentSpatial\Objects\Point;
|
|
|
|
/**
|
|
* Populates the database with enough sample data to exercise every
|
|
* page of the customer-web app. Idempotent — safe to re-run.
|
|
*
|
|
* - 3 partner stores (active, with inventory) so /stores has rows
|
|
* - Driver + helper + scanner users with verified profiles
|
|
* - 1 truck, 1 collection team
|
|
* - 1 route covering the seeded DOPs (including Juan's)
|
|
* - 3 scheduled trips: today (in_progress), tomorrow, +3 days
|
|
* - 5 past collection logs against Juan's household so /collections
|
|
* + /home recent activity show real entries
|
|
* - One live truck position broadcast inside the route, so /tracker
|
|
* shows a moving marker
|
|
*
|
|
* Depends on: DemoResidentSeeder (Juan's household), SamplePsgcSeeder,
|
|
* SampleDropOffPointsSeeder, SampleDumpsitesSeeder.
|
|
*/
|
|
class DemoScenarioSeeder extends Seeder
|
|
{
|
|
public function run(TruckTracker $tracker): void
|
|
{
|
|
$juan = User::where('email', 'juan@verde.local')->first();
|
|
$juanHousehold = $juan ? Household::where('head_user_id', $juan->id)->first() : null;
|
|
if (! $juan || ! $juanHousehold) {
|
|
$this->command->warn('Run DemoResidentSeeder first.');
|
|
|
|
return;
|
|
}
|
|
|
|
$dumpsite = Dumpsite::first();
|
|
$dops = DropOffPoint::orderBy('id')->get();
|
|
if ($dops->count() < 1 || ! $dumpsite) {
|
|
$this->command->warn('Need DOPs and a dumpsite seeded first.');
|
|
|
|
return;
|
|
}
|
|
|
|
// 1) Partner stores
|
|
$stores = $this->seedStores($juanHousehold);
|
|
$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);
|
|
|
|
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
|
|
$truck = Truck::firstOrCreate(
|
|
['plate_number' => 'VRD-001'],
|
|
[
|
|
'uuid' => (string) Str::uuid(),
|
|
'model' => 'Isuzu NPR 6-wheeler',
|
|
'capacity_kg' => 4000,
|
|
'status' => Truck::STATUS_ACTIVE,
|
|
],
|
|
);
|
|
|
|
// 4) Team
|
|
$team = CollectionTeam::firstOrCreate(
|
|
['name' => 'Diliman Day Crew'],
|
|
[
|
|
'uuid' => (string) Str::uuid(),
|
|
'driver_id' => $driver->id,
|
|
'scanner_id' => $scanner->id,
|
|
'truck_id' => $truck->id,
|
|
'status' => CollectionTeam::STATUS_ACTIVE,
|
|
],
|
|
);
|
|
TeamMember::updateOrCreate(
|
|
['team_id' => $team->id, 'user_id' => $helper->id],
|
|
['role_in_team' => 'helper', 'assigned_from' => now()->subDays(30)],
|
|
);
|
|
$truck->update(['assigned_team_id' => $team->id]);
|
|
|
|
// 5) Route with all DOPs
|
|
$route = Route::firstOrCreate(
|
|
['code' => 'RT-DILIMAN-A'],
|
|
[
|
|
'uuid' => (string) Str::uuid(),
|
|
'name' => 'Diliman Route A',
|
|
'default_dumpsite_id' => $dumpsite->id,
|
|
'default_team_id' => $team->id,
|
|
'estimated_duration_minutes' => 180,
|
|
'total_distance_km' => 12.5,
|
|
'status' => Route::STATUS_ACTIVE,
|
|
],
|
|
);
|
|
|
|
if ($route->stops()->count() === 0) {
|
|
foreach ($dops as $i => $dop) {
|
|
RouteStop::create([
|
|
'route_id' => $route->id,
|
|
'drop_off_point_id' => $dop->id,
|
|
'sequence' => $i + 1,
|
|
'estimated_duration_at_stop_minutes' => 15,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// 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');
|
|
|
|
// 7) Past collection logs for Juan (use a few of his "used" or active codes)
|
|
$this->seedCollectionsForJuan($juanHousehold, $todayTrip, $scanner);
|
|
$this->command->info(' Collection logs seeded for Juan');
|
|
|
|
// 8) Live truck position — drop the truck near Juan's DOP for the tracker page
|
|
$juanDop = $juanHousehold->assignedDropOffPoint;
|
|
if ($juanDop && $juanDop->coordinates) {
|
|
$tracker->record(
|
|
$truck,
|
|
$juanDop->coordinates->latitude + 0.0008,
|
|
$juanDop->coordinates->longitude - 0.0005,
|
|
heading: 90,
|
|
speedKmh: 18.0,
|
|
trip: $todayTrip,
|
|
);
|
|
$this->command->info(" Truck broadcast near {$juanDop->name}");
|
|
}
|
|
|
|
$this->command->info('Demo scenario complete.');
|
|
$this->command->info(' Partner stores: '.PartnerStore::count());
|
|
$this->command->info(' Trips: today (in_progress), tomorrow (scheduled), +3 days');
|
|
}
|
|
|
|
private function seedStores(Household $juanHousehold): Collection
|
|
{
|
|
$center = $juanHousehold->coordinates;
|
|
if (! $center) {
|
|
return PartnerStore::query()->get();
|
|
}
|
|
|
|
$samples = [
|
|
['name' => 'Aling Nena Sari-Sari', 'lat_offset' => 0.001, 'lng_offset' => 0.0008, 'addr' => '24 Sampaguita St'],
|
|
['name' => '7-Eleven Diliman', 'lat_offset' => -0.002, 'lng_offset' => 0.003, 'addr' => 'Cor. Maharlika Ave'],
|
|
['name' => 'Mercury Drug Quezon Avenue', 'lat_offset' => 0.004, 'lng_offset' => -0.002, 'addr' => '142 Quezon Ave'],
|
|
];
|
|
|
|
foreach ($samples as $i => $s) {
|
|
$owner = User::firstOrCreate(
|
|
['email' => "store{$i}@verde.local"],
|
|
[
|
|
'phone' => '+63919000000'.$i,
|
|
'password' => Hash::make('password'),
|
|
'first_name' => 'Store',
|
|
'last_name' => "Owner {$i}",
|
|
'role' => User::ROLE_STORE_PARTNER,
|
|
'status' => User::STATUS_ACTIVE,
|
|
'email_verified_at' => now(),
|
|
'phone_verified_at' => now(),
|
|
],
|
|
);
|
|
$owner->syncRoles([User::ROLE_STORE_PARTNER]);
|
|
StorePartnerProfile::firstOrCreate(['user_id' => $owner->id], ['business_name' => $s['name'], 'verification_status' => 'approved', 'verified_at' => now()]);
|
|
NotificationPreference::firstOrCreate(['user_id' => $owner->id]);
|
|
|
|
$store = PartnerStore::firstOrCreate(
|
|
['business_name' => $s['name']],
|
|
[
|
|
'uuid' => (string) Str::uuid(),
|
|
'owner_user_id' => $owner->id,
|
|
'business_permit_number' => 'BP-'.strtoupper(Str::random(6)),
|
|
'address_line' => $s['addr'].', '.($juanHousehold->barangay->name ?? 'Quezon City'),
|
|
'barangay_id' => $juanHousehold->barangay_id,
|
|
'coordinates' => new Point(
|
|
$center->latitude + $s['lat_offset'],
|
|
$center->longitude + $s['lng_offset'],
|
|
4326,
|
|
),
|
|
'commission_rate_percent' => 10,
|
|
'status' => PartnerStore::STATUS_ACTIVE,
|
|
],
|
|
);
|
|
|
|
StoreInventory::firstOrCreate(
|
|
['store_id' => $store->id],
|
|
['current_code_balance' => 50 + ($i * 25), 'last_updated_at' => now()],
|
|
);
|
|
}
|
|
|
|
return PartnerStore::query()->get();
|
|
}
|
|
|
|
private function createStaff(string $email, string $phone, string $first, string $last, string $role): User
|
|
{
|
|
$u = User::firstOrCreate(
|
|
['email' => $email],
|
|
[
|
|
'phone' => $phone,
|
|
'password' => Hash::make('password'),
|
|
'first_name' => $first,
|
|
'last_name' => $last,
|
|
'role' => $role,
|
|
'status' => User::STATUS_ACTIVE,
|
|
'email_verified_at' => now(),
|
|
'phone_verified_at' => now(),
|
|
],
|
|
);
|
|
$u->syncRoles([$role]);
|
|
NotificationPreference::firstOrCreate(['user_id' => $u->id]);
|
|
|
|
return $u;
|
|
}
|
|
|
|
private function seedTrip(Route $route, CollectionTeam $team, Truck $truck, Dumpsite $dumpsite, Carbon $date, string $status, string $tripNumber): Trip
|
|
{
|
|
$trip = Trip::firstOrCreate(
|
|
['trip_number' => $tripNumber],
|
|
[
|
|
'uuid' => (string) Str::uuid(),
|
|
'route_id' => $route->id,
|
|
'team_id' => $team->id,
|
|
'truck_id' => $truck->id,
|
|
'dumpsite_id' => $dumpsite->id,
|
|
'scheduled_date' => $date->toDateString(),
|
|
'scheduled_start_time' => '08:00:00',
|
|
'status' => $status,
|
|
'actual_start_time' => $status === Trip::STATUS_IN_PROGRESS ? now()->subMinutes(30) : null,
|
|
],
|
|
);
|
|
|
|
if ($trip->stops()->count() === 0) {
|
|
foreach ($route->stops as $rs) {
|
|
TripStop::create([
|
|
'trip_id' => $trip->id,
|
|
'drop_off_point_id' => $rs->drop_off_point_id,
|
|
'sequence' => $rs->sequence,
|
|
'status' => 'pending',
|
|
'total_scans' => 0,
|
|
'estimated_load_added_kg' => 0,
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $trip;
|
|
}
|
|
|
|
private function seedCollectionsForJuan(Household $juanHousehold, Trip $trip, User $scanner): void
|
|
{
|
|
if (CollectionLog::where('household_id', $juanHousehold->id)->exists()) {
|
|
return; // idempotent — only seed once
|
|
}
|
|
|
|
$codes = QrCode::where('assigned_to_household_id', $juanHousehold->id)
|
|
->where('status', 'active')
|
|
->limit(5)
|
|
->get();
|
|
if ($codes->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$dop = $juanHousehold->assignedDropOffPoint;
|
|
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];
|
|
$types = ['mixed', 'recyclable', 'mixed', 'organic', 'mixed'];
|
|
|
|
foreach ($codes as $i => $code) {
|
|
$scannedAt = now()->subDays(($i + 1) * 5)->setTime(9, 30);
|
|
CollectionLog::create([
|
|
'qr_code_id' => $code->id,
|
|
'household_id' => $juanHousehold->id,
|
|
'drop_off_point_id' => $dop->id,
|
|
'scanned_by_user_id' => $scanner->id,
|
|
'trip_id' => $trip->id,
|
|
'trip_stop_id' => $tripStop?->id,
|
|
'scanned_at' => $scannedAt,
|
|
'coordinates_at_scan' => $dop->coordinates,
|
|
'weight_kg' => $weights[$i] ?? 3.0,
|
|
'waste_type' => $types[$i] ?? 'mixed',
|
|
'verification_status' => CollectionLog::STATUS_VALID,
|
|
]);
|
|
$code->update(['status' => 'used', 'used_at' => $scannedAt, 'used_at_drop_off_id' => $dop->id, 'scanned_by_user_id' => $scanner->id]);
|
|
}
|
|
}
|
|
}
|