diff --git a/api/tests/Feature/Event/FestivalEventTest.php b/api/tests/Feature/Event/FestivalEventTest.php index c746ee9d..8152facb 100644 --- a/api/tests/Feature/Event/FestivalEventTest.php +++ b/api/tests/Feature/Event/FestivalEventTest.php @@ -477,6 +477,38 @@ class FestivalEventTest extends TestCase ]); } + public function test_update_shift_on_sub_event_with_parent_festival_time_slot(): void + { + $section = FestivalSection::factory()->create([ + 'event_id' => $this->subEvent->id, + ]); + + $subEventTimeSlot = TimeSlot::factory()->create([ + 'event_id' => $this->subEvent->id, + ]); + + $festivalTimeSlot = TimeSlot::factory()->create([ + 'event_id' => $this->festival->id, + ]); + + // Create a shift with the sub-event time slot + $shift = Shift::factory()->create([ + 'festival_section_id' => $section->id, + 'time_slot_id' => $subEventTimeSlot->id, + ]); + + Sanctum::actingAs($this->orgAdmin); + + // Update to use a festival time slot — should succeed + $response = $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->subEvent->id}/sections/{$section->id}/shifts/{$shift->id}", [ + 'time_slot_id' => $festivalTimeSlot->id, + ]); + + $response->assertOk() + ->assertJsonPath('data.time_slot_id', $festivalTimeSlot->id) + ->assertJsonPath('data.time_slot.id', $festivalTimeSlot->id); + } + public function test_create_shift_on_local_section_with_other_event_time_slot_returns_422(): void { $section = FestivalSection::factory()->create([ diff --git a/api/tests/Feature/Shift/ShiftTest.php b/api/tests/Feature/Shift/ShiftTest.php index 66eb4975..23c68f38 100644 --- a/api/tests/Feature/Shift/ShiftTest.php +++ b/api/tests/Feature/Shift/ShiftTest.php @@ -110,6 +110,86 @@ class ShiftTest extends TestCase ->assertJson(['data' => ['title' => 'Barhoofd', 'slots_total' => 1]]); } + public function test_update_shift_with_valid_time_slot_id(): void + { + $shift = Shift::factory()->create([ + 'festival_section_id' => $this->section->id, + 'time_slot_id' => $this->timeSlot->id, + ]); + + $newTimeSlot = TimeSlot::factory()->create(['event_id' => $this->event->id]); + + Sanctum::actingAs($this->orgAdmin); + + $response = $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/sections/{$this->section->id}/shifts/{$shift->id}", [ + 'time_slot_id' => $newTimeSlot->id, + ]); + + $response->assertOk() + ->assertJsonPath('data.time_slot_id', $newTimeSlot->id) + ->assertJsonPath('data.time_slot.id', $newTimeSlot->id) + ->assertJsonPath('data.time_slot.name', $newTimeSlot->name); + } + + public function test_update_shift_with_other_org_time_slot_returns_422(): void + { + $shift = Shift::factory()->create([ + 'festival_section_id' => $this->section->id, + 'time_slot_id' => $this->timeSlot->id, + ]); + + $otherEvent = Event::factory()->create(['organisation_id' => $this->otherOrganisation->id]); + $otherTimeSlot = TimeSlot::factory()->create(['event_id' => $otherEvent->id]); + + Sanctum::actingAs($this->orgAdmin); + + $response = $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/sections/{$this->section->id}/shifts/{$shift->id}", [ + 'time_slot_id' => $otherTimeSlot->id, + ]); + + $response->assertUnprocessable() + ->assertJsonValidationErrors('time_slot_id'); + } + + public function test_update_shift_response_includes_time_slot_object(): void + { + $shift = Shift::factory()->create([ + 'festival_section_id' => $this->section->id, + 'time_slot_id' => $this->timeSlot->id, + 'title' => 'Runner', + ]); + + Sanctum::actingAs($this->orgAdmin); + + $response = $this->putJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/sections/{$this->section->id}/shifts/{$shift->id}", [ + 'title' => 'Stage Manager', + ]); + + $response->assertOk() + ->assertJsonStructure(['data' => [ + 'time_slot_id', + 'time_slot' => ['id', 'name', 'date', 'start_time', 'end_time'], + ]]); + } + + public function test_store_response_includes_time_slot_object(): void + { + Sanctum::actingAs($this->orgAdmin); + + $response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/sections/{$this->section->id}/shifts", [ + 'time_slot_id' => $this->timeSlot->id, + 'title' => 'Tapper', + 'slots_total' => 4, + 'slots_open_for_claiming' => 3, + ]); + + $response->assertCreated() + ->assertJsonStructure(['data' => [ + 'time_slot_id', + 'time_slot' => ['id', 'name', 'date', 'start_time', 'end_time'], + ]]); + } + public function test_destroy_soft_deletes_shift(): void { $shift = Shift::factory()->create([ diff --git a/apps/app/src/components/sections/CreateShiftDialog.vue b/apps/app/src/components/sections/CreateShiftDialog.vue index 65efcf9c..b7890e2a 100644 --- a/apps/app/src/components/sections/CreateShiftDialog.vue +++ b/apps/app/src/components/sections/CreateShiftDialog.vue @@ -28,7 +28,7 @@ const sectionIdRef = computed(() => props.sectionId) const isEditing = computed(() => !!props.shift) const isSubEventRef = computed(() => props.isSubEvent) -const { data: timeSlots } = useTimeSlotList(orgIdRef, eventIdRef, { includeParent: isSubEventRef }) +const { data: timeSlots, isLoading: timeSlotsLoading } = useTimeSlotList(orgIdRef, eventIdRef, { includeParent: isSubEventRef }) const { mutate: createShift, isPending: isCreating } = useCreateShift(orgIdRef, eventIdRef, sectionIdRef) const { mutate: updateShift, isPending: isUpdating } = useUpdateShift(orgIdRef, eventIdRef, sectionIdRef) @@ -74,15 +74,25 @@ watch( { immediate: true }, ) +function formatTimeSlotItem(ts: { id: string; name: string; date: string; start_time: string; end_time: string }) { + return { + title: `${ts.name} — ${ts.date} ${ts.start_time}–${ts.end_time}`, + value: ts.id, + } +} + const timeSlotItems = computed(() => { - if (!timeSlots.value?.length) return [] + // While loading, show the current shift's time slot so the dropdown doesn't flash a raw ULID + if (!timeSlots.value?.length) { + if (props.shift?.time_slot) { + return [formatTimeSlotItem(props.shift.time_slot)] + } + return [] + } const hasFestival = timeSlots.value.some(ts => ts.source === 'festival') if (!hasFestival) { - return timeSlots.value.map(ts => ({ - title: `${ts.name} — ${ts.date} ${ts.start_time}–${ts.end_time}`, - value: ts.id, - })) + return timeSlots.value.map(formatTimeSlotItem) } const subEventSlots = timeSlots.value.filter(ts => ts.source !== 'festival') @@ -91,18 +101,12 @@ const timeSlotItems = computed(() => { if (subEventSlots.length) { items.push({ title: subEventSlots[0]?.event_name ?? 'Programma', type: 'subheader' }) - items.push(...subEventSlots.map(ts => ({ - title: `${ts.name} — ${ts.date} ${ts.start_time}–${ts.end_time}`, - value: ts.id, - }))) + items.push(...subEventSlots.map(formatTimeSlotItem)) } if (festivalSlots.length) { items.push({ title: festivalSlots[0]?.event_name ?? 'Festival', type: 'subheader' }) - items.push(...festivalSlots.map(ts => ({ - title: `${ts.name} — ${ts.date} ${ts.start_time}–${ts.end_time}`, - value: ts.id, - }))) + items.push(...festivalSlots.map(formatTimeSlotItem)) } return items @@ -192,10 +196,11 @@ function onSubmit() {