Files
Verde-Web/database/seeders/DemoScenarioSeeder.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]);
}
}
}