feat: registration section preferences with show_in_registration filtering and deduplication
Add show_in_registration and registration_description columns to festival_sections. Registration form now shows deduplicated sections by name (across sub-events), filtered by show_in_registration=true, grouped by category with card-based UI. Section preferences use section_name instead of section_id. Add GET/PUT registration-settings endpoints for festival-level bulk management. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('festival_sections', function (Blueprint $table) {
|
||||
$table->boolean('show_in_registration')->default(false)->after('timed_accreditations');
|
||||
$table->text('registration_description')->nullable()->after('show_in_registration');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('festival_sections', function (Blueprint $table) {
|
||||
$table->dropColumn(['show_in_registration', 'registration_description']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -247,34 +247,38 @@ class DevSeeder extends Seeder
|
||||
'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,
|
||||
]);
|
||||
|
||||
$nachtsecurity = FestivalSection::create([
|
||||
'event_id' => $festival->id, 'name' => 'Nachtsecurity', 'type' => 'standard',
|
||||
'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],
|
||||
'theaterbar' => ['name' => 'Theatertent Bar', 'category' => 'Bar', 'icon' => 'tabler-beer', 'crew_auto_accepts' => true],
|
||||
'hospitality' => ['name' => 'Backstage Hospitality', 'category' => 'Hospitality', 'icon' => 'tabler-armchair', 'crew_auto_accepts' => false],
|
||||
'podiumtechniek' => ['name' => 'Podiumtechniek', 'category' => 'Techniek', 'icon' => 'tabler-speakerphone', 'crew_auto_accepts' => false],
|
||||
'ingang' => ['name' => 'Ingang & Tickets', 'category' => 'Ontvangst', 'icon' => 'tabler-ticket', 'crew_auto_accepts' => true],
|
||||
'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 = [];
|
||||
@@ -290,6 +294,8 @@ class DevSeeder extends Seeder
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -598,8 +604,67 @@ class DevSeeder extends Seeder
|
||||
$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 Early: 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_early'];
|
||||
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();
|
||||
@@ -631,6 +696,11 @@ class DevSeeder extends Seeder
|
||||
$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,
|
||||
@@ -641,6 +711,11 @@ class DevSeeder extends Seeder
|
||||
'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;
|
||||
@@ -840,9 +915,9 @@ class DevSeeder extends Seeder
|
||||
// ── Sections, time slots, shifts per sub-event ──
|
||||
|
||||
$sectionDefs = [
|
||||
['name' => 'Schaatsbaan Bar', 'category' => 'Bar', 'icon' => 'tabler-beer'],
|
||||
['name' => 'Schaatsverhuur', 'category' => 'Ontvangst', 'icon' => 'tabler-ticket'],
|
||||
['name' => 'Terrein', 'category' => 'Productie', 'icon' => 'tabler-shovel'],
|
||||
['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 = [];
|
||||
@@ -863,6 +938,8 @@ class DevSeeder extends Seeder
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -905,9 +982,10 @@ class DevSeeder extends Seeder
|
||||
$draftShifts = collect($allShifts)->filter(fn (Shift $shift) => $shift->status === 'draft');
|
||||
$allApproved = $approvedVol->merge($approvedCrew);
|
||||
|
||||
// Week 1+2: 5-6 per open shift
|
||||
// Week 1+2: up to slots_total per open shift
|
||||
foreach ($openShifts as $shift) {
|
||||
$assigned = $allApproved->shuffle()->take(rand(5, 6));
|
||||
$count = min(rand(5, 6), $shift->slots_total);
|
||||
$assigned = $allApproved->shuffle()->take($count);
|
||||
foreach ($assigned as $person) {
|
||||
ShiftAssignment::create([
|
||||
'shift_id' => $shift->id,
|
||||
@@ -981,10 +1059,10 @@ class DevSeeder extends Seeder
|
||||
|
||||
// ── 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]);
|
||||
$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]);
|
||||
$secKids = FestivalSection::create(['event_id' => $koningsdag->id, 'name' => 'Kinderactiviteiten', 'type' => 'standard', 'category' => 'Entertainment', 'icon' => 'tabler-balloon', 'sort_order' => 3, 'responder_self_checkin' => true]);
|
||||
$secBev = FestivalSection::create(['event_id' => $koningsdag->id, 'name' => 'Beveiliging', 'type' => 'standard', 'category' => 'Veiligheid', 'icon' => 'tabler-shield', 'sort_order' => 4, 'responder_self_checkin' => true]);
|
||||
$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];
|
||||
|
||||
Reference in New Issue
Block a user