Files
crewli/api/database/seeders/DevSeeder.php
bert.hausmans 5d8a749cb3 fix: seeder creates User accounts for approved/no_show persons
The DevSeeder was creating approved persons without linked User
accounts, which can't happen in production (approval flow always
creates accounts). Added linkUsersToApprovedPersons() helper that
runs after person creation in each event seeder, creating User
accounts via firstOrCreate for approved and no_show persons that
lack user_id.

Also added safeguard tests verifying the approval flow creates
user accounts and reuses existing ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 20:42:47 +02:00

1418 lines
86 KiB
PHP

<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Enums\CrowdListType;
use App\Enums\ShiftAssignmentStatus;
use App\Models\Company;
use App\Models\CrowdList;
use App\Models\CrowdType;
use App\Models\Event;
use App\Models\FestivalSection;
use App\Models\Location;
use App\Models\Organisation;
use App\Models\Person;
use App\Models\Shift;
use App\Models\ShiftAssignment;
use App\Models\TimeSlot;
use App\Models\User;
use App\Models\UserOrganisationTag;
use App\Models\VolunteerAvailability;
use Illuminate\Database\Seeder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class DevSeeder extends Seeder
{
private Organisation $org;
/** @var array<string, User> */
private array $users = [];
/** @var array<string, CrowdType> */
private array $crowdTypes = [];
/** @var array<string, Company> */
private array $companies = [];
/** @var array<string, \App\Models\PersonTag> */
private array $personTags = [];
public function run(): void
{
$this->call(RoleSeeder::class);
$this->seedOrganisation();
$this->seedEchtFeesten();
$this->seedBraderie();
$this->seedIJsbaan();
$this->seedKoningsdag();
$this->seedNachtVanDeKaap();
}
// =========================================================================
// Organisation: Stichting Feestfabriek
// =========================================================================
private function seedOrganisation(): void
{
DB::transaction(function (): void {
$this->command->info('Seeding organisation: Stichting Feestfabriek...');
$this->org = Organisation::create([
'name' => 'Stichting Feestfabriek',
'slug' => 'stichting-feestfabriek',
'billing_status' => 'active',
'settings' => [],
]);
// ── Users (8) ──
$usersData = [
['email' => 'admin@crewli.test', 'first_name' => 'Super', 'last_name' => 'Admin', 'app_role' => 'super_admin', 'org_role' => 'org_admin', 'date_of_birth' => '1985-01-15'],
['email' => 'bert@feestfabriek.nl', 'first_name' => 'Bert', 'last_name' => 'Hausmans', 'org_role' => 'org_admin', 'date_of_birth' => '1990-06-28'],
['email' => 'lisa@feestfabriek.nl', 'first_name' => 'Lisa', 'last_name' => 'van den Berg', 'org_role' => 'org_member', 'date_of_birth' => '1993-05-12'],
['email' => 'ahmed@feestfabriek.nl', 'first_name' => 'Ahmed', 'last_name' => 'Yilmaz', 'org_role' => 'org_member', 'date_of_birth' => '1989-09-03'],
['email' => 'sara@feestfabriek.nl', 'first_name' => 'Sara', 'last_name' => 'de Groot', 'org_role' => 'org_member', 'date_of_birth' => '1991-08-24'],
['email' => 'tom@feestfabriek.nl', 'first_name' => 'Tom', 'last_name' => 'Visser', 'org_role' => 'org_member', 'date_of_birth' => '1994-11-07'],
['email' => 'nina@feestfabriek.nl', 'first_name' => 'Nina', 'last_name' => 'Jansen', 'org_role' => 'org_member', 'date_of_birth' => '1996-02-14'],
['email' => 'mark@feestfabriek.nl', 'first_name' => 'Mark', 'last_name' => 'de Boer', 'org_role' => 'org_member', 'date_of_birth' => '1988-03-17'],
];
foreach ($usersData as $data) {
$user = User::create([
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'email' => $data['email'],
'date_of_birth' => $data['date_of_birth'] ?? null,
'password' => Hash::make('password'),
]);
if (isset($data['app_role'])) {
$user->assignRole($data['app_role']);
}
$this->org->users()->attach($user, ['role' => $data['org_role']]);
$this->users[$data['email']] = $user;
}
// ── Companies (6) ──
$companiesData = [
['name' => 'Tap & Co Horeca', 'type' => 'supplier', 'contact_first_name' => 'Jan', 'contact_last_name' => 'Tapper', 'contact_email' => 'jan@tapco.nl', 'contact_phone' => '+31612340001'],
['name' => 'SecureEvent BV', 'type' => 'supplier', 'contact_first_name' => 'Klaas', 'contact_last_name' => 'Veilig', 'contact_email' => 'klaas@secureevent.nl', 'contact_phone' => '+31612340002'],
['name' => 'Podiumtechniek Rijnmond', 'type' => 'supplier', 'contact_first_name' => 'Pieter', 'contact_last_name' => 'Geluid', 'contact_email' => 'pieter@podiumtechniek.nl', 'contact_phone' => '+31612340003'],
['name' => 'Brouwerij De Schelde', 'type' => 'partner', 'contact_first_name' => 'Eva', 'contact_last_name' => 'Brouwer', 'contact_email' => 'eva@brouwerijdeschelde.nl', 'contact_phone' => '+31612340004'],
['name' => 'Van Dijk Catering', 'type' => 'supplier', 'contact_first_name' => 'Maria', 'contact_last_name' => 'van Dijk', 'contact_email' => 'maria@vandijkcatering.nl', 'contact_phone' => '+31612340005'],
['name' => 'Rotterdam Festivals', 'type' => 'agency', 'contact_first_name' => 'Femke', 'contact_last_name' => 'de Wit', 'contact_email' => 'femke@rotterdamfestivals.nl', 'contact_phone' => '+31612340006'],
];
foreach ($companiesData as $data) {
$company = Company::create(['organisation_id' => $this->org->id, ...$data]);
$this->companies[$data['name']] = $company;
}
// ── Crowd Types (7) ──
$crowdTypesData = [
['name' => 'Vrijwilliger', 'system_type' => 'VOLUNTEER', 'color' => '#4CAF50', 'icon' => 'tabler-heart-handshake'],
['name' => 'Medewerker', 'system_type' => 'CREW', 'color' => '#2196F3', 'icon' => 'tabler-id-badge-2'],
['name' => 'Artiest', 'system_type' => 'ARTIST', 'color' => '#9C27B0', 'icon' => 'tabler-microphone-2'],
['name' => 'Pers', 'system_type' => 'PRESS', 'color' => '#FF9800', 'icon' => 'tabler-camera'],
['name' => 'Gast', 'system_type' => 'GUEST', 'color' => '#607D8B', 'icon' => 'tabler-ticket'],
['name' => 'Leverancier', 'system_type' => 'SUPPLIER', 'color' => '#795548', 'icon' => 'tabler-truck-delivery'],
['name' => 'Partner', 'system_type' => 'PARTNER', 'color' => '#FFC107', 'icon' => 'tabler-affiliate'],
];
foreach ($crowdTypesData as $data) {
$ct = CrowdType::create(['organisation_id' => $this->org->id, ...$data, 'is_active' => true]);
$this->crowdTypes[$data['system_type']] = $ct;
}
// ── Person Tags (10) ──
$personTagsData = [
['name' => 'Tapper', 'category' => 'Horeca', 'icon' => 'tabler-beer', 'color' => '#FF9800', 'is_active' => true, 'sort_order' => 1],
['name' => 'Barista', 'category' => 'Horeca', 'icon' => 'tabler-coffee', 'color' => '#795548', 'is_active' => true, 'sort_order' => 2],
['name' => 'EHBO', 'category' => 'Veiligheid', 'icon' => 'tabler-first-aid-kit', 'color' => '#F44336', 'is_active' => true, 'sort_order' => 3],
['name' => 'BHV', 'category' => 'Veiligheid', 'icon' => 'tabler-fire-extinguisher', 'color' => '#E91E63', 'is_active' => true, 'sort_order' => 4],
['name' => 'Rijbewijs B', 'category' => 'Logistiek', 'icon' => 'tabler-car', 'color' => '#607D8B', 'is_active' => true, 'sort_order' => 5],
['name' => 'Heftruck', 'category' => 'Logistiek', 'icon' => 'tabler-forklift', 'color' => '#9E9E9E', 'is_active' => true, 'sort_order' => 6],
['name' => 'Duits', 'category' => 'Taal', 'icon' => 'tabler-flag', 'color' => '#000000', 'is_active' => true, 'sort_order' => 7],
['name' => 'Engels', 'category' => 'Taal', 'icon' => 'tabler-flag', 'color' => '#1565C0', 'is_active' => true, 'sort_order' => 8],
['name' => 'Podiumervaring', 'category' => 'Techniek', 'icon' => 'tabler-speakerphone', 'color' => '#9C27B0', 'is_active' => true, 'sort_order' => 9],
['name' => 'Teamleider', 'category' => 'Rol', 'icon' => 'tabler-crown', 'color' => '#FFC107', 'is_active' => false, 'sort_order' => 10],
];
foreach ($personTagsData as $data) {
$tag = $this->org->personTags()->create($data);
$this->personTags[$data['name']] = $tag;
}
// ── Registration Field Templates (system defaults) ──
\App\Services\RegistrationFieldTemplateService::seedSystemTemplates($this->org);
$this->command->info(' Organisation, 8 users, 6 companies, 7 crowd types, 10 person tags, 16 registration templates created');
});
}
// =========================================================================
// Event 1: Echt Feesten 2026 (festival, registration_open, 150 persons)
// =========================================================================
private function seedEchtFeesten(): void
{
DB::transaction(function (): void {
$this->command->info('Seeding Echt Feesten 2026...');
// ── Event hierarchy ──
$festival = Event::create([
'organisation_id' => $this->org->id,
'name' => 'Echt Feesten 2026',
'slug' => 'echt-feesten-2026',
'start_date' => '2026-07-10',
'end_date' => '2026-07-12',
'timezone' => 'Europe/Amsterdam',
'status' => 'registration_open',
'event_type' => 'festival',
'event_type_label' => 'Festival',
'sub_event_label' => 'Programmaonderdeel',
'registration_welcome_text' => 'Wij zoeken enthousiaste vrijwilligers voor Echt Feesten 2026! Word onderdeel van ons team en beleef het festival van de andere kant. Gratis toegang, maaltijden, en een onvergetelijke ervaring.',
]);
$vrijdag = Event::create([
'organisation_id' => $this->org->id,
'parent_event_id' => $festival->id,
'name' => 'Dag 1 — Vrijdag',
'slug' => 'echt-feesten-2026-vrijdag',
'start_date' => '2026-07-10',
'end_date' => '2026-07-10',
'timezone' => 'Europe/Amsterdam',
'status' => 'registration_open',
'event_type' => 'event',
]);
$zaterdag = Event::create([
'organisation_id' => $this->org->id,
'parent_event_id' => $festival->id,
'name' => 'Dag 2 — Zaterdag',
'slug' => 'echt-feesten-2026-zaterdag',
'start_date' => '2026-07-11',
'end_date' => '2026-07-11',
'timezone' => 'Europe/Amsterdam',
'status' => 'registration_open',
'event_type' => 'event',
]);
$zondag = Event::create([
'organisation_id' => $this->org->id,
'parent_event_id' => $festival->id,
'name' => 'Dag 3 — Zondag',
'slug' => 'echt-feesten-2026-zondag',
'start_date' => '2026-07-12',
'end_date' => '2026-07-12',
'timezone' => 'Europe/Amsterdam',
'status' => 'published',
'event_type' => 'event',
]);
$subEvents = ['vrijdag' => $vrijdag, 'zaterdag' => $zaterdag, 'zondag' => $zondag];
// Attach event-level user roles
$eventRoles = [
'lisa@feestfabriek.nl' => 'event_manager',
'ahmed@feestfabriek.nl' => 'event_manager',
'sara@feestfabriek.nl' => 'volunteer_coordinator',
'tom@feestfabriek.nl' => 'staff_coordinator',
'nina@feestfabriek.nl' => 'artist_manager',
];
foreach ($eventRoles as $email => $role) {
$festival->users()->attach($this->users[$email], ['role' => $role]);
}
// ── Locations (5, on parent) ──
$locations = [];
$locationsData = [
'hoofdpodium' => ['name' => 'Hoofdpodium', 'address' => 'Parkweg 1, Echt', 'lat' => 51.1039, 'lng' => 5.8718],
'theatertent' => ['name' => 'Theatertent', 'address' => 'Parkweg 1A, Echt', 'lat' => 51.1041, 'lng' => 5.8725],
'ingang' => ['name' => 'Festivalterrein Ingang', 'address' => 'Parkweg 2, Echt', 'lat' => 51.1035, 'lng' => 5.8710],
'backstage' => ['name' => 'Backstage Area', 'address' => 'Parkweg 1B, Echt', 'lat' => 51.1043, 'lng' => 5.8720],
'camping' => ['name' => 'Camping', 'address' => 'Kampeerweg 5, Echt', 'lat' => 51.1050, 'lng' => 5.8740],
];
foreach ($locationsData as $key => $data) {
$locations[$key] = Location::create(['event_id' => $festival->id, ...$data]);
}
// ── Festival-level sections (4) ──
$ehbo = FestivalSection::create([
'event_id' => $festival->id, 'name' => 'EHBO', 'type' => 'cross_event',
'category' => 'Veiligheid', 'icon' => 'tabler-first-aid-kit', 'sort_order' => 1,
'responder_self_checkin' => true, 'crew_auto_accepts' => false,
'show_in_registration' => false,
]);
$security = FestivalSection::create([
'event_id' => $festival->id, 'name' => 'Security', 'type' => 'cross_event',
'category' => 'Veiligheid', 'icon' => 'tabler-shield', 'sort_order' => 2,
'responder_self_checkin' => true, 'crew_auto_accepts' => false,
'show_in_registration' => false,
]);
$terreinploeg = FestivalSection::create([
'event_id' => $festival->id, 'name' => 'Terreinploeg', 'type' => 'standard',
'category' => 'Productie', 'icon' => 'tabler-shovel', 'sort_order' => 3,
'responder_self_checkin' => true, 'crew_auto_accepts' => true,
'show_in_registration' => false,
]);
$accreditatiebalie = FestivalSection::create([
'event_id' => $festival->id, 'name' => 'Accreditatiebalie', 'type' => 'cross_event',
'category' => 'Ontvangst', 'icon' => 'tabler-id-badge', 'sort_order' => 4,
'responder_self_checkin' => true, 'crew_auto_accepts' => true,
'show_in_registration' => false,
]);
// ── Sub-event sections (5 per sub-event) ──
$sectionDefs = [
'hoofdbar' => ['name' => 'Hoofdpodium Bar', 'category' => 'Bar', 'icon' => 'tabler-beer', 'crew_auto_accepts' => true, 'show_in_registration' => true, 'registration_description' => 'Tap bier en drankjes voor festivalgangers bij het hoofdpodium'],
'theaterbar' => ['name' => 'Theatertent Bar', 'category' => 'Bar', 'icon' => 'tabler-beer', 'crew_auto_accepts' => true, 'show_in_registration' => true, 'registration_description' => 'Bediening in de overdekte theatertent'],
'hospitality' => ['name' => 'Backstage Hospitality', 'category' => 'Hospitality', 'icon' => 'tabler-armchair', 'crew_auto_accepts' => false, 'show_in_registration' => true, 'registration_description' => 'Ontvang en begeleid artiesten en gasten backstage'],
'podiumtechniek' => ['name' => 'Podiumtechniek', 'category' => 'Techniek', 'icon' => 'tabler-speakerphone', 'crew_auto_accepts' => false, 'show_in_registration' => true, 'registration_description' => 'Help met geluid- en lichttechniek bij de podia'],
'ingang' => ['name' => 'Ingang & Tickets', 'category' => 'Ontvangst', 'icon' => 'tabler-ticket', 'crew_auto_accepts' => true, 'show_in_registration' => true, 'registration_description' => 'Verwelkom bezoekers en scan tickets bij de ingang'],
];
$sections = [];
foreach ($subEvents as $dayKey => $subEvent) {
$order = 1;
foreach ($sectionDefs as $key => $def) {
$sections["{$dayKey}_{$key}"] = FestivalSection::create([
'event_id' => $subEvent->id,
'name' => $def['name'],
'type' => 'standard',
'category' => $def['category'],
'icon' => $def['icon'],
'sort_order' => $order++,
'responder_self_checkin' => true,
'crew_auto_accepts' => $def['crew_auto_accepts'],
'show_in_registration' => $def['show_in_registration'] ?? false,
'registration_description' => $def['registration_description'] ?? null,
]);
}
}
// ── Festival-level time slots (CREW only — operational planning) ──
// No VOLUNTEER slots at festival level — cross_event sections use sub-event time slots
$fSlots = [];
$fSlots['opbouw1'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Opbouw vrijdag', 'person_type' => 'CREW', 'date' => '2026-07-09', 'start_time' => '08:00', 'end_time' => '18:00', 'duration_hours' => 10.00]);
$fSlots['opbouw2'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Opbouw zaterdag', 'person_type' => 'CREW', 'date' => '2026-07-10', 'start_time' => '08:00', 'end_time' => '14:00', 'duration_hours' => 6.00]);
$fSlots['nacht_vr'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Nachtsecurity vr→za', 'person_type' => 'CREW', 'date' => '2026-07-10', 'start_time' => '23:00', 'end_time' => '07:00', 'duration_hours' => 8.00]);
$fSlots['nacht_za'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Nachtsecurity za→zo', 'person_type' => 'CREW', 'date' => '2026-07-11', 'start_time' => '23:00', 'end_time' => '07:00', 'duration_hours' => 8.00]);
$fSlots['afbraak'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Afbraak', 'person_type' => 'CREW', 'date' => '2026-07-13', 'start_time' => '08:00', 'end_time' => '18:00', 'duration_hours' => 10.00]);
// ── Sub-event time slots (program-specific) ──
$ts = [];
// Dag 1 — Vrijdag
$ts['vr_middag'] = TimeSlot::create(['event_id' => $vrijdag->id, 'name' => 'Vrijdag middag — vrijwilliger', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-10', 'start_time' => '12:00', 'end_time' => '18:00', 'duration_hours' => 6.00]);
$ts['vr_avond'] = TimeSlot::create(['event_id' => $vrijdag->id, 'name' => 'Vrijdag avond — vrijwilliger', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-10', 'start_time' => '18:00', 'end_time' => '02:00', 'duration_hours' => 8.00]);
$ts['vr_crew'] = TimeSlot::create(['event_id' => $vrijdag->id, 'name' => 'Vrijdag avond — crew', 'person_type' => 'CREW', 'date' => '2026-07-10', 'start_time' => '17:00', 'end_time' => '03:00', 'duration_hours' => 10.00]);
$ts['vr_pers'] = TimeSlot::create(['event_id' => $vrijdag->id, 'name' => 'Vrijdag pers', 'person_type' => 'PRESS', 'date' => '2026-07-10', 'start_time' => '16:00', 'end_time' => '22:00', 'duration_hours' => 6.00]);
// Dag 2 — Zaterdag
$ts['za_middag'] = TimeSlot::create(['event_id' => $zaterdag->id, 'name' => 'Zaterdag middag — vrijwilliger', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-11', 'start_time' => '12:00', 'end_time' => '18:00', 'duration_hours' => 6.00]);
$ts['za_avond'] = TimeSlot::create(['event_id' => $zaterdag->id, 'name' => 'Zaterdag avond — vrijwilliger', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-11', 'start_time' => '18:00', 'end_time' => '02:00', 'duration_hours' => 8.00]);
$ts['za_crew'] = TimeSlot::create(['event_id' => $zaterdag->id, 'name' => 'Zaterdag avond — crew', 'person_type' => 'CREW', 'date' => '2026-07-11', 'start_time' => '17:00', 'end_time' => '03:00', 'duration_hours' => 10.00]);
// Dag 3 — Zondag
$ts['zo_middag'] = TimeSlot::create(['event_id' => $zondag->id, 'name' => 'Zondag middag — vrijwilliger', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-12', 'start_time' => '10:00', 'end_time' => '18:00', 'duration_hours' => 8.00]);
$ts['zo_avond'] = TimeSlot::create(['event_id' => $zondag->id, 'name' => 'Zondag avond — vrijwilliger', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-12', 'start_time' => '18:00', 'end_time' => '22:00', 'duration_hours' => 4.00]);
// ── Shifts (~55) ──
$allShifts = [];
$s = []; // named shifts for explicit assignments
// EHBO (cross_event): shifts use sub-event time slots (cross_event exception)
// + 1 festival-level shift for opbouw duties
$s['ehbo_opbouw'] = Shift::create([
'festival_section_id' => $ehbo->id, 'time_slot_id' => $fSlots['opbouw1']->id,
'title' => 'EHBO Post', 'slots_total' => 2, 'slots_open_for_claiming' => 0, 'status' => 'open',
]);
$allShifts[] = $s['ehbo_opbouw'];
foreach (['vr_middag', 'vr_avond', 'za_middag', 'za_avond', 'zo_middag'] as $key) {
$shift = Shift::create([
'festival_section_id' => $ehbo->id,
'time_slot_id' => $ts[$key]->id,
'title' => 'EHBO Post',
'slots_total' => $key === 'za_middag' ? 4 : 3,
'slots_open_for_claiming' => $key === 'za_middag' ? 0 : 2,
'status' => 'open',
]);
$allShifts[] = $shift;
$s["ehbo_{$key}"] = $shift;
}
// Security (cross_event): festival night shifts + sub-event crew shifts
foreach (['nacht_vr', 'nacht_za'] as $key) {
$shift = Shift::create([
'festival_section_id' => $security->id,
'time_slot_id' => $fSlots[$key]->id,
'title' => 'Beveiliger',
'slots_total' => 6,
'slots_open_for_claiming' => 0,
'status' => 'open',
]);
$allShifts[] = $shift;
$s["security_{$key}"] = $shift;
}
// Security shifts using sub-event CREW time slots (cross_event exception)
foreach (['vr_crew', 'za_crew'] as $key) {
$shift = Shift::create([
'festival_section_id' => $security->id,
'time_slot_id' => $ts[$key]->id,
'title' => 'Beveiliger',
'slots_total' => 4,
'slots_open_for_claiming' => 0,
'status' => 'open',
]);
$allShifts[] = $shift;
$s["security_{$key}"] = $shift;
}
// Terreinploeg (standard on festival): shifts use festival time slots only
foreach (['opbouw1', 'opbouw2', 'afbraak'] as $key) {
$shift = Shift::create([
'festival_section_id' => $terreinploeg->id,
'time_slot_id' => $fSlots[$key]->id,
'title' => 'Terreinmedewerker',
'slots_total' => $key === 'afbraak' ? 20 : 15,
'slots_open_for_claiming' => $key === 'afbraak' ? 16 : 12,
'status' => $key === 'afbraak' ? 'draft' : 'open',
]);
$allShifts[] = $shift;
$s["terrein_{$key}"] = $shift;
}
// Accreditatiebalie (cross_event): shifts use sub-event time slots
foreach (['vr_middag', 'za_middag', 'zo_middag', 'za_avond'] as $key) {
$shift = Shift::create([
'festival_section_id' => $accreditatiebalie->id,
'time_slot_id' => $ts[$key]->id,
'title' => 'Accreditatiemedewerker',
'slots_total' => 3,
'slots_open_for_claiming' => 2,
'status' => 'open',
]);
$allShifts[] = $shift;
$s["accred_{$key}"] = $shift;
}
// Sub-event shifts
$slotMap = [
'vrijdag' => ['vr_middag', 'vr_avond'],
'zaterdag' => ['za_middag', 'za_avond'],
'zondag' => ['zo_middag', 'zo_avond'],
];
$locationMap = [
'hoofdbar' => 'hoofdpodium',
'theaterbar' => 'theatertent',
'hospitality' => 'backstage',
'podiumtechniek' => 'hoofdpodium',
'ingang' => 'ingang',
];
$titleMap = [
'hoofdbar' => 'Tapper',
'theaterbar' => 'Tapper',
'hospitality' => 'Hospitality',
'podiumtechniek' => 'Stagehand',
'ingang' => 'Ticketcontrole',
];
foreach ($slotMap as $dayKey => $slotKeys) {
foreach (array_keys($sectionDefs) as $secKey) {
foreach ($slotKeys as $slotKey) {
$sectionModel = $sections["{$dayKey}_{$secKey}"];
$slotsTotal = match ($secKey) {
'hoofdbar', 'theaterbar' => rand(8, 12),
'ingang' => rand(6, 8),
default => rand(3, 4),
};
$slotsOpen = match ($secKey) {
'hoofdbar', 'theaterbar' => rand(6, 10),
'ingang' => rand(4, 6),
default => 0,
};
$isDraft = $dayKey === 'zondag' && $slotKey === 'zo_avond';
$shift = Shift::create([
'festival_section_id' => $sectionModel->id,
'time_slot_id' => $ts[$slotKey]->id,
'location_id' => $locations[$locationMap[$secKey]]->id,
'title' => $titleMap[$secKey],
'slots_total' => $slotsTotal,
'slots_open_for_claiming' => $slotsOpen,
'status' => $isDraft ? 'draft' : 'open',
'allow_overlap' => $secKey === 'podiumtechniek',
]);
$allShifts[] = $shift;
$s["{$secKey}_{$slotKey}"] = $shift;
}
}
}
$shiftCount = count($allShifts);
$this->command->info(" {$shiftCount} shifts created");
// ── Named persons (30) ──
$vol = $this->crowdTypes['VOLUNTEER']->id;
$crew = $this->crowdTypes['CREW']->id;
$press = $this->crowdTypes['PRESS']->id;
$guest = $this->crowdTypes['GUEST']->id;
$supplier = $this->crowdTypes['SUPPLIER']->id;
// Create user accounts for volunteers who need user_id links
$volunteerUsers = [];
foreach ([
['first_name' => 'Jan', 'last_name' => 'de Vries', 'email' => 'jan@gmail.com'],
['first_name' => 'Ahmed', 'last_name' => 'Hassan', 'email' => 'ahmed.h@gmail.com'],
['first_name' => 'Tom', 'last_name' => 'Visser', 'email' => 'tom.visser@gmail.com'],
['first_name' => 'Lotte', 'last_name' => 'de Jong', 'email' => 'lotte@gmail.com'],
['first_name' => 'Pieter', 'last_name' => 'Geluid', 'email' => 'pieter@podiumtechniek.nl'],
] as $u) {
$volunteerUsers[$u['email']] = User::create([
'first_name' => $u['first_name'],
'last_name' => $u['last_name'],
'email' => $u['email'],
'password' => Hash::make('password'),
]);
}
// 15 named volunteers
$jan = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['jan@gmail.com']->id, 'first_name' => 'Jan', 'last_name' => 'de Vries', 'email' => 'jan@gmail.com', 'phone' => '+31612345001', 'status' => 'approved', 'date_of_birth' => '1995-03-15']);
$lisaB = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Lisa', 'last_name' => 'Bakker', 'email' => 'lisa.bakker@hotmail.com', 'phone' => '+31612345002', 'status' => 'approved', 'date_of_birth' => '1998-07-22']);
$ahmedP = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['ahmed.h@gmail.com']->id, 'first_name' => 'Ahmed', 'last_name' => 'Hassan', 'email' => 'ahmed.h@gmail.com', 'phone' => '+31612345003', 'status' => 'approved', 'date_of_birth' => '1992-11-08']);
$saraJ = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Sara', 'last_name' => 'Jansen', 'email' => 'sara.j@outlook.com', 'phone' => '+31612345004', 'status' => 'approved', 'date_of_birth' => '2000-01-30']);
$tomV = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['tom.visser@gmail.com']->id, 'first_name' => 'Tom', 'last_name' => 'Visser', 'email' => 'tom.visser@gmail.com', 'phone' => '+31612345005', 'status' => 'approved', 'date_of_birth' => '1997-06-14']);
$fatima = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Fatima', 'last_name' => 'El Amrani', 'email' => 'fatima@gmail.com', 'phone' => '+31612345006', 'status' => 'approved', 'date_of_birth' => '1996-05-20']);
$daan = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Daan', 'last_name' => 'Smit', 'email' => 'daan.smit@gmail.com', 'phone' => '+31612345007', 'status' => 'pending', 'date_of_birth' => '1993-08-11']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Sophie', 'last_name' => 'Mulder', 'email' => 'sophie.m@hotmail.com', 'phone' => '+31612345008', 'status' => 'pending', 'date_of_birth' => '2001-02-28']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Jesse', 'last_name' => 'van Dijk', 'email' => 'jesse@gmail.com', 'phone' => '+31612345009', 'status' => 'applied', 'date_of_birth' => '1999-10-05']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Noa', 'last_name' => 'Hendriks', 'email' => 'noa.h@outlook.com', 'phone' => '+31612345010', 'status' => 'applied', 'date_of_birth' => '2000-07-19']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Kevin', 'last_name' => 'Bos', 'email' => 'kevin.bos@gmail.com', 'phone' => '+31612345011', 'status' => 'rejected', 'admin_notes' => 'Vorig jaar no-show', 'date_of_birth' => '1994-12-01']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Priya', 'last_name' => 'Sharma', 'email' => 'priya@gmail.com', 'phone' => '+31612345012', 'status' => 'invited', 'date_of_birth' => '1998-03-27']);
$lotte = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['lotte@gmail.com']->id, 'first_name' => 'Lotte', 'last_name' => 'de Jong', 'email' => 'lotte@gmail.com', 'phone' => '+31612345013', 'status' => 'approved', 'date_of_birth' => '1994-09-25']);
$robin = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Robin', 'last_name' => 'Peters', 'email' => 'robin.p@hotmail.com', 'phone' => '+31612345014', 'status' => 'approved', 'date_of_birth' => '1991-12-03']);
$emma = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Emma', 'last_name' => 'Willems', 'email' => 'emma.w@gmail.com', 'phone' => '+31612345015', 'status' => 'no_show', 'is_blacklisted' => true, 'date_of_birth' => '1999-04-17']);
// 6 named crew
$klaas = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $crew, 'company_id' => $this->companies['SecureEvent BV']->id, 'first_name' => 'Klaas', 'last_name' => 'Veilig Jr.', 'email' => 'klaas.jr@secureevent.nl', 'phone' => '+31612345016', 'status' => 'approved']);
$dennis = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $crew, 'company_id' => $this->companies['SecureEvent BV']->id, 'first_name' => 'Dennis', 'last_name' => 'Schild', 'email' => 'dennis@secureevent.nl', 'phone' => '+31612345017', 'status' => 'approved']);
$pieter = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $crew, 'company_id' => $this->companies['Podiumtechniek Rijnmond']->id, 'user_id' => $volunteerUsers['pieter@podiumtechniek.nl']->id, 'first_name' => 'Pieter', 'last_name' => 'Geluid', 'email' => 'pieter@podiumtechniek.nl', 'phone' => '+31612345018', 'status' => 'approved']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $crew, 'company_id' => $this->companies['Podiumtechniek Rijnmond']->id, 'first_name' => 'Marco', 'last_name' => 'Licht', 'email' => 'marco@podiumtechniek.nl', 'phone' => '+31612345019', 'status' => 'approved']);
$eva = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $crew, 'company_id' => $this->companies['Brouwerij De Schelde']->id, 'first_name' => 'Eva', 'last_name' => 'Brouwer', 'email' => 'eva@brouwerijdeschelde.nl', 'phone' => '+31612345020', 'status' => 'approved']);
$maria = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $crew, 'company_id' => $this->companies['Van Dijk Catering']->id, 'first_name' => 'Maria', 'last_name' => 'van Dijk', 'email' => 'maria@vandijkcatering.nl', 'phone' => '+31612345021', 'status' => 'approved']);
// 3 named press
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $press, 'first_name' => 'Joris', 'last_name' => 'van Laar', 'email' => 'joris@pers.nl', 'phone' => '+31612345022', 'status' => 'approved']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $press, 'first_name' => 'Tamara', 'last_name' => 'Smeets', 'email' => 'tamara@pers.nl', 'phone' => '+31612345023', 'status' => 'approved']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $press, 'first_name' => 'Sven', 'last_name' => 'Pieterse', 'email' => 'sven@pers.nl', 'phone' => '+31612345024', 'status' => 'pending']);
// 4 named guests
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $guest, 'first_name' => 'Jan', 'last_name' => 'Slagter', 'email' => 'burgemeester@echt.nl', 'phone' => '+31612345025', 'status' => 'approved']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $guest, 'first_name' => 'Petra', 'last_name' => 'Kamps', 'email' => 'wethouder@echt.nl', 'phone' => '+31612345026', 'status' => 'approved']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $guest, 'first_name' => 'Jan', 'last_name' => 'Sponsor', 'email' => 'sponsor@bedrijf.nl', 'phone' => '+31612345027', 'status' => 'approved']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $guest, 'first_name' => 'Gast', 'last_name' => 'van Artiest', 'email' => 'artiestgast@gmail.com', 'phone' => '+31612345028', 'status' => 'invited']);
// 2 named suppliers
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $supplier, 'company_id' => $this->companies['Tap & Co Horeca']->id, 'first_name' => 'Hans', 'last_name' => 'Tapper', 'email' => 'hans@tapco.nl', 'phone' => '+31612345029', 'status' => 'approved']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $supplier, 'company_id' => $this->companies['Van Dijk Catering']->id, 'first_name' => 'Frank', 'last_name' => 'van Dijk', 'email' => 'frank@vandijkcatering.nl', 'phone' => '+31612345030', 'status' => 'pending']);
// ── Factory persons (120) ──
$companyIds = collect($this->companies)->pluck('id');
// 80 factory volunteers
$factoryVolunteers = collect();
$factoryVolunteers = $factoryVolunteers->merge(Person::factory()->count(55)->approved()->create(['event_id' => $festival->id, 'crowd_type_id' => $vol]));
$factoryVolunteers = $factoryVolunteers->merge(Person::factory()->count(10)->create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'status' => 'pending']));
$factoryVolunteers = $factoryVolunteers->merge(Person::factory()->count(8)->create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'status' => 'applied']));
$factoryVolunteers = $factoryVolunteers->merge(Person::factory()->count(4)->create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'status' => 'invited']));
$factoryVolunteers = $factoryVolunteers->merge(Person::factory()->count(2)->create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'status' => 'rejected']));
$factoryVolunteers = $factoryVolunteers->merge(Person::factory()->count(1)->create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'status' => 'no_show']));
// 19 factory crew
Person::factory()->count(12)->approved()
->sequence(fn () => ['company_id' => $companyIds->random()])
->create(['event_id' => $festival->id, 'crowd_type_id' => $crew]);
Person::factory()->count(5)->approved()->create(['event_id' => $festival->id, 'crowd_type_id' => $crew]);
Person::factory()->count(2)->create(['event_id' => $festival->id, 'crowd_type_id' => $crew, 'status' => 'pending']);
// 7 factory press
Person::factory()->count(5)->approved()->create(['event_id' => $festival->id, 'crowd_type_id' => $press]);
Person::factory()->count(2)->create(['event_id' => $festival->id, 'crowd_type_id' => $press, 'status' => 'pending']);
// 8 factory guests
Person::factory()->count(6)->approved()->create(['event_id' => $festival->id, 'crowd_type_id' => $guest]);
Person::factory()->count(2)->create(['event_id' => $festival->id, 'crowd_type_id' => $guest, 'status' => 'invited']);
// 6 factory suppliers
Person::factory()->count(4)->approved()->create(['event_id' => $festival->id, 'crowd_type_id' => $supplier]);
Person::factory()->count(2)->create(['event_id' => $festival->id, 'crowd_type_id' => $supplier, 'status' => 'pending']);
// ── Identity match test data ──
// Persons whose emails match org member accounts (for email-based identity matching)
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Lisa', 'last_name' => 'van den Berg', 'email' => 'lisa@feestfabriek.nl', 'phone' => '+31612345040', 'status' => 'applied', 'date_of_birth' => '1993-05-12']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Sara', 'last_name' => 'de Groot', 'email' => 'sara@feestfabriek.nl', 'phone' => '+31612345041', 'status' => 'pending', 'date_of_birth' => '1991-08-24']);
// Person with fuzzy name match to org member "Nina Jansen" (different email)
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Nena', 'last_name' => 'Jansen', 'email' => 'nena.jansen@gmail.com', 'phone' => '+31612345042', 'status' => 'pending', 'date_of_birth' => null]);
// Person with fuzzy name match + DOB match to org member "Mark de Boer" (DOB already set in user creation)
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Marc', 'last_name' => 'de Boer', 'email' => 'marc.deboer@gmail.com', 'phone' => '+31612345043', 'status' => 'pending', 'date_of_birth' => '1988-03-17']);
// Person with unique email (no match expected)
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Unique', 'last_name' => 'Persoon', 'email' => 'unique.persoon@nowhere.test', 'phone' => '+31612345044', 'status' => 'pending']);
$linked = $this->linkUsersToApprovedPersons($festival);
$personCount = Person::where('event_id', $festival->id)->count();
$this->command->info(" {$personCount} persons created ({$linked} user accounts linked)");
// ── Named shift assignments (22) ──
$tom = $this->users['tom@feestfabriek.nl'];
$namedAssignments = [
// Jan de Vries: 3 bar shifts
['person' => $jan, 'shift' => 'hoofdbar_vr_middag', 'status' => ShiftAssignmentStatus::APPROVED, 'auto' => true],
['person' => $jan, 'shift' => 'hoofdbar_vr_avond', 'status' => ShiftAssignmentStatus::APPROVED, 'auto' => false],
['person' => $jan, 'shift' => 'hoofdbar_za_middag', 'status' => ShiftAssignmentStatus::APPROVED],
// Lisa Bakker
['person' => $lisaB, 'shift' => 'hoofdbar_vr_middag', 'status' => ShiftAssignmentStatus::APPROVED, 'auto' => true],
['person' => $lisaB, 'shift' => 'hoofdbar_za_middag', 'status' => ShiftAssignmentStatus::PENDING_APPROVAL],
['person' => $lisaB, 'shift' => 'theaterbar_zo_middag', 'status' => ShiftAssignmentStatus::APPROVED, 'auto' => true],
// Ahmed Hassan: 4 EHBO shifts (cross_event section using sub-event time slots)
['person' => $ahmedP, 'shift' => 'ehbo_vr_middag', 'status' => ShiftAssignmentStatus::APPROVED],
['person' => $ahmedP, 'shift' => 'ehbo_vr_avond', 'status' => ShiftAssignmentStatus::APPROVED],
['person' => $ahmedP, 'shift' => 'ehbo_za_middag', 'status' => ShiftAssignmentStatus::APPROVED],
['person' => $ahmedP, 'shift' => 'ehbo_za_avond', 'status' => ShiftAssignmentStatus::APPROVED],
// Sara Jansen
['person' => $saraJ, 'shift' => 'theaterbar_vr_avond', 'status' => ShiftAssignmentStatus::PENDING_APPROVAL],
// Tom Visser (volunteer)
['person' => $tomV, 'shift' => 'podiumtechniek_vr_avond', 'status' => ShiftAssignmentStatus::APPROVED],
// Fatima
['person' => $fatima, 'shift' => 'hoofdbar_za_avond', 'status' => ShiftAssignmentStatus::APPROVED],
// Lotte: terreinploeg
['person' => $lotte, 'shift' => 'terrein_opbouw1', 'status' => ShiftAssignmentStatus::APPROVED, 'auto' => true],
['person' => $lotte, 'shift' => 'terrein_afbraak', 'status' => ShiftAssignmentStatus::APPROVED, 'auto' => true],
// Robin
['person' => $robin, 'shift' => 'theaterbar_za_middag', 'status' => ShiftAssignmentStatus::APPROVED, 'auto' => true],
// Klaas: security, assigned by Tom
['person' => $klaas, 'shift' => 'security_nacht_vr', 'status' => ShiftAssignmentStatus::APPROVED, 'assigned_by' => $tom->id],
// Dennis: security
['person' => $dennis, 'shift' => 'security_nacht_za', 'status' => ShiftAssignmentStatus::APPROVED, 'assigned_by' => $tom->id],
// Daan: rejected
['person' => $daan, 'shift' => 'hoofdbar_vr_middag', 'status' => ShiftAssignmentStatus::REJECTED, 'reason' => 'Nog niet goedgekeurd'],
// Emma: cancelled
['person' => $emma, 'shift' => 'ingang_vr_middag', 'status' => ShiftAssignmentStatus::CANCELLED],
// Pieter: podiumtechniek with overlap
['person' => $pieter, 'shift' => 'podiumtechniek_vr_avond', 'status' => ShiftAssignmentStatus::APPROVED],
['person' => $pieter, 'shift' => 'podiumtechniek_za_avond', 'status' => ShiftAssignmentStatus::APPROVED],
];
$usedPersonSlots = []; // track person_id => [time_slot_ids] for factory assignments
foreach ($namedAssignments as $a) {
$shift = $s[$a['shift']];
$isApproved = $a['status'] === ShiftAssignmentStatus::APPROVED;
ShiftAssignment::create([
'shift_id' => $shift->id,
'person_id' => $a['person']->id,
'time_slot_id' => $shift->time_slot_id,
'status' => $a['status'],
'auto_approved' => $a['auto'] ?? false,
'assigned_by' => $a['assigned_by'] ?? null,
'assigned_at' => now(),
'approved_at' => $isApproved ? now() : null,
'rejection_reason' => $a['reason'] ?? null,
]);
$usedPersonSlots[$a['person']->id][] = $shift->time_slot_id;
}
// ── Intentional overbooking for UI testing ──
$overbookPersons = Person::where('event_id', $festival->id)
->where('status', 'approved')
->whereNotIn('id', array_keys($usedPersonSlots))
->limit(20)
->get();
// EHBO Vrijdag Middag: slots_total=3, target 4 approved (1 over)
// Already has 1 named approved (Ahmed). Add 3 more = 4 total.
$ehboOverbookTarget = 3;
$ehboShift = $s['ehbo_vr_middag'];
foreach ($overbookPersons->splice(0, $ehboOverbookTarget) as $person) {
ShiftAssignment::create([
'shift_id' => $ehboShift->id,
'person_id' => $person->id,
'time_slot_id' => $ehboShift->time_slot_id,
'status' => ShiftAssignmentStatus::APPROVED,
'auto_approved' => false,
'assigned_at' => now(),
'approved_at' => now(),
]);
$usedPersonSlots[$person->id][] = $ehboShift->time_slot_id;
}
// Terreinploeg Opbouw Dag 1: slots_total=15, target 17 approved (2 over)
// Already has 1 named approved (Lotte). Add 16 more = 17 total.
$terreinOverbookTarget = 16;
$terreinShift = $s['terrein_opbouw1'];
$terreinOverbookPersons = Person::where('event_id', $festival->id)
->where('status', 'approved')
->whereNotIn('id', collect($usedPersonSlots)->keys()->filter(
fn ($pid) => in_array($terreinShift->time_slot_id, $usedPersonSlots[$pid] ?? []),
)->values())
->limit($terreinOverbookTarget)
->get();
foreach ($terreinOverbookPersons as $person) {
ShiftAssignment::create([
'shift_id' => $terreinShift->id,
'person_id' => $person->id,
'time_slot_id' => $terreinShift->time_slot_id,
'status' => ShiftAssignmentStatus::APPROVED,
'auto_approved' => true,
'assigned_at' => now(),
'approved_at' => now(),
]);
$usedPersonSlots[$person->id][] = $terreinShift->time_slot_id;
}
// ── Factory shift assignments (~100) ──
// Track filled (approved/completed) counts per shift to respect slots_total
$shiftFilledCounts = [];
$existingAssignments = ShiftAssignment::whereIn('shift_id', collect($allShifts)->pluck('id'))
->whereIn('status', [ShiftAssignmentStatus::APPROVED, ShiftAssignmentStatus::COMPLETED])
->get()
->groupBy('shift_id');
foreach ($allShifts as $shift) {
$shiftFilledCounts[$shift->id] = ($existingAssignments[$shift->id] ?? collect())->count();
}
$approvedPersons = Person::where('event_id', $festival->id)
->where('status', 'approved')
->get();
$openShifts = collect($allShifts)->filter(fn (Shift $shift) => $shift->status === 'open' && $shift->slots_open_for_claiming > 0);
$statusPool = array_merge(
array_fill(0, 75, ShiftAssignmentStatus::APPROVED),
array_fill(0, 15, ShiftAssignmentStatus::PENDING_APPROVAL),
array_fill(0, 5, ShiftAssignmentStatus::REJECTED),
array_fill(0, 5, ShiftAssignmentStatus::CANCELLED),
);
shuffle($statusPool);
$statusIdx = 0;
foreach ($approvedPersons->shuffle() as $person) {
$existing = $usedPersonSlots[$person->id] ?? [];
$available = $openShifts->filter(fn (Shift $shift) => !in_array($shift->time_slot_id, $existing));
if ($available->isEmpty()) {
continue;
}
$numShifts = min(rand(2, 3), $available->count());
$picked = $available->shuffle()->take($numShifts);
foreach ($picked as $shift) {
$status = $statusPool[$statusIdx % count($statusPool)];
$statusIdx++;
$isApproved = in_array($status, [ShiftAssignmentStatus::APPROVED, ShiftAssignmentStatus::COMPLETED]);
// Skip approved assignments if shift is already at capacity
if ($isApproved && ($shiftFilledCounts[$shift->id] ?? 0) >= $shift->slots_total) {
continue;
}
ShiftAssignment::create([
'shift_id' => $shift->id,
'person_id' => $person->id,
'time_slot_id' => $shift->time_slot_id,
'status' => $status,
'auto_approved' => $isApproved && rand(0, 1) === 1,
'assigned_at' => now(),
'approved_at' => $isApproved ? now() : null,
'rejection_reason' => $status === ShiftAssignmentStatus::REJECTED ? 'Geen beschikbare plek' : null,
]);
if ($isApproved) {
$shiftFilledCounts[$shift->id] = ($shiftFilledCounts[$shift->id] ?? 0) + 1;
}
$existing[] = $shift->time_slot_id;
}
$usedPersonSlots[$person->id] = $existing;
}
$assignmentCount = ShiftAssignment::whereIn('shift_id', collect($allShifts)->pluck('id'))->count();
$this->command->info(" {$assignmentCount} shift assignments created");
// Auto-correct shift statuses based on actual fill
foreach ($allShifts as $shift) {
$filled = ShiftAssignment::where('shift_id', $shift->id)
->whereIn('status', [ShiftAssignmentStatus::APPROVED, ShiftAssignmentStatus::COMPLETED])
->count();
$correctStatus = $filled >= $shift->slots_total ? 'full' : 'open';
if ($shift->status !== $correctStatus) {
$shift->update(['status' => $correctStatus]);
}
}
// ── Volunteer availabilities (~70) ──
$volSlotKeys = ['vr_middag', 'vr_avond', 'za_middag', 'za_avond', 'zo_middag', 'zo_avond'];
$volSlots = collect($volSlotKeys)->map(fn (string $k) => $ts[$k]);
// Named availabilities
$namedAvails = [
[$jan, $volSlots->random(4), fn () => rand(2, 5)],
[$lisaB, $volSlots->random(5), fn () => rand(1, 5)],
[$ahmedP, $volSlots, fn () => 5],
[$saraJ, collect([$ts['vr_avond'], $ts['za_avond']]), fn () => rand(1, 5)],
[$fatima, collect([$ts['za_middag'], $ts['za_avond'], $ts['zo_middag']]), fn () => rand(1, 5)],
];
$usedAvails = [];
foreach ($namedAvails as [$person, $slots, $prefFn]) {
foreach ($slots as $slot) {
$combo = "{$person->id}_{$slot->id}";
if (isset($usedAvails[$combo])) {
continue;
}
VolunteerAvailability::create([
'person_id' => $person->id,
'time_slot_id' => $slot->id,
'preference_level' => $prefFn(),
'submitted_at' => now(),
]);
$usedAvails[$combo] = true;
}
}
// Factory availabilities for approved volunteers
$approvedVolunteers = Person::where('event_id', $festival->id)
->where('status', 'approved')
->whereHas('crowdType', fn ($q) => $q->where('system_type', 'VOLUNTEER'))
->get();
foreach ($approvedVolunteers as $person) {
$numSlots = rand(2, 5);
foreach ($volSlots->shuffle()->take($numSlots) as $slot) {
$combo = "{$person->id}_{$slot->id}";
if (isset($usedAvails[$combo])) {
continue;
}
VolunteerAvailability::create([
'person_id' => $person->id,
'time_slot_id' => $slot->id,
'preference_level' => rand(1, 5),
'submitted_at' => now(),
]);
$usedAvails[$combo] = true;
}
}
// ── Crowd Lists (7) ──
$bert = $this->users['bert@feestfabriek.nl'];
$allApprovedVolunteers = Person::where('event_id', $festival->id)->where('status', 'approved')
->whereHas('crowdType', fn ($q) => $q->where('system_type', 'VOLUNTEER'))->get();
$allApprovedCrew = Person::where('event_id', $festival->id)->where('status', 'approved')
->whereHas('crowdType', fn ($q) => $q->where('system_type', 'CREW'))->get();
$allApprovedGuests = Person::where('event_id', $festival->id)->where('status', 'approved')
->whereHas('crowdType', fn ($q) => $q->where('system_type', 'GUEST'))->get();
$this->createCrowdList($festival, 'Vaste Bar Crew', CrowdListType::INTERNAL, $this->crowdTypes['VOLUNTEER'], null, false, null, $allApprovedVolunteers->take(20), $bert);
$this->createCrowdList($festival, 'EHBO Team', CrowdListType::INTERNAL, $this->crowdTypes['VOLUNTEER'], null, false, 10, $allApprovedVolunteers->shuffle()->take(5), $bert);
$this->createCrowdList($festival, 'Terreinploeg Pool', CrowdListType::INTERNAL, $this->crowdTypes['VOLUNTEER'], null, true, 25, $allApprovedVolunteers->shuffle()->take(15), $bert);
$this->createCrowdList($festival, 'SecureEvent Beveiliging', CrowdListType::EXTERNAL, $this->crowdTypes['CREW'], $this->companies['SecureEvent BV'], true, 15, $allApprovedCrew->shuffle()->take(8), $bert);
$this->createCrowdList($festival, 'Podiumtechniek Crew', CrowdListType::EXTERNAL, $this->crowdTypes['CREW'], $this->companies['Podiumtechniek Rijnmond'], false, 6, $allApprovedCrew->shuffle()->take(5), $bert);
$this->createCrowdList($festival, 'Catering Crew', CrowdListType::EXTERNAL, $this->crowdTypes['CREW'], $this->companies['Van Dijk Catering'], false, 8, $allApprovedCrew->shuffle()->take(4), $bert);
$this->createCrowdList($festival, 'VIP Gastenlijst', CrowdListType::INTERNAL, $this->crowdTypes['GUEST'], null, true, 30, $allApprovedGuests->take(12), $bert);
// ── Event person activations (crew on sub-events) ──
$activations = [
[$klaas, [$vrijdag, $zaterdag, $zondag]],
[$dennis, [$zaterdag, $zondag]],
[$eva, [$vrijdag]],
[$maria, [$vrijdag, $zaterdag, $zondag]],
];
foreach ($activations as [$person, $events]) {
foreach ($events as $event) {
DB::table('event_person_activations')->insert([
'event_id' => $event->id,
'person_id' => $person->id,
]);
}
}
// Activate remaining approved crew on 1-3 random sub-events
$subEventIds = [$vrijdag->id, $zaterdag->id, $zondag->id];
$activatedPersonIds = collect($activations)->pluck('0.id');
$remainingCrew = $allApprovedCrew->reject(fn (Person $p) => $activatedPersonIds->contains($p->id));
foreach ($remainingCrew as $person) {
$numEvents = rand(1, 3);
$selectedEvents = collect($subEventIds)->shuffle()->take($numEvents);
foreach ($selectedEvents as $eventId) {
DB::table('event_person_activations')->insert([
'event_id' => $eventId,
'person_id' => $person->id,
]);
}
}
// ── User Organisation Tags ──
$tagData = [
['user' => $volunteerUsers['jan@gmail.com'], 'tag' => 'Tapper', 'source' => 'self_reported', 'proficiency' => 'experienced'],
['user' => $volunteerUsers['jan@gmail.com'], 'tag' => 'EHBO', 'source' => 'organiser_assigned', 'proficiency' => 'beginner', 'assigned_by' => $bert->id],
['user' => $volunteerUsers['ahmed.h@gmail.com'], 'tag' => 'EHBO', 'source' => 'self_reported', 'proficiency' => 'expert'],
['user' => $volunteerUsers['ahmed.h@gmail.com'], 'tag' => 'BHV', 'source' => 'organiser_assigned', 'proficiency' => 'expert', 'assigned_by' => $bert->id],
['user' => $volunteerUsers['ahmed.h@gmail.com'], 'tag' => 'Rijbewijs B', 'source' => 'self_reported', 'proficiency' => null],
['user' => $volunteerUsers['tom.visser@gmail.com'], 'tag' => 'Podiumervaring', 'source' => 'self_reported', 'proficiency' => 'experienced'],
['user' => $volunteerUsers['tom.visser@gmail.com'], 'tag' => 'Engels', 'source' => 'self_reported', 'proficiency' => 'experienced'],
['user' => $volunteerUsers['tom.visser@gmail.com'], 'tag' => 'Duits', 'source' => 'self_reported', 'proficiency' => 'beginner'],
['user' => $volunteerUsers['lotte@gmail.com'], 'tag' => 'Rijbewijs B', 'source' => 'self_reported', 'proficiency' => null],
['user' => $volunteerUsers['lotte@gmail.com'], 'tag' => 'Heftruck', 'source' => 'organiser_assigned', 'proficiency' => 'experienced', 'assigned_by' => $bert->id],
];
foreach ($tagData as $td) {
UserOrganisationTag::create([
'user_id' => $td['user']->id,
'organisation_id' => $this->org->id,
'person_tag_id' => $this->personTags[$td['tag']]->id,
'source' => $td['source'],
'assigned_by_user_id' => $td['assigned_by'] ?? null,
'proficiency' => $td['proficiency'],
'assigned_at' => now(),
]);
}
$this->command->info(' Echt Feesten 2026 complete');
});
}
// =========================================================================
// Event 2: Braderie Dorpstown 2026 (flat event, registration_open)
// =========================================================================
private function seedBraderie(): void
{
DB::transaction(function (): void {
$this->command->info('Seeding Braderie Dorpstown 2026...');
$braderie = Event::create([
'organisation_id' => $this->org->id,
'name' => 'Braderie Dorpstown 2026',
'slug' => 'braderie-dorpstown-2026',
'start_date' => '2026-06-14',
'end_date' => '2026-06-14',
'timezone' => 'Europe/Amsterdam',
'status' => 'registration_open',
'event_type' => 'event',
]);
// ── Time Slots (3) ──
$tsOchtend = TimeSlot::create(['event_id' => $braderie->id, 'name' => 'Zondag ochtend — vrijwilliger', 'person_type' => 'VOLUNTEER', 'date' => '2026-06-14', 'start_time' => '07:00', 'end_time' => '13:00', 'duration_hours' => 6.00]);
$tsMiddag = TimeSlot::create(['event_id' => $braderie->id, 'name' => 'Zondag middag — vrijwilliger', 'person_type' => 'VOLUNTEER', 'date' => '2026-06-14', 'start_time' => '13:00', 'end_time' => '18:00', 'duration_hours' => 5.00]);
$tsCrew = TimeSlot::create(['event_id' => $braderie->id, 'name' => 'Zondag — crew', 'person_type' => 'CREW', 'date' => '2026-06-14', 'start_time' => '06:00', 'end_time' => '20:00', 'duration_hours' => 14.00]);
// ── Sections (2) ──
$verkeersregelaars = FestivalSection::create([
'event_id' => $braderie->id, 'name' => 'Verkeersregelaars', 'type' => 'standard',
'category' => 'Veiligheid', 'icon' => 'tabler-traffic-cone', 'sort_order' => 1,
'responder_self_checkin' => true, 'crew_auto_accepts' => false,
'show_in_registration' => true, 'registration_description' => 'Verkeersregelaar bij de braderie',
]);
$centraleBar = FestivalSection::create([
'event_id' => $braderie->id, 'name' => 'Centrale bar', 'type' => 'standard',
'category' => 'Bar', 'icon' => 'tabler-beer', 'sort_order' => 2,
'responder_self_checkin' => true, 'crew_auto_accepts' => true,
'show_in_registration' => true, 'registration_description' => 'Tappen en serveren bij de centrale bar',
]);
// ── Shifts (4) ──
Shift::create(['festival_section_id' => $verkeersregelaars->id, 'time_slot_id' => $tsOchtend->id, 'title' => 'Verkeersregelaar', 'slots_total' => 4, 'slots_open_for_claiming' => 4, 'status' => 'open']);
Shift::create(['festival_section_id' => $verkeersregelaars->id, 'time_slot_id' => $tsMiddag->id, 'title' => 'Verkeersregelaar', 'slots_total' => 4, 'slots_open_for_claiming' => 4, 'status' => 'open']);
Shift::create(['festival_section_id' => $centraleBar->id, 'time_slot_id' => $tsOchtend->id, 'title' => 'Tapper', 'slots_total' => 6, 'slots_open_for_claiming' => 6, 'status' => 'open']);
Shift::create(['festival_section_id' => $centraleBar->id, 'time_slot_id' => $tsMiddag->id, 'title' => 'Tapper', 'slots_total' => 6, 'slots_open_for_claiming' => 6, 'status' => 'open']);
// ── Persons (15) ──
$vol = $this->crowdTypes['VOLUNTEER']->id;
Person::factory()->count(10)->approved()->create(['event_id' => $braderie->id, 'crowd_type_id' => $vol]);
Person::factory()->count(3)->create(['event_id' => $braderie->id, 'crowd_type_id' => $vol, 'status' => 'pending']);
Person::factory()->count(2)->create(['event_id' => $braderie->id, 'crowd_type_id' => $vol, 'status' => 'applied']);
$this->linkUsersToApprovedPersons($braderie);
$this->command->info(' Braderie Dorpstown 2026 complete');
});
}
// =========================================================================
// Event 3: IJsbaan Winterpark (series, published, 60 persons)
// =========================================================================
private function seedIJsbaan(): void
{
DB::transaction(function (): void {
$this->command->info('Seeding IJsbaan Winterpark...');
// ── Event hierarchy ──
$ijsbaan = Event::create([
'organisation_id' => $this->org->id,
'name' => 'IJsbaan Winterpark',
'slug' => 'ijsbaan-winterpark',
'start_date' => '2026-12-05',
'end_date' => '2027-01-25',
'timezone' => 'Europe/Amsterdam',
'status' => 'published',
'event_type' => 'series',
'event_type_label' => 'Schaatsseizoen',
'sub_event_label' => 'Editie',
]);
$weeksData = [
['name' => 'Week 1 — Opening', 'start' => '2026-12-05', 'end' => '2026-12-07', 'status' => 'published', 'zaterdag' => '2026-12-05', 'zondag' => '2026-12-06'],
['name' => 'Week 2 — Kerst Special', 'start' => '2026-12-19', 'end' => '2026-12-21', 'status' => 'published', 'zaterdag' => '2026-12-19', 'zondag' => '2026-12-20'],
['name' => 'Week 3 — Nieuwjaar', 'start' => '2027-01-02', 'end' => '2027-01-04', 'status' => 'draft', 'zaterdag' => '2027-01-03', 'zondag' => '2027-01-04'],
['name' => 'Week 4 — Afsluiting', 'start' => '2027-01-23', 'end' => '2027-01-25', 'status' => 'draft', 'zaterdag' => '2027-01-24', 'zondag' => '2027-01-25'],
];
$weeks = [];
foreach ($weeksData as $i => $wd) {
$weeks[$i] = Event::create([
'organisation_id' => $this->org->id,
'parent_event_id' => $ijsbaan->id,
'name' => $wd['name'],
'slug' => 'ijsbaan-week-' . ($i + 1),
'start_date' => $wd['start'],
'end_date' => $wd['end'],
'timezone' => 'Europe/Amsterdam',
'status' => $wd['status'],
'event_type' => 'event',
]);
}
// ── Locations (2, on parent) ──
Location::create(['event_id' => $ijsbaan->id, 'name' => 'IJsbaan Kralingse Plas', 'address' => 'Kralingseweg 200, Rotterdam', 'lat' => 51.9300, 'lng' => 4.5100]);
Location::create(['event_id' => $ijsbaan->id, 'name' => 'Verwarmde tent', 'address' => 'Kralingseweg 200A, Rotterdam', 'lat' => 51.9302, 'lng' => 4.5105]);
// ── Sections, time slots, shifts per sub-event ──
$sectionDefs = [
['name' => 'Schaatsbaan Bar', 'category' => 'Bar', 'icon' => 'tabler-beer', 'show_in_registration' => true, 'registration_description' => 'Warme dranken en snacks serveren aan schaatsers'],
['name' => 'Schaatsverhuur', 'category' => 'Ontvangst', 'icon' => 'tabler-ticket', 'show_in_registration' => true, 'registration_description' => 'Schaatsen uitgeven en innemen bij de verhuurbalie'],
['name' => 'Terrein', 'category' => 'Productie', 'icon' => 'tabler-shovel', 'show_in_registration' => true, 'registration_description' => 'IJsbaan onderhoud en terreinbeheer'],
];
$allShifts = [];
foreach ($weeks as $i => $week) {
$wd = $weeksData[$i];
$isOpen = in_array($wd['status'], ['published', 'registration_open']);
// Sections
$weekSections = [];
foreach ($sectionDefs as $order => $sd) {
$weekSections[] = FestivalSection::create([
'event_id' => $week->id,
'name' => $sd['name'],
'type' => 'standard',
'category' => $sd['category'],
'icon' => $sd['icon'],
'sort_order' => $order + 1,
'responder_self_checkin' => true,
'crew_auto_accepts' => true,
'show_in_registration' => $sd['show_in_registration'] ?? false,
'registration_description' => $sd['registration_description'] ?? null,
]);
}
// Time slots
$zatSlot = TimeSlot::create(['event_id' => $week->id, 'name' => 'Zaterdag', 'person_type' => 'VOLUNTEER', 'date' => $wd['zaterdag'], 'start_time' => '10:00', 'end_time' => '18:00', 'duration_hours' => 8.00]);
$zonSlot = TimeSlot::create(['event_id' => $week->id, 'name' => 'Zondag', 'person_type' => 'VOLUNTEER', 'date' => $wd['zondag'], 'start_time' => '10:00', 'end_time' => '18:00', 'duration_hours' => 8.00]);
// Shifts
foreach ($weekSections as $section) {
foreach ([$zatSlot, $zonSlot] as $slot) {
$shift = Shift::create([
'festival_section_id' => $section->id,
'time_slot_id' => $slot->id,
'title' => $section->name,
'slots_total' => 6,
'slots_open_for_claiming' => 6,
'status' => $isOpen ? 'open' : 'draft',
]);
$allShifts[] = $shift;
}
}
}
// ── Persons (60, all factory) ──
$vol = $this->crowdTypes['VOLUNTEER']->id;
$crewType = $this->crowdTypes['CREW']->id;
$guestType = $this->crowdTypes['GUEST']->id;
$approvedVol = Person::factory()->count(35)->approved()->create(['event_id' => $ijsbaan->id, 'crowd_type_id' => $vol]);
Person::factory()->count(5)->create(['event_id' => $ijsbaan->id, 'crowd_type_id' => $vol, 'status' => 'pending']);
Person::factory()->count(3)->create(['event_id' => $ijsbaan->id, 'crowd_type_id' => $vol, 'status' => 'applied']);
$approvedCrew = Person::factory()->count(10)->approved()->create(['event_id' => $ijsbaan->id, 'crowd_type_id' => $crewType]);
Person::factory()->count(2)->create(['event_id' => $ijsbaan->id, 'crowd_type_id' => $crewType, 'status' => 'pending']);
Person::factory()->count(5)->approved()->create(['event_id' => $ijsbaan->id, 'crowd_type_id' => $guestType]);
$this->linkUsersToApprovedPersons($ijsbaan);
// ── Shift assignments (~80) ──
$openShifts = collect($allShifts)->filter(fn (Shift $shift) => $shift->status === 'open' && $shift->slots_open_for_claiming > 0);
$draftShifts = collect($allShifts)->filter(fn (Shift $shift) => $shift->status === 'draft');
$allApproved = $approvedVol->merge($approvedCrew);
// Week 1+2: up to slots_total per open shift
foreach ($openShifts as $shift) {
$count = min(rand(5, 6), $shift->slots_total);
$assigned = $allApproved->shuffle()->take($count);
foreach ($assigned as $person) {
ShiftAssignment::create([
'shift_id' => $shift->id,
'person_id' => $person->id,
'time_slot_id' => $shift->time_slot_id,
'status' => ShiftAssignmentStatus::APPROVED,
'auto_approved' => true,
'assigned_at' => now(),
'approved_at' => now(),
]);
}
}
// ~10 pending for week 3 draft shifts
$pendingCount = 0;
foreach ($draftShifts->take(4) as $shift) {
$assigned = $allApproved->shuffle()->take(rand(2, 3));
foreach ($assigned as $person) {
ShiftAssignment::create([
'shift_id' => $shift->id,
'person_id' => $person->id,
'time_slot_id' => $shift->time_slot_id,
'status' => ShiftAssignmentStatus::PENDING_APPROVAL,
'assigned_at' => now(),
]);
$pendingCount++;
if ($pendingCount >= 10) {
break 2;
}
}
}
// ── Crowd Lists (2) ──
$bert = $this->users['bert@feestfabriek.nl'];
$this->createCrowdList($ijsbaan, 'IJsbaan Vrijwilligerspool', CrowdListType::INTERNAL, $this->crowdTypes['VOLUNTEER'], null, false, null, $approvedVol, $bert);
$this->createCrowdList($ijsbaan, 'IJsbaan Vaste Crew', CrowdListType::INTERNAL, $this->crowdTypes['CREW'], null, false, null, $approvedCrew, $bert);
$personCount = Person::where('event_id', $ijsbaan->id)->count();
$this->command->info(" {$personCount} persons, " . count($allShifts) . ' shifts created');
});
}
// =========================================================================
// Event 3: Koningsdag Rotterdam 2026 (flat event, closed, 100 persons)
// =========================================================================
private function seedKoningsdag(): void
{
DB::transaction(function (): void {
$this->command->info('Seeding Koningsdag Rotterdam 2026...');
// ── Event ──
$koningsdag = Event::create([
'organisation_id' => $this->org->id,
'name' => 'Koningsdag Rotterdam 2026',
'slug' => 'koningsdag-rotterdam-2026',
'start_date' => '2026-04-27',
'end_date' => '2026-04-27',
'timezone' => 'Europe/Amsterdam',
'status' => 'closed',
'event_type' => 'event',
]);
// ── Locations (3) ──
$locErasmus = Location::create(['event_id' => $koningsdag->id, 'name' => 'Erasmusbrug podium', 'address' => 'Erasmusbrug, Rotterdam', 'lat' => 51.9090, 'lng' => 4.4870]);
$locWillems = Location::create(['event_id' => $koningsdag->id, 'name' => 'Willemsplein', 'address' => 'Willemsplein, Rotterdam', 'lat' => 51.9070, 'lng' => 4.4860]);
$locOudeHaven = Location::create(['event_id' => $koningsdag->id, 'name' => 'Oude Haven', 'address' => 'Oude Haven, Rotterdam', 'lat' => 51.9200, 'lng' => 4.4950]);
// ── Sections (4) ──
$secPodium = FestivalSection::create(['event_id' => $koningsdag->id, 'name' => 'Podium Erasmusbrug', 'type' => 'standard', 'category' => 'Podium', 'icon' => 'tabler-microphone-2', 'sort_order' => 1, 'responder_self_checkin' => true, 'show_in_registration' => true, 'registration_description' => 'Podiummedewerker bij de Erasmusbrug']);
$secBar = FestivalSection::create(['event_id' => $koningsdag->id, 'name' => 'Bar Willemsplein', 'type' => 'standard', 'category' => 'Bar', 'icon' => 'tabler-beer', 'sort_order' => 2, 'responder_self_checkin' => true, 'crew_auto_accepts' => true, 'show_in_registration' => true, 'registration_description' => 'Tappen en serveren op het Willemsplein']);
$secKids = FestivalSection::create(['event_id' => $koningsdag->id, 'name' => 'Kinderactiviteiten', 'type' => 'standard', 'category' => 'Entertainment', 'icon' => 'tabler-balloon', 'sort_order' => 3, 'responder_self_checkin' => true, 'show_in_registration' => true, 'registration_description' => 'Begeleid kinderactiviteiten en spelletjes']);
$secBev = FestivalSection::create(['event_id' => $koningsdag->id, 'name' => 'Beveiliging', 'type' => 'standard', 'category' => 'Veiligheid', 'icon' => 'tabler-shield', 'sort_order' => 4, 'responder_self_checkin' => true, 'show_in_registration' => true, 'registration_description' => 'Beveiliging en crowd management']);
$kSections = [$secPodium, $secBar, $secKids, $secBev];
$kLocations = [$locErasmus, $locWillems, $locOudeHaven, $locOudeHaven];
// ── Time Slots (3) ──
$tsOchtend = TimeSlot::create(['event_id' => $koningsdag->id, 'name' => 'Ochtend', 'person_type' => 'VOLUNTEER', 'date' => '2026-04-27', 'start_time' => '09:00', 'end_time' => '13:00', 'duration_hours' => 4.00]);
$tsMiddag = TimeSlot::create(['event_id' => $koningsdag->id, 'name' => 'Middag', 'person_type' => 'VOLUNTEER', 'date' => '2026-04-27', 'start_time' => '13:00', 'end_time' => '18:00', 'duration_hours' => 5.00]);
$tsAvond = TimeSlot::create(['event_id' => $koningsdag->id, 'name' => 'Avond', 'person_type' => 'VOLUNTEER', 'date' => '2026-04-27', 'start_time' => '18:00', 'end_time' => '23:00', 'duration_hours' => 5.00]);
$kSlots = [$tsOchtend, $tsMiddag, $tsAvond];
// ── Shifts (12, all completed) ──
$kShifts = [];
foreach ($kSections as $i => $section) {
foreach ($kSlots as $slot) {
$slotsTotal = $section->name === 'Bar Willemsplein' ? 15 : rand(8, 12);
$kShifts[] = Shift::create([
'festival_section_id' => $section->id,
'time_slot_id' => $slot->id,
'location_id' => $kLocations[$i]->id,
'title' => match ($section->name) {
'Bar Willemsplein' => 'Tapper',
'Podium Erasmusbrug' => 'Podiummedewerker',
'Kinderactiviteiten' => 'Animator',
'Beveiliging' => 'Beveiliger',
},
'slots_total' => $slotsTotal,
'slots_open_for_claiming' => 0,
'status' => 'completed',
]);
}
}
// ── Persons (100, all factory) ──
$vol = $this->crowdTypes['VOLUNTEER']->id;
$crewType = $this->crowdTypes['CREW']->id;
$pressType = $this->crowdTypes['PRESS']->id;
$guestType = $this->crowdTypes['GUEST']->id;
$supplierType = $this->crowdTypes['SUPPLIER']->id;
// 75 volunteers (55 approved, 8 no_show, 5 rejected, 4 approved no-shift, 3 rejected)
$kApprovedVol = Person::factory()->count(59)->approved()->create(['event_id' => $koningsdag->id, 'crowd_type_id' => $vol]);
$kNoShowVol = Person::factory()->count(8)->create(['event_id' => $koningsdag->id, 'crowd_type_id' => $vol, 'status' => 'no_show']);
Person::factory()->count(8)->create(['event_id' => $koningsdag->id, 'crowd_type_id' => $vol, 'status' => 'rejected']);
// 12 crew (8 with SecureEvent BV)
$kCrew = Person::factory()->count(8)->approved()->create(['event_id' => $koningsdag->id, 'crowd_type_id' => $crewType, 'company_id' => $this->companies['SecureEvent BV']->id]);
$kCrew = $kCrew->merge(Person::factory()->count(4)->approved()->create(['event_id' => $koningsdag->id, 'crowd_type_id' => $crewType]));
// 6 press
Person::factory()->count(6)->approved()->create(['event_id' => $koningsdag->id, 'crowd_type_id' => $pressType]);
// 5 guests (with Rotterdam Festivals)
$kGuests = Person::factory()->count(5)->approved()->create(['event_id' => $koningsdag->id, 'crowd_type_id' => $guestType, 'company_id' => $this->companies['Rotterdam Festivals']->id]);
// 2 suppliers
Person::factory()->count(2)->approved()->create(['event_id' => $koningsdag->id, 'crowd_type_id' => $supplierType]);
$this->linkUsersToApprovedPersons($koningsdag);
// ── Shift assignments (~150) ──
// 120 completed + 12 cancelled + 8 no-show + 5 rejected + 5 pending
$statusPool = array_merge(
array_fill(0, 120, 'completed'),
array_fill(0, 12, 'cancelled'),
array_fill(0, 5, 'rejected'),
array_fill(0, 5, 'pending'),
);
shuffle($statusPool);
$statusIdx = 0;
// Assign approved volunteers 2-3 shifts each
foreach ($kApprovedVol as $person) {
$numShifts = rand(2, 3);
$picked = collect($kShifts)->shuffle()->take($numShifts);
foreach ($picked as $shift) {
$statusKey = $statusPool[$statusIdx % count($statusPool)] ?? 'completed';
$statusIdx++;
$status = match ($statusKey) {
'completed' => ShiftAssignmentStatus::COMPLETED,
'cancelled' => ShiftAssignmentStatus::CANCELLED,
'rejected' => ShiftAssignmentStatus::REJECTED,
'pending' => ShiftAssignmentStatus::PENDING_APPROVAL,
};
$isCompleted = $status === ShiftAssignmentStatus::COMPLETED;
ShiftAssignment::create([
'shift_id' => $shift->id,
'person_id' => $person->id,
'time_slot_id' => $shift->time_slot_id,
'status' => $status,
'auto_approved' => $isCompleted,
'assigned_at' => now(),
'approved_at' => $isCompleted ? now() : null,
'hours_completed' => $isCompleted ? round(rand(35, 80) / 10, 1) : null,
'checked_in_at' => $isCompleted ? now() : null,
'checked_out_at' => $isCompleted ? now()->addHours(rand(3, 8)) : null,
]);
}
}
// No-show persons: approved assignment but no check-in
foreach ($kNoShowVol as $person) {
$shift = collect($kShifts)->random();
ShiftAssignment::create([
'shift_id' => $shift->id,
'person_id' => $person->id,
'time_slot_id' => $shift->time_slot_id,
'status' => ShiftAssignmentStatus::APPROVED,
'auto_approved' => true,
'assigned_at' => now(),
'approved_at' => now(),
'checked_in_at' => null,
]);
}
// ── Crowd Lists (3) ──
$bert = $this->users['bert@feestfabriek.nl'];
$allKVolunteers = Person::where('event_id', $koningsdag->id)->where('status', 'approved')
->whereHas('crowdType', fn ($q) => $q->where('system_type', 'VOLUNTEER'))->get();
$this->createCrowdList($koningsdag, 'Koningsdag Crew 2026', CrowdListType::INTERNAL, $this->crowdTypes['VOLUNTEER'], null, false, null, $allKVolunteers->take(55), $bert);
$this->createCrowdList($koningsdag, 'Beveiliging Koningsdag', CrowdListType::EXTERNAL, $this->crowdTypes['CREW'], $this->companies['SecureEvent BV'], false, null, $kCrew->take(8), $bert);
$this->createCrowdList($koningsdag, 'Gemeente Gasten', CrowdListType::EXTERNAL, $this->crowdTypes['GUEST'], $this->companies['Rotterdam Festivals'], false, null, $kGuests, $bert);
$personCount = Person::where('event_id', $koningsdag->id)->count();
$assignCount = ShiftAssignment::whereIn('shift_id', collect($kShifts)->pluck('id'))->count();
$this->command->info(" {$personCount} persons, 12 shifts, {$assignCount} assignments");
});
}
// =========================================================================
// Event 4: Nacht van de Kaap 2026 (flat event, draft, empty)
// =========================================================================
private function seedNachtVanDeKaap(): void
{
DB::transaction(function (): void {
$this->command->info('Seeding Nacht van de Kaap 2026...');
$event = Event::create([
'organisation_id' => $this->org->id,
'name' => 'Nacht van de Kaap 2026',
'slug' => 'nacht-van-de-kaap-2026',
'start_date' => '2026-09-12',
'end_date' => '2026-09-12',
'timezone' => 'Europe/Amsterdam',
'status' => 'draft',
'event_type' => 'event',
]);
Location::create(['event_id' => $event->id, 'name' => 'Katendrecht', 'address' => 'Deliplein, Rotterdam', 'lat' => 51.8950, 'lng' => 4.4850]);
FestivalSection::create(['event_id' => $event->id, 'name' => 'Kaapse Bar', 'type' => 'standard', 'category' => 'Bar', 'icon' => 'tabler-beer', 'sort_order' => 1, 'responder_self_checkin' => true]);
FestivalSection::create(['event_id' => $event->id, 'name' => 'Podium Deliplein', 'type' => 'standard', 'category' => 'Podium', 'icon' => 'tabler-microphone-2', 'sort_order' => 2, 'responder_self_checkin' => true]);
TimeSlot::create(['event_id' => $event->id, 'name' => 'Nacht', 'person_type' => 'VOLUNTEER', 'date' => '2026-09-12', 'start_time' => '20:00', 'end_time' => '04:00', 'duration_hours' => 8.00]);
$this->command->info(' Empty draft event created');
});
}
// =========================================================================
// Helpers
// =========================================================================
/**
* Create User accounts for approved/no_show persons that lack one.
* Mirrors the production approval flow (PersonController::approve).
*/
private function linkUsersToApprovedPersons(Event $event): int
{
$linked = 0;
Person::withoutGlobalScopes()
->where('event_id', $event->id)
->whereIn('status', ['approved', 'no_show'])
->whereNull('user_id')
->each(function (Person $person) use (&$linked): void {
$user = User::firstOrCreate(
['email' => strtolower($person->email)],
[
'first_name' => $person->first_name,
'last_name' => $person->last_name,
'password' => Hash::make('password'),
]
);
$person->user_id = $user->id;
$person->save();
$linked++;
});
return $linked;
}
private function createCrowdList(
Event $event,
string $name,
CrowdListType $type,
CrowdType $crowdType,
?Company $company,
bool $autoApprove,
?int $maxPersons,
Collection $persons,
User $addedBy,
): CrowdList {
$list = CrowdList::create([
'event_id' => $event->id,
'crowd_type_id' => $crowdType->id,
'name' => $name,
'type' => $type,
'recipient_company_id' => $company?->id,
'auto_approve' => $autoApprove,
'max_persons' => $maxPersons,
]);
$pivotData = $persons->mapWithKeys(fn (Person $p) => [
$p->id => ['added_at' => now(), 'added_by_user_id' => $addedBy->id],
])->all();
$list->persons()->attach($pivotData);
return $list;
}
}