From 531443f0dfd7af58c80accd90c53b22207286db1 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Wed, 4 Feb 2026 14:32:56 +0100 Subject: [PATCH] fix admin panel time entry save and update, fixes #997 --- app/Filament/Resources/TimeEntryResource.php | 18 ++- .../Pages/CreateTimeEntry.php | 19 +++ .../TimeEntryResource/Pages/EditTimeEntry.php | 19 +++ .../Resources/TimeEntryResourceTest.php | 147 ++++++++++++++++++ 4 files changed, 199 insertions(+), 4 deletions(-) diff --git a/app/Filament/Resources/TimeEntryResource.php b/app/Filament/Resources/TimeEntryResource.php index ffd133b3..c54a4678 100644 --- a/app/Filament/Resources/TimeEntryResource.php +++ b/app/Filament/Resources/TimeEntryResource.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Filament\Resources; use App\Filament\Resources\TimeEntryResource\Pages; +use App\Models\Member; use App\Models\TimeEntry; use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\Select; @@ -16,6 +17,7 @@ use Filament\Tables; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; class TimeEntryResource extends Resource { @@ -51,15 +53,23 @@ class TimeEntryResource extends Resource ->rules([ 'after_or_equal:start', ]), - Select::make('user_id') - ->relationship(name: 'user', titleAttribute: 'email') - ->searchable(['name', 'email']) + Select::make('member_id') + ->relationship( + name: 'member', + titleAttribute: 'id', + modifyQueryUsing: fn (Builder $query) => $query->with(['user', 'organization']) + ) + ->getOptionLabelFromRecordUsing(fn (Member $record): string => $record->user->email.' ('.$record->organization->name.')') + ->searchable() ->required(), Select::make('project_id') ->relationship(name: 'project', titleAttribute: 'name') ->searchable(['name']) ->nullable(), - // TODO + Select::make('task_id') + ->relationship(name: 'task', titleAttribute: 'name') + ->searchable(['name']) + ->nullable(), ]); } diff --git a/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php b/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php index 35a85daf..a8bb9498 100644 --- a/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php +++ b/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php @@ -5,9 +5,28 @@ declare(strict_types=1); namespace App\Filament\Resources\TimeEntryResource\Pages; use App\Filament\Resources\TimeEntryResource; +use App\Models\Member; use Filament\Resources\Pages\CreateRecord; class CreateTimeEntry extends CreateRecord { protected static string $resource = TimeEntryResource::class; + + /** + * @param array $data + * @return array + */ + protected function mutateFormDataBeforeCreate(array $data): array + { + if (isset($data['member_id'])) { + /** @var Member|null $member */ + $member = Member::query()->find($data['member_id']); + if ($member !== null) { + $data['user_id'] = $member->user_id; + $data['organization_id'] = $member->organization_id; + } + } + + return $data; + } } diff --git a/app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php b/app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php index 96105ecd..f8ef533f 100644 --- a/app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php +++ b/app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Filament\Resources\TimeEntryResource\Pages; use App\Filament\Resources\TimeEntryResource; +use App\Models\Member; use Filament\Actions; use Filament\Resources\Pages\EditRecord; @@ -19,4 +20,22 @@ class EditTimeEntry extends EditRecord ->icon('heroicon-m-trash'), ]; } + + /** + * @param array $data + * @return array + */ + protected function mutateFormDataBeforeSave(array $data): array + { + if (isset($data['member_id'])) { + /** @var Member|null $member */ + $member = Member::query()->find($data['member_id']); + if ($member !== null) { + $data['user_id'] = $member->user_id; + $data['organization_id'] = $member->organization_id; + } + } + + return $data; + } } diff --git a/tests/Unit/Filament/Resources/TimeEntryResourceTest.php b/tests/Unit/Filament/Resources/TimeEntryResourceTest.php index 86d2f9e1..d4cdec04 100644 --- a/tests/Unit/Filament/Resources/TimeEntryResourceTest.php +++ b/tests/Unit/Filament/Resources/TimeEntryResourceTest.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Tests\Unit\Filament\Resources; use App\Filament\Resources\TimeEntryResource; +use App\Models\Member; +use App\Models\Organization; use App\Models\TimeEntry; use App\Models\User; use Illuminate\Support\Facades\Config; @@ -50,4 +52,149 @@ class TimeEntryResourceTest extends FilamentTestCase // Assert $response->assertSuccessful(); } + + public function test_can_see_create_page_of_time_entry(): void + { + // Act + $response = Livewire::test(TimeEntryResource\Pages\CreateTimeEntry::class); + + // Assert + $response->assertSuccessful(); + } + + public function test_can_create_time_entry(): void + { + // Arrange + $organization = Organization::factory()->create(); + $user = User::factory()->create(); + $member = Member::factory() + ->forOrganization($organization) + ->forUser($user) + ->create(); + + // Act + $response = Livewire::test(TimeEntryResource\Pages\CreateTimeEntry::class) + ->fillForm([ + 'description' => 'Test time entry', + 'billable' => true, + 'start' => '2024-01-01 08:00:00', + 'end' => '2024-01-01 10:00:00', + 'member_id' => $member->getKey(), + ]) + ->call('create') + ->assertHasNoFormErrors(); + + // Assert + $response->assertSuccessful(); + $timeEntry = TimeEntry::where('description', 'Test time entry')->first(); + $this->assertNotNull($timeEntry); + $this->assertSame($member->getKey(), $timeEntry->member_id); + $this->assertSame($user->getKey(), $timeEntry->user_id); + $this->assertSame($organization->getKey(), $timeEntry->organization_id); + $this->assertTrue($timeEntry->billable); + } + + public function test_can_create_time_entry_and_derives_user_and_organization_from_member(): void + { + // Arrange + $organization = Organization::factory()->create(); + $user = User::factory()->create(); + $member = Member::factory() + ->forOrganization($organization) + ->forUser($user) + ->create(); + $otherUser = User::factory()->create(); + $otherOrganization = Organization::factory()->create(); + + // Act + $response = Livewire::test(TimeEntryResource\Pages\CreateTimeEntry::class) + ->fillForm([ + 'description' => 'Derived fields test', + 'billable' => false, + 'start' => '2024-03-01 09:00:00', + 'end' => '2024-03-01 11:00:00', + 'member_id' => $member->getKey(), + 'user_id' => $otherUser->getKey(), + 'organization_id' => $otherOrganization->getKey(), + ]) + ->call('create') + ->assertHasNoFormErrors(); + + // Assert + $response->assertSuccessful(); + $timeEntry = TimeEntry::where('description', 'Derived fields test')->first(); + $this->assertNotNull($timeEntry); + $this->assertSame($user->getKey(), $timeEntry->user_id); + $this->assertSame($organization->getKey(), $timeEntry->organization_id); + } + + public function test_can_update_time_entry(): void + { + // Arrange + $organization = Organization::factory()->create(); + $user = User::factory()->create(); + $member = Member::factory() + ->forOrganization($organization) + ->forUser($user) + ->create(); + $timeEntry = TimeEntry::factory()->forMember($member)->create(); + + // Act + $response = Livewire::test(TimeEntryResource\Pages\EditTimeEntry::class, ['record' => $timeEntry->getKey()]) + ->fillForm([ + 'description' => 'Updated description', + 'billable' => true, + 'start' => '2024-02-01 08:00:00', + 'end' => '2024-02-01 12:00:00', + 'member_id' => $member->getKey(), + ]) + ->call('save') + ->assertHasNoFormErrors(); + + // Assert + $response->assertSuccessful(); + $timeEntry->refresh(); + $this->assertSame('Updated description', $timeEntry->description); + $this->assertTrue($timeEntry->billable); + $this->assertSame($user->getKey(), $timeEntry->user_id); + $this->assertSame($organization->getKey(), $timeEntry->organization_id); + } + + public function test_update_time_entry_derives_user_and_organization_from_new_member(): void + { + // Arrange + $organization = Organization::factory()->create(); + $user = User::factory()->create(); + $member = Member::factory() + ->forOrganization($organization) + ->forUser($user) + ->create(); + $timeEntry = TimeEntry::factory()->create(); + + $newOrganization = Organization::factory()->create(); + $newUser = User::factory()->create(); + $newMember = Member::factory() + ->forOrganization($newOrganization) + ->forUser($newUser) + ->create(); + + // Act + $response = Livewire::test(TimeEntryResource\Pages\EditTimeEntry::class, ['record' => $timeEntry->getKey()]) + ->fillForm([ + 'description' => 'Reassigned entry', + 'billable' => false, + 'start' => '2024-02-01 08:00:00', + 'end' => '2024-02-01 12:00:00', + 'member_id' => $newMember->getKey(), + ]) + ->call('save') + ->assertHasNoFormErrors(); + + // Assert + $response->assertSuccessful(); + $timeEntry->refresh(); + $this->assertSame($newMember->getKey(), $timeEntry->member_id); + $this->assertSame($newUser->getKey(), $timeEntry->user_id); + $this->assertSame($newOrganization->getKey(), $timeEntry->organization_id); + } }