diff --git a/app/Http/Controllers/VolunteerController.php b/app/Http/Controllers/VolunteerController.php index 23083b49..49f31bb5 100644 --- a/app/Http/Controllers/VolunteerController.php +++ b/app/Http/Controllers/VolunteerController.php @@ -91,7 +91,7 @@ public function checkOut(Request $request, TimeEntry $timeEntry): JsonResponse|R /** * Create a time entry for a user */ - public function storeTimeEntry(TimeEntryStoreRequest $request, Event $event, User $user): JsonResponse { + public function storeTimeEntry(TimeEntryStoreRequest $request, Event $event, User $user): JsonResponse|RedirectResponse { // Don't allow an ongoing entry to be created if there already is one if (!$request->filled('stop')) { if ($user->timeEntries()->ongoing()->forEvent($event)->exists()) { @@ -114,7 +114,7 @@ public function storeTimeEntry(TimeEntryStoreRequest $request, Event $event, Use /** * Delete a time entry */ - public function destroyTimeEntry(Request $request, TimeEntry $timeEntry): JsonResponse { + public function destroyTimeEntry(Request $request, TimeEntry $timeEntry): JsonResponse|RedirectResponse { $this->authorize('delete', $timeEntry); $timeEntry->delete(); diff --git a/app/Models/TimeEntry.php b/app/Models/TimeEntry.php index 21f3bbc0..cbd66f3a 100644 --- a/app/Models/TimeEntry.php +++ b/app/Models/TimeEntry.php @@ -239,7 +239,8 @@ public function calculateBonusTime(?Event $event = null, ?Collection $bonuses = // Total up all applicable bonus time $bonusTotal = 0; foreach ($bonuses as $bonus) { - if ($bonus->pivot->department_id !== $this->department_id) continue; + // Bonuses loaded through a department have a pivot to check, while queried ones are already filtered + if ($bonus->pivot && $bonus->pivot->department_id !== $this->department_id) continue; $bonusTime = static::calculateOverlap($this->start, $this->stop ?? now(), $bonus->start, $bonus->stop); $bonusTotal += round($bonusTime * ($bonus->modifier - 1)); } diff --git a/tests/Feature/TimeEntryBonusTest.php b/tests/Feature/TimeEntryBonusTest.php new file mode 100644 index 00000000..f73ced8f --- /dev/null +++ b/tests/Feature/TimeEntryBonusTest.php @@ -0,0 +1,45 @@ +event = Event::factory()->create(); + $this->department = Department::factory()->create(['event_id' => $this->event->id]); + $this->start = Carbon::parse('2026-07-01 17:00:00'); + $this->entry = TimeEntry::factory()->create([ + 'event_id' => $this->event->id, + 'department_id' => $this->department->id, + 'start' => $this->start, + 'stop' => $this->start->avoidMutation()->addHours(4), + ]); +}); + +it('calculates bonus time without preloaded bonuses', function () { + // Overlaps the entry by 2 hours at double time + $bonus = TimeBonus::factory()->create([ + 'event_id' => $this->event->id, + 'start' => $this->start->avoidMutation()->addHours(2), + 'stop' => $this->start->avoidMutation()->addHours(6), + 'modifier' => 2, + ]); + $bonus->departments()->attach($this->department); + + // A bonus for another department doesn't apply + $otherBonus = TimeBonus::factory()->create([ + 'event_id' => $this->event->id, + 'start' => $this->start, + 'stop' => $this->start->avoidMutation()->addHours(4), + 'modifier' => 3, + ]); + $otherBonus->departments()->attach(Department::factory()->create(['event_id' => $this->event->id])); + + expect($this->entry->calculateBonusTime($this->event))->toEqual(7200) + ->and($this->entry->calculateTotalTime($this->event))->toEqual(14400 + 7200); +}); diff --git a/tests/Feature/TimeEntryFormRedirectTest.php b/tests/Feature/TimeEntryFormRedirectTest.php new file mode 100644 index 00000000..1ea802c7 --- /dev/null +++ b/tests/Feature/TimeEntryFormRedirectTest.php @@ -0,0 +1,52 @@ +event = Event::factory()->create(); + $this->department = Department::factory()->create(['event_id' => $this->event->id]); + $this->start = Carbon::parse('2026-07-01 17:00:00'); + $this->entry = TimeEntry::factory()->create([ + 'event_id' => $this->event->id, + 'department_id' => $this->department->id, + 'start' => $this->start, + 'stop' => $this->start->avoidMutation()->addHours(4), + ]); +}); + +it('redirects back after creating a time entry from a form', function () { + $admin = User::factory()->create(['role' => Role::Admin]); + $volunteer = User::factory()->create(); + + $this->actingAs($admin) + ->from('/manage') + ->put(route('volunteer.time.store', [$this->event, $volunteer]), [ + 'department_id' => $this->department->id, + 'start' => now()->subHours(2)->toISOString(), + 'stop' => now()->subHour()->toISOString(), + ]) + ->assertRedirect('/manage') + ->assertSessionHas('success', 'Created time entry.'); + + expect($volunteer->timeEntries()->count())->toBe(1); +}); + +it('redirects back after deleting a time entry from a form', function () { + $admin = User::factory()->create(['role' => Role::Admin]); + + $this->actingAs($admin) + ->from('/manage') + ->delete(route('volunteer.time.destroy', $this->entry)) + ->assertRedirect('/manage') + ->assertSessionHas('success', 'Deleted time entry.'); + + expect(TimeEntry::count())->toBe(0); +});