diff --git a/app/Http/Controllers/VolunteerController.php b/app/Http/Controllers/VolunteerController.php index 9b49b49..23083b4 100644 --- a/app/Http/Controllers/VolunteerController.php +++ b/app/Http/Controllers/VolunteerController.php @@ -93,8 +93,7 @@ public function checkOut(Request $request, TimeEntry $timeEntry): JsonResponse|R */ public function storeTimeEntry(TimeEntryStoreRequest $request, Event $event, User $user): JsonResponse { // Don't allow an ongoing entry to be created if there already is one - $input = $request->safe(); - if (!isset($input['stop'])) { + if (!$request->filled('stop')) { if ($user->timeEntries()->ongoing()->forEvent($event)->exists()) { return response()->json(['error' => 'User already has an ongoing time entry.'], 409); } diff --git a/app/Http/Requests/TimeEntryStoreRequest.php b/app/Http/Requests/TimeEntryStoreRequest.php index 83b759d..ad055c4 100644 --- a/app/Http/Requests/TimeEntryStoreRequest.php +++ b/app/Http/Requests/TimeEntryStoreRequest.php @@ -43,8 +43,13 @@ public function rules(): array { */ public function passedValidation(): void { $this->merge([ - 'start' => $this->date('start')?->timezone(config('app.timezone')) ?? now(), - 'stop' => $this->date('stop')?->timezone(config('app.timezone')), + 'start' => ($this->date('start')?->timezone(config('app.timezone')) ?? now())->toISOString(true), + 'stop' => $this->date('stop')?->timezone(config('app.timezone'))?->toISOString(true), ]); + + $this->validator->setData(array_merge($this->validator->getData(), [ + 'start' => $this->input('start'), + 'stop' => $this->input('stop'), + ])); } } diff --git a/tests/Feature/VolunteerTimeEntryStoreTest.php b/tests/Feature/VolunteerTimeEntryStoreTest.php new file mode 100644 index 0000000..bc52b11 --- /dev/null +++ b/tests/Feature/VolunteerTimeEntryStoreTest.php @@ -0,0 +1,59 @@ +create(['role' => Role::Admin]); + $volunteer = User::factory()->create(); + $event = Event::factory()->create(); + $department = Department::factory()->create(['event_id' => $event->id]); + + $this->actingAs($manager); + + // Mirrors the real clock-in UI, which always sends an explicit 'start' and omits 'stop'. + $this->putJson(route('volunteer.time.store', [$event, $volunteer]), [ + 'department_id' => $department->id, + 'start' => now()->subSecond()->toISOString(), + ])->assertOk(); + + expect(TimeEntry::where('user_id', $volunteer->id)->whereNull('stop')->count())->toBe(1); + + $this->putJson(route('volunteer.time.store', [$event, $volunteer]), [ + 'department_id' => $department->id, + 'start' => now()->subSecond()->toISOString(), + ]) + ->assertStatus(409) + ->assertJson(['error' => 'User already has an ongoing time entry.']); + + expect(TimeEntry::where('user_id', $volunteer->id)->whereNull('stop')->count())->toBe(1); +}); + +it('allows creating a time entry with an explicit stop while another is ongoing', function () { + $manager = User::factory()->create(['role' => Role::Admin]); + $volunteer = User::factory()->create(); + $event = Event::factory()->create(); + $department = Department::factory()->create(['event_id' => $event->id]); + + $this->actingAs($manager); + + $this->putJson(route('volunteer.time.store', [$event, $volunteer]), [ + 'department_id' => $department->id, + 'start' => now()->subSecond()->toISOString(), + ])->assertOk(); + + $this->putJson(route('volunteer.time.store', [$event, $volunteer]), [ + 'department_id' => $department->id, + 'start' => now()->subHour()->toISOString(), + 'stop' => now()->toISOString(), + ])->assertOk(); + + expect(TimeEntry::where('user_id', $volunteer->id)->count())->toBe(2); + expect(TimeEntry::where('user_id', $volunteer->id)->whereNull('stop')->count())->toBe(1); +}); \ No newline at end of file