diff --git a/api/app/Http/Controllers/Api/V1/PortalMeController.php b/api/app/Http/Controllers/Api/V1/PortalMeController.php index 305e871a..7c9908e1 100644 --- a/api/app/Http/Controllers/Api/V1/PortalMeController.php +++ b/api/app/Http/Controllers/Api/V1/PortalMeController.php @@ -4,11 +4,13 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; +use App\Enums\ShiftAssignmentStatus; use App\Http\Controllers\Controller; use App\Http\Requests\Api\V1\PortalMeRequest; use App\Http\Resources\Api\V1\PersonResource; use App\Models\Event; use App\Models\Person; +use App\Models\TimeSlot; use Illuminate\Http\JsonResponse; final class PortalMeController extends Controller @@ -37,6 +39,42 @@ final class PortalMeController extends Controller return $this->notFound('No registration found for this event'); } - return $this->success(new PersonResource($person)); + $upcomingShift = $person->shiftAssignments() + ->where('status', ShiftAssignmentStatus::APPROVED) + ->whereHas('shift.timeSlot', fn ($q) => $q->where('date', '>=', now()->toDateString())) + ->with([ + 'shift:id,title,festival_section_id,time_slot_id,location_id', + 'shift.timeSlot:id,name,date,start_time,end_time', + 'shift.festivalSection:id,name', + 'shift.location:id,name', + ]) + ->orderBy( + TimeSlot::select('date') + ->whereColumn('time_slots.id', 'shift_assignments.time_slot_id') + ->limit(1) + ) + ->orderBy( + TimeSlot::select('start_time') + ->whereColumn('time_slots.id', 'shift_assignments.time_slot_id') + ->limit(1) + ) + ->first(); + + $formattedShift = null; + if ($upcomingShift) { + $timeSlot = $upcomingShift->shift->timeSlot; + $formattedShift = [ + 'date' => $timeSlot->date->toDateString(), + 'time' => substr($timeSlot->start_time, 0, 5) . ' - ' . substr($timeSlot->end_time, 0, 5), + 'title' => $upcomingShift->shift->title, + 'section' => $upcomingShift->shift->festivalSection?->name, + 'location' => $upcomingShift->shift->location?->name, + ]; + } + + $data = (new PersonResource($person))->resolve(); + $data['upcoming_shift'] = $formattedShift; + + return $this->success($data); } } diff --git a/api/tests/Feature/Api/V1/PortalMeUpcomingShiftTest.php b/api/tests/Feature/Api/V1/PortalMeUpcomingShiftTest.php new file mode 100644 index 00000000..2734499d --- /dev/null +++ b/api/tests/Feature/Api/V1/PortalMeUpcomingShiftTest.php @@ -0,0 +1,269 @@ +seed(RoleSeeder::class); + + $this->organisation = Organisation::factory()->create(); + $this->volunteerCrowdType = CrowdType::factory()->systemType('VOLUNTEER')->create([ + 'organisation_id' => $this->organisation->id, + ]); + $this->event = Event::factory()->create([ + 'organisation_id' => $this->organisation->id, + 'status' => 'published', + ]); + $this->user = User::factory()->create(); + $this->person = Person::factory()->approved()->create([ + 'event_id' => $this->event->id, + 'crowd_type_id' => $this->volunteerCrowdType->id, + 'user_id' => $this->user->id, + 'email' => $this->user->email, + ]); + } + + public function test_portal_me_returns_upcoming_shift_for_approved_person(): void + { + $futureDate = now()->addDays(7)->toDateString(); + + $timeSlot = TimeSlot::factory()->create([ + 'event_id' => $this->event->id, + 'date' => $futureDate, + 'start_time' => '18:00:00', + 'end_time' => '02:00:00', + ]); + + $section = FestivalSection::factory()->create([ + 'event_id' => $this->event->id, + 'name' => 'Bar Hoofdpodium', + ]); + + $location = Location::factory()->create([ + 'event_id' => $this->event->id, + 'name' => 'Festivalterrein', + ]); + + $shift = Shift::factory()->create([ + 'festival_section_id' => $section->id, + 'time_slot_id' => $timeSlot->id, + 'location_id' => $location->id, + 'title' => 'Tapper', + ]); + + ShiftAssignment::factory()->approved()->create([ + 'shift_id' => $shift->id, + 'person_id' => $this->person->id, + 'time_slot_id' => $timeSlot->id, + ]); + + Sanctum::actingAs($this->user); + + $response = $this->getJson("/api/v1/portal/me?event_id={$this->event->id}"); + + $response->assertOk(); + $response->assertJsonPath('data.upcoming_shift.date', $futureDate); + $response->assertJsonPath('data.upcoming_shift.time', '18:00 - 02:00'); + $response->assertJsonPath('data.upcoming_shift.title', 'Tapper'); + $response->assertJsonPath('data.upcoming_shift.section', 'Bar Hoofdpodium'); + $response->assertJsonPath('data.upcoming_shift.location', 'Festivalterrein'); + } + + public function test_portal_me_returns_null_upcoming_shift_when_no_future_shifts(): void + { + // Create a past shift assignment + $pastDate = now()->subDays(7)->toDateString(); + + $timeSlot = TimeSlot::factory()->create([ + 'event_id' => $this->event->id, + 'date' => $pastDate, + 'start_time' => '18:00:00', + 'end_time' => '02:00:00', + ]); + + $section = FestivalSection::factory()->create([ + 'event_id' => $this->event->id, + ]); + + $shift = Shift::factory()->create([ + 'festival_section_id' => $section->id, + 'time_slot_id' => $timeSlot->id, + 'title' => 'Tapper', + ]); + + ShiftAssignment::factory()->approved()->create([ + 'shift_id' => $shift->id, + 'person_id' => $this->person->id, + 'time_slot_id' => $timeSlot->id, + ]); + + Sanctum::actingAs($this->user); + + $response = $this->getJson("/api/v1/portal/me?event_id={$this->event->id}"); + + $response->assertOk(); + $response->assertJsonPath('data.upcoming_shift', null); + } + + public function test_portal_me_returns_null_upcoming_shift_for_pending_person(): void + { + $pendingPerson = Person::factory()->create([ + 'event_id' => $this->event->id, + 'crowd_type_id' => $this->volunteerCrowdType->id, + 'user_id' => null, + 'status' => 'pending', + ]); + + $pendingUser = User::factory()->create(['email' => $pendingPerson->email]); + $pendingPerson->update(['user_id' => $pendingUser->id]); + + $futureDate = now()->addDays(7)->toDateString(); + + $timeSlot = TimeSlot::factory()->create([ + 'event_id' => $this->event->id, + 'date' => $futureDate, + ]); + + $section = FestivalSection::factory()->create([ + 'event_id' => $this->event->id, + ]); + + $shift = Shift::factory()->create([ + 'festival_section_id' => $section->id, + 'time_slot_id' => $timeSlot->id, + ]); + + // Assignment is pending_approval, not approved + ShiftAssignment::factory()->create([ + 'shift_id' => $shift->id, + 'person_id' => $pendingPerson->id, + 'time_slot_id' => $timeSlot->id, + 'status' => ShiftAssignmentStatus::PENDING_APPROVAL, + ]); + + Sanctum::actingAs($pendingUser); + + $response = $this->getJson("/api/v1/portal/me?event_id={$this->event->id}"); + + $response->assertOk(); + $response->assertJsonPath('data.upcoming_shift', null); + } + + public function test_portal_me_returns_nearest_future_shift(): void + { + $nearDate = now()->addDays(3)->toDateString(); + $farDate = now()->addDays(10)->toDateString(); + + $nearTimeSlot = TimeSlot::factory()->create([ + 'event_id' => $this->event->id, + 'date' => $nearDate, + 'start_time' => '10:00:00', + 'end_time' => '18:00:00', + ]); + + $farTimeSlot = TimeSlot::factory()->create([ + 'event_id' => $this->event->id, + 'date' => $farDate, + 'start_time' => '18:00:00', + 'end_time' => '02:00:00', + ]); + + $section = FestivalSection::factory()->create([ + 'event_id' => $this->event->id, + ]); + + $nearShift = Shift::factory()->create([ + 'festival_section_id' => $section->id, + 'time_slot_id' => $nearTimeSlot->id, + 'title' => 'Ochtend Runner', + ]); + + $farShift = Shift::factory()->create([ + 'festival_section_id' => $section->id, + 'time_slot_id' => $farTimeSlot->id, + 'title' => 'Avond Tapper', + ]); + + ShiftAssignment::factory()->approved()->create([ + 'shift_id' => $farShift->id, + 'person_id' => $this->person->id, + 'time_slot_id' => $farTimeSlot->id, + ]); + + ShiftAssignment::factory()->approved()->create([ + 'shift_id' => $nearShift->id, + 'person_id' => $this->person->id, + 'time_slot_id' => $nearTimeSlot->id, + ]); + + Sanctum::actingAs($this->user); + + $response = $this->getJson("/api/v1/portal/me?event_id={$this->event->id}"); + + $response->assertOk(); + $response->assertJsonPath('data.upcoming_shift.title', 'Ochtend Runner'); + $response->assertJsonPath('data.upcoming_shift.date', $nearDate); + } + + public function test_portal_me_ignores_cancelled_shift_assignments(): void + { + $futureDate = now()->addDays(7)->toDateString(); + + $timeSlot = TimeSlot::factory()->create([ + 'event_id' => $this->event->id, + 'date' => $futureDate, + ]); + + $section = FestivalSection::factory()->create([ + 'event_id' => $this->event->id, + ]); + + $shift = Shift::factory()->create([ + 'festival_section_id' => $section->id, + 'time_slot_id' => $timeSlot->id, + ]); + + ShiftAssignment::factory()->create([ + 'shift_id' => $shift->id, + 'person_id' => $this->person->id, + 'time_slot_id' => $timeSlot->id, + 'status' => ShiftAssignmentStatus::CANCELLED, + ]); + + Sanctum::actingAs($this->user); + + $response = $this->getJson("/api/v1/portal/me?event_id={$this->event->id}"); + + $response->assertOk(); + $response->assertJsonPath('data.upcoming_shift', null); + } +}