fix: shift edit time slot dropdown loading state and test coverage

The time slot dropdown in the shift edit dialog could flash the
"create a time slot first" alert during loading, and show raw ULIDs
when time slot data hadn't loaded yet. Fixed by:
- Adding loading state indicator to the time slot dropdown
- Using the shift's existing time_slot object as a fallback item
  while the full list is fetching
- Showing the dropdown (with loading spinner) instead of the
  misleading "no time slots" alert during fetch

Added test coverage for time_slot_id validation on shift updates:
- Update with valid same-event time slot (200)
- Update with cross-org time slot (422)
- Update on sub-event with parent festival time slot (200)
- Store/update responses include nested time_slot object

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 20:46:36 +02:00
parent ea159a34fe
commit cf02500453
3 changed files with 132 additions and 15 deletions

View File

@@ -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([

View File

@@ -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([