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,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class CompanyResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'organisation_id' => $this->organisation_id,
'name' => $this->name,
'type' => $this->type,
'contact_name' => $this->contact_name,
'contact_email' => $this->contact_email,
'contact_phone' => $this->contact_phone,
'created_at' => $this->created_at->toIso8601String(),
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class CrowdListResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'event_id' => $this->event_id,
'crowd_type_id' => $this->crowd_type_id,
'name' => $this->name,
'type' => $this->type,
'recipient_company_id' => $this->recipient_company_id,
'auto_approve' => $this->auto_approve,
'max_persons' => $this->max_persons,
'created_at' => $this->created_at->toIso8601String(),
'persons_count' => $this->whenCounted('persons'),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class CrowdTypeResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'organisation_id' => $this->organisation_id,
'name' => $this->name,
'system_type' => $this->system_type,
'color' => $this->color,
'icon' => $this->icon,
'is_active' => $this->is_active,
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class FestivalSectionResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'event_id' => $this->event_id,
'name' => $this->name,
'type' => $this->type,
'sort_order' => $this->sort_order,
'crew_need' => $this->crew_need,
'crew_auto_accepts' => $this->crew_auto_accepts,
'responder_self_checkin' => $this->responder_self_checkin,
'added_to_timeline' => $this->added_to_timeline,
'crew_accreditation_level' => $this->crew_accreditation_level,
'created_at' => $this->created_at->toIso8601String(),
'shifts_count' => $this->whenCounted('shifts'),
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class InvitationResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'email' => $this->email,
'role' => $this->role,
'status' => $this->isExpired() && $this->isPending() ? 'expired' : $this->status,
'expires_at' => $this->expires_at->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
'organisation' => $this->whenLoaded('organisation', fn () => [
'name' => $this->organisation->name,
]),
'invited_by' => $this->whenLoaded('invitedBy', fn () => [
'name' => $this->invitedBy?->name,
]),
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class LocationResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'event_id' => $this->event_id,
'name' => $this->name,
'address' => $this->address,
'lat' => $this->lat,
'lng' => $this->lng,
'description' => $this->description,
'access_instructions' => $this->access_instructions,
'created_at' => $this->created_at->toIso8601String(),
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use App\Models\UserInvitation;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
final class MemberCollection extends ResourceCollection
{
public $collects = MemberResource::class;
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
];
}
/** @return array<string, mixed> */
public function with(Request $request): array
{
$organisation = $request->route('organisation');
return [
'meta' => [
'total_members' => $this->collection->count(),
'pending_invitations_count' => UserInvitation::where('organisation_id', $organisation->id)
->pending()
->where('expires_at', '>', now())
->count(),
],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class MemberResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->pivot?->role,
'avatar' => $this->avatar,
'event_roles' => $this->whenLoaded('events', fn () =>
$this->events->map(fn ($event) => [
'event_id' => $event->id,
'event_name' => $event->name,
'role' => $event->pivot->role,
])
),
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
final class PersonCollection extends ResourceCollection
{
public $collects = PersonResource::class;
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
];
}
public function with(Request $request): array
{
$persons = $this->collection;
$byCrowdType = $persons->groupBy(fn ($person) => $person->crowd_type_id);
$crowdTypeMeta = [];
foreach ($byCrowdType as $crowdTypeId => $group) {
$crowdTypeMeta[$crowdTypeId] = [
'approved_count' => $group->where('status', 'approved')->count(),
'pending_count' => $group->where('status', 'pending')->count(),
];
}
return [
'meta' => [
'total' => $persons->count(),
'approved_count' => $persons->where('status', 'approved')->count(),
'pending_count' => $persons->where('status', 'pending')->count(),
'by_crowd_type' => $crowdTypeMeta,
],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class PersonResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'event_id' => $this->event_id,
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
'status' => $this->status,
'is_blacklisted' => $this->is_blacklisted,
'admin_notes' => $this->admin_notes,
'custom_fields' => $this->custom_fields,
'created_at' => $this->created_at->toIso8601String(),
'crowd_type' => new CrowdTypeResource($this->whenLoaded('crowdType')),
'company' => new CompanyResource($this->whenLoaded('company')),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class ShiftAssignmentResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'shift_id' => $this->shift_id,
'person_id' => $this->person_id,
'status' => $this->status,
'auto_approved' => $this->auto_approved,
'assigned_at' => $this->assigned_at?->toIso8601String(),
'approved_at' => $this->approved_at?->toIso8601String(),
];
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class ShiftResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'festival_section_id' => $this->festival_section_id,
'time_slot_id' => $this->time_slot_id,
'location_id' => $this->location_id,
'title' => $this->title,
'description' => $this->description,
'instructions' => $this->instructions,
'coordinator_notes' => $this->when(
$this->shouldShowCoordinatorNotes($request),
$this->coordinator_notes,
),
'slots_total' => $this->slots_total,
'slots_open_for_claiming' => $this->slots_open_for_claiming,
'is_lead_role' => $this->is_lead_role,
'report_time' => $this->report_time,
'actual_start_time' => $this->actual_start_time,
'actual_end_time' => $this->actual_end_time,
'allow_overlap' => $this->allow_overlap,
'status' => $this->status,
'filled_slots' => $this->filled_slots,
'fill_rate' => $this->fill_rate,
'effective_start_time' => $this->effective_start_time,
'effective_end_time' => $this->effective_end_time,
'created_at' => $this->created_at->toIso8601String(),
'time_slot' => new TimeSlotResource($this->whenLoaded('timeSlot')),
'location' => new LocationResource($this->whenLoaded('location')),
'festival_section' => new FestivalSectionResource($this->whenLoaded('festivalSection')),
];
}
private function shouldShowCoordinatorNotes(Request $request): bool
{
$user = $request->user();
if (! $user) {
return false;
}
if ($user->hasRole('super_admin')) {
return true;
}
$shift = $this->resource;
$event = $shift->festivalSection?->event;
if (! $event) {
return false;
}
return $event->organisation->users()
->where('user_id', $user->id)
->wherePivot('role', 'org_admin')
->exists()
|| $event->users()
->where('user_id', $user->id)
->wherePivot('role', 'event_manager')
->exists();
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
final class TimeSlotResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'event_id' => $this->event_id,
'name' => $this->name,
'person_type' => $this->person_type,
'date' => $this->date->toDateString(),
'start_time' => $this->start_time,
'end_time' => $this->end_time,
'duration_hours' => $this->duration_hours,
'created_at' => $this->created_at->toIso8601String(),
];
}
}