feat: fase 2 backend — crowd types, persons, sections, shifts, invite flow

- Crowd Types + Persons CRUD (73 tests)
- Festival Sections + Time Slots + Shifts CRUD met assign/claim flow (84 tests)
- Invite Flow + Member Management met InvitationService (109 tests)
- Schema v1.6 migraties volledig uitgevoerd
- DevSeeder bijgewerkt met crowd types voor testorganisatie
This commit is contained in:
2026-04-08 01:34:46 +02:00
parent c417a6647a
commit 9acb27af3a
114 changed files with 6916 additions and 984 deletions

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use App\Models\User;
use App\Models\UserInvitation;
use Illuminate\Foundation\Http\FormRequest;
final class AcceptInvitationRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
$invitation = UserInvitation::where('token', $this->route('token'))->first();
$userExists = $invitation && User::where('email', $invitation->email)->exists();
return [
'name' => [$userExists ? 'nullable' : 'required', 'string', 'max:255'],
'password' => [$userExists ? 'nullable' : 'required', 'string', 'min:8', 'confirmed'],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class AssignShiftRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'person_id' => ['required', 'ulid', 'exists:persons,id'],
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class ReorderFestivalSectionsRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'sections' => ['required', 'array'],
'sections.*.id' => ['required', 'ulid'],
'sections.*.sort_order' => ['required', 'integer', 'min:0'],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StoreCompanyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'type' => ['required', 'in:supplier,partner,agency,venue,other'],
'contact_name' => ['nullable', 'string', 'max:255'],
'contact_email' => ['nullable', 'email', 'max:255'],
'contact_phone' => ['nullable', 'string', 'max:30'],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StoreCrowdListRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'crowd_type_id' => ['required', 'ulid', 'exists:crowd_types,id'],
'name' => ['required', 'string', 'max:255'],
'type' => ['required', 'in:internal,external'],
'recipient_company_id' => ['nullable', 'ulid', 'exists:companies,id'],
'auto_approve' => ['sometimes', 'boolean'],
'max_persons' => ['nullable', 'integer', 'min:1'],
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StoreCrowdTypeRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'system_type' => ['required', 'in:CREW,GUEST,ARTIST,VOLUNTEER,PRESS,PARTNER,SUPPLIER'],
'color' => ['required', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'icon' => ['nullable', 'string', 'max:50'],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StoreFestivalSectionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'sort_order' => ['nullable', 'integer', 'min:0'],
'type' => ['nullable', 'in:standard,cross_event'],
'crew_auto_accepts' => ['nullable', 'boolean'],
'responder_self_checkin' => ['nullable', 'boolean'],
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StoreInvitationRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'email' => ['required', 'email', 'max:255'],
'role' => ['required', 'in:org_admin,org_member,event_manager,staff_coordinator,volunteer_coordinator'],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StoreLocationRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'address' => ['nullable', 'string', 'max:255'],
'lat' => ['nullable', 'numeric', 'between:-90,90'],
'lng' => ['nullable', 'numeric', 'between:-180,180'],
'description' => ['nullable', 'string'],
'access_instructions' => ['nullable', 'string'],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StorePersonRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'crowd_type_id' => ['required', 'ulid', 'exists:crowd_types,id'],
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255'],
'phone' => ['nullable', 'string', 'max:30'],
'company_id' => ['nullable', 'ulid', 'exists:companies,id'],
'status' => ['nullable', 'in:invited,applied,pending,approved,rejected,no_show'],
'custom_fields' => ['nullable', 'array'],
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StoreShiftRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'time_slot_id' => ['required', 'ulid', 'exists:time_slots,id'],
'location_id' => ['nullable', 'ulid', 'exists:locations,id'],
'title' => ['nullable', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'instructions' => ['nullable', 'string'],
'coordinator_notes' => ['nullable', 'string'],
'slots_total' => ['required', 'integer', 'min:1'],
'slots_open_for_claiming' => ['required', 'integer', 'min:0', 'lte:slots_total'],
'report_time' => ['nullable', 'date_format:H:i'],
'actual_start_time' => ['nullable', 'date_format:H:i'],
'actual_end_time' => ['nullable', 'date_format:H:i'],
'is_lead_role' => ['nullable', 'boolean'],
'allow_overlap' => ['nullable', 'boolean'],
'status' => ['nullable', 'in:draft,open,full,in_progress,completed,cancelled'],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class StoreTimeSlotRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'person_type' => ['required', 'in:CREW,VOLUNTEER,PRESS,PHOTO,PARTNER'],
'date' => ['required', 'date'],
'start_time' => ['required', 'date_format:H:i'],
'end_time' => ['required', 'date_format:H:i'],
'duration_hours' => ['nullable', 'numeric', 'min:0.5', 'max:24'],
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateCompanyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:255'],
'type' => ['sometimes', 'in:supplier,partner,agency,venue,other'],
'contact_name' => ['nullable', 'string', 'max:255'],
'contact_email' => ['nullable', 'email', 'max:255'],
'contact_phone' => ['nullable', 'string', 'max:30'],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateCrowdListRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'crowd_type_id' => ['sometimes', 'ulid', 'exists:crowd_types,id'],
'name' => ['sometimes', 'string', 'max:255'],
'type' => ['sometimes', 'in:internal,external'],
'recipient_company_id' => ['nullable', 'ulid', 'exists:companies,id'],
'auto_approve' => ['sometimes', 'boolean'],
'max_persons' => ['nullable', 'integer', 'min:1'],
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateCrowdTypeRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:100'],
'color' => ['sometimes', 'regex:/^#[0-9A-Fa-f]{6}$/'],
'icon' => ['nullable', 'string', 'max:50'],
'is_active' => ['sometimes', 'boolean'],
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateFestivalSectionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:255'],
'sort_order' => ['sometimes', 'integer', 'min:0'],
'type' => ['sometimes', 'in:standard,cross_event'],
'crew_auto_accepts' => ['sometimes', 'boolean'],
'responder_self_checkin' => ['sometimes', 'boolean'],
'crew_need' => ['nullable', 'integer', 'min:0'],
'crew_invited_to_events' => ['sometimes', 'boolean'],
'added_to_timeline' => ['sometimes', 'boolean'],
'timed_accreditations' => ['sometimes', 'boolean'],
'crew_accreditation_level' => ['nullable', 'string', 'max:50'],
'public_form_accreditation_level' => ['nullable', 'string', 'max:50'],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateLocationRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:255'],
'address' => ['nullable', 'string', 'max:255'],
'lat' => ['nullable', 'numeric', 'between:-90,90'],
'lng' => ['nullable', 'numeric', 'between:-180,180'],
'description' => ['nullable', 'string'],
'access_instructions' => ['nullable', 'string'],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateMemberRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'role' => ['required', 'in:org_admin,org_member,event_manager,staff_coordinator,volunteer_coordinator'],
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdatePersonRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'crowd_type_id' => ['sometimes', 'ulid', 'exists:crowd_types,id'],
'name' => ['sometimes', 'string', 'max:255'],
'email' => ['sometimes', 'email', 'max:255'],
'phone' => ['nullable', 'string', 'max:30'],
'company_id' => ['nullable', 'ulid', 'exists:companies,id'],
'status' => ['sometimes', 'in:invited,applied,pending,approved,rejected,no_show'],
'is_blacklisted' => ['sometimes', 'boolean'],
'admin_notes' => ['nullable', 'string'],
'custom_fields' => ['nullable', 'array'],
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateShiftRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'time_slot_id' => ['sometimes', 'ulid', 'exists:time_slots,id'],
'location_id' => ['nullable', 'ulid', 'exists:locations,id'],
'title' => ['nullable', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'instructions' => ['nullable', 'string'],
'coordinator_notes' => ['nullable', 'string'],
'slots_total' => ['sometimes', 'integer', 'min:1'],
'slots_open_for_claiming' => ['sometimes', 'integer', 'min:0'],
'report_time' => ['nullable', 'date_format:H:i'],
'actual_start_time' => ['nullable', 'date_format:H:i'],
'actual_end_time' => ['nullable', 'date_format:H:i'],
'is_lead_role' => ['nullable', 'boolean'],
'allow_overlap' => ['nullable', 'boolean'],
'status' => ['sometimes', 'in:draft,open,full,in_progress,completed,cancelled'],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateTimeSlotRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:255'],
'person_type' => ['sometimes', 'in:CREW,VOLUNTEER,PRESS,PHOTO,PARTNER'],
'date' => ['sometimes', 'date'],
'start_time' => ['sometimes', 'date_format:H:i'],
'end_time' => ['sometimes', 'date_format:H:i'],
'duration_hours' => ['nullable', 'numeric', 'min:0.5', 'max:24'],
];
}
}