From bd9cede081e3c593108ed3e37e1b53a29effc722 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Mon, 10 Jun 2024 19:19:51 +0200 Subject: [PATCH] Prevent and remove zero values for billable rates --- .../Resources/OrganizationResource.php | 5 + .../Resources/ProjectMemberResource.php | 91 +++++++++++++++++++ .../Pages/CreateProjectMember.php | 13 +++ .../Pages/EditProjectMember.php | 21 +++++ .../Pages/ListProjectMembers.php | 21 +++++ .../Pages/ViewProjectMembers.php | 22 +++++ app/Filament/Resources/ProjectResource.php | 12 ++- .../ProjectMembersRelationManager.php | 60 ++++++++++++ .../Api/V1/OrganizationController.php | 2 +- .../Controllers/Api/V1/ProjectController.php | 4 +- .../Api/V1/ProjectMemberController.php | 4 +- .../V1/Member/MemberUpdateRequest.php | 7 ++ .../OrganizationUpdateRequest.php | 7 ++ .../V1/Project/ProjectStoreRequest.php | 7 ++ .../V1/Project/ProjectUpdateRequest.php | 7 ++ .../ProjectMemberStoreRequest.php | 7 ++ .../ProjectMemberUpdateRequest.php | 7 ++ app/Service/Import/ImportDatabaseHelper.php | 8 +- .../Import/Importers/DefaultImporter.php | 12 ++- ...eset_billable_rates_with_zero_as_value.php | 35 +++++++ 20 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 app/Filament/Resources/ProjectMemberResource.php create mode 100644 app/Filament/Resources/ProjectMemberResource/Pages/CreateProjectMember.php create mode 100644 app/Filament/Resources/ProjectMemberResource/Pages/EditProjectMember.php create mode 100644 app/Filament/Resources/ProjectMemberResource/Pages/ListProjectMembers.php create mode 100644 app/Filament/Resources/ProjectMemberResource/Pages/ViewProjectMembers.php create mode 100644 app/Filament/Resources/ProjectResource/RelationManagers/ProjectMembersRelationManager.php create mode 100644 database/migrations/2024_06_10_161831_reset_billable_rates_with_zero_as_value.php diff --git a/app/Filament/Resources/OrganizationResource.php b/app/Filament/Resources/OrganizationResource.php index 3d531db3..d40c75f5 100644 --- a/app/Filament/Resources/OrganizationResource.php +++ b/app/Filament/Resources/OrganizationResource.php @@ -65,6 +65,11 @@ class OrganizationResource extends Resource Forms\Components\TextInput::make('billable_rate') ->label('Billable rate (in Cents)') ->nullable() + ->rules([ + 'nullable', + 'integer', + 'gt:0', + ]) ->numeric(), Forms\Components\DateTimePicker::make('created_at') ->label('Created At') diff --git a/app/Filament/Resources/ProjectMemberResource.php b/app/Filament/Resources/ProjectMemberResource.php new file mode 100644 index 00000000..feb58ca4 --- /dev/null +++ b/app/Filament/Resources/ProjectMemberResource.php @@ -0,0 +1,91 @@ +schema([ + Forms\Components\TextInput::make('billable_rate') + ->label('Billable rate (in Cents)') + ->nullable() + ->rules([ + 'nullable', + 'integer', + 'gt:0', + ]) + ->numeric(), + Forms\Components\Select::make('user_id') + ->relationship('user', 'name') + ->required(), + Forms\Components\Select::make('member_id') + ->relationship('member', 'id') + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('id') + ->label('ID'), + Tables\Columns\TextColumn::make('billable_rate') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('project.name'), + Tables\Columns\TextColumn::make('user.name'), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable(), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListProjectMembers::route('/'), + 'create' => Pages\CreateProjectMember::route('/create'), + 'edit' => Pages\EditProjectMember::route('/{record}/edit'), + 'view' => Pages\ViewProjectMembers::route('/{record}'), + ]; + } +} diff --git a/app/Filament/Resources/ProjectMemberResource/Pages/CreateProjectMember.php b/app/Filament/Resources/ProjectMemberResource/Pages/CreateProjectMember.php new file mode 100644 index 00000000..8838b2e1 --- /dev/null +++ b/app/Filament/Resources/ProjectMemberResource/Pages/CreateProjectMember.php @@ -0,0 +1,13 @@ +icon('heroicon-s-pencil'), + ]; + } +} diff --git a/app/Filament/Resources/ProjectResource.php b/app/Filament/Resources/ProjectResource.php index 316c8c7f..56e3aca7 100644 --- a/app/Filament/Resources/ProjectResource.php +++ b/app/Filament/Resources/ProjectResource.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Filament\Resources; use App\Filament\Resources\ProjectResource\Pages; +use App\Filament\Resources\ProjectResource\RelationManagers\ProjectMembersRelationManager; use App\Models\Project; use Filament\Forms; use Filament\Forms\Components\ColorPicker; @@ -37,6 +38,15 @@ class ProjectResource extends Resource ColorPicker::make('color') ->label('Color') ->required(), + Forms\Components\TextInput::make('billable_rate') + ->label('Billable rate (in Cents)') + ->nullable() + ->rules([ + 'nullable', + 'integer', + 'gt:0', + ]) + ->numeric(), Forms\Components\Select::make('organization_id') ->relationship(name: 'organization', titleAttribute: 'name') ->searchable(['name']) @@ -78,7 +88,7 @@ class ProjectResource extends Resource public static function getRelations(): array { return [ - // + ProjectMembersRelationManager::make(), ]; } diff --git a/app/Filament/Resources/ProjectResource/RelationManagers/ProjectMembersRelationManager.php b/app/Filament/Resources/ProjectResource/RelationManagers/ProjectMembersRelationManager.php new file mode 100644 index 00000000..67e1403e --- /dev/null +++ b/app/Filament/Resources/ProjectResource/RelationManagers/ProjectMembersRelationManager.php @@ -0,0 +1,60 @@ +schema([ + ]); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('name') + ->columns([ + Tables\Columns\TextColumn::make('user.name'), + Tables\Columns\TextColumn::make('billable_rate') + ->numeric() + ->sortable(), + ]) + ->filters([ + // + ]) + ->headerActions([ + ]) + ->actions([ + Action::make('view') + ->icon('heroicon-o-eye') + ->color('gray') + ->url(fn (ProjectMember $record): string => ProjectMemberResource::getUrl('view', [ + 'record' => $record->getKey(), + ])), + Action::make('edit') + ->icon('heroicon-o-pencil') + ->url(fn (ProjectMember $record): string => ProjectMemberResource::getUrl('edit', [ + 'record' => $record->getKey(), + ])) + ->openUrlInNewTab(), + ]) + ->bulkActions([ + ]); + } +} diff --git a/app/Http/Controllers/Api/V1/OrganizationController.php b/app/Http/Controllers/Api/V1/OrganizationController.php index 5dcd431c..686b30ca 100644 --- a/app/Http/Controllers/Api/V1/OrganizationController.php +++ b/app/Http/Controllers/Api/V1/OrganizationController.php @@ -37,7 +37,7 @@ class OrganizationController extends Controller $this->checkPermission($organization, 'organizations:update'); $organization->name = $request->input('name'); - $organization->billable_rate = $request->input('billable_rate'); + $organization->billable_rate = $request->getBillableRate(); $organization->save(); return new OrganizationResource($organization); diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php index de48efdb..eb4a6ccc 100644 --- a/app/Http/Controllers/Api/V1/ProjectController.php +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -86,7 +86,7 @@ class ProjectController extends Controller $project->name = $request->input('name'); $project->color = $request->input('color'); $project->is_billable = (bool) $request->input('is_billable'); - $project->billable_rate = $request->input('billable_rate'); + $project->billable_rate = $request->getBillableRate(); $project->client_id = $request->input('client_id'); $project->organization()->associate($organization); $project->save(); @@ -107,7 +107,7 @@ class ProjectController extends Controller $project->name = $request->input('name'); $project->color = $request->input('color'); $project->is_billable = (bool) $request->input('is_billable'); - $project->billable_rate = $request->input('billable_rate'); + $project->billable_rate = $request->getBillableRate(); $project->client_id = $request->input('client_id'); $project->save(); diff --git a/app/Http/Controllers/Api/V1/ProjectMemberController.php b/app/Http/Controllers/Api/V1/ProjectMemberController.php index 27229506..a74aa50f 100644 --- a/app/Http/Controllers/Api/V1/ProjectMemberController.php +++ b/app/Http/Controllers/Api/V1/ProjectMemberController.php @@ -71,7 +71,7 @@ class ProjectMemberController extends Controller } $projectMember = new ProjectMember(); - $projectMember->billable_rate = $request->input('billable_rate'); + $projectMember->billable_rate = $request->getBillableRate(); $projectMember->member()->associate($member); $projectMember->user()->associate($member->user); $projectMember->project()->associate($project); @@ -90,7 +90,7 @@ class ProjectMemberController extends Controller public function update(Organization $organization, ProjectMember $projectMember, ProjectMemberUpdateRequest $request): JsonResource { $this->checkPermission($organization, 'project-members:update', projectMember: $projectMember); - $projectMember->billable_rate = $request->input('billable_rate'); + $projectMember->billable_rate = $request->getBillableRate(); $projectMember->save(); return new ProjectMemberResource($projectMember); diff --git a/app/Http/Requests/V1/Member/MemberUpdateRequest.php b/app/Http/Requests/V1/Member/MemberUpdateRequest.php index 3b1859fa..5f9ae09e 100644 --- a/app/Http/Requests/V1/Member/MemberUpdateRequest.php +++ b/app/Http/Requests/V1/Member/MemberUpdateRequest.php @@ -36,4 +36,11 @@ class MemberUpdateRequest extends FormRequest ], ]; } + + public function getBillableRate(): ?int + { + $input = $this->input('billable_rate'); + + return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null; + } } diff --git a/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php b/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php index 69981194..7c25fa37 100644 --- a/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php +++ b/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php @@ -33,4 +33,11 @@ class OrganizationUpdateRequest extends FormRequest ], ]; } + + public function getBillableRate(): ?int + { + $input = $this->input('billable_rate'); + + return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null; + } } diff --git a/app/Http/Requests/V1/Project/ProjectStoreRequest.php b/app/Http/Requests/V1/Project/ProjectStoreRequest.php index 3446e496..0e852fc0 100644 --- a/app/Http/Requests/V1/Project/ProjectStoreRequest.php +++ b/app/Http/Requests/V1/Project/ProjectStoreRequest.php @@ -56,4 +56,11 @@ class ProjectStoreRequest extends FormRequest ], ]; } + + public function getBillableRate(): ?int + { + $input = $this->input('billable_rate'); + + return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null; + } } diff --git a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php index 29af649f..6e6ca391 100644 --- a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php +++ b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php @@ -55,4 +55,11 @@ class ProjectUpdateRequest extends FormRequest ], ]; } + + public function getBillableRate(): ?int + { + $input = $this->input('billable_rate'); + + return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null; + } } diff --git a/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php b/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php index dd176c9f..de3e8cf4 100644 --- a/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php +++ b/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php @@ -39,4 +39,11 @@ class ProjectMemberStoreRequest extends FormRequest ], ]; } + + public function getBillableRate(): ?int + { + $input = $this->input('billable_rate'); + + return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null; + } } diff --git a/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php b/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php index 7401e635..664a2d56 100644 --- a/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php +++ b/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php @@ -28,4 +28,11 @@ class ProjectMemberUpdateRequest extends FormRequest ], ]; } + + public function getBillableRate(): ?int + { + $input = $this->input('billable_rate'); + + return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null; + } } diff --git a/app/Service/Import/ImportDatabaseHelper.php b/app/Service/Import/ImportDatabaseHelper.php index ab747895..1ac73ea4 100644 --- a/app/Service/Import/ImportDatabaseHelper.php +++ b/app/Service/Import/ImportDatabaseHelper.php @@ -58,12 +58,14 @@ class ImportDatabaseHelper */ private array $validate; + private ?Closure $beforeSave; + /** * @param class-string $model * @param array $identifiers * @param array> $validate */ - public function __construct(string $model, array $identifiers, bool $attachToExisting = false, ?Closure $queryModifier = null, ?Closure $afterCreate = null, array $validate = []) + public function __construct(string $model, array $identifiers, bool $attachToExisting = false, ?Closure $queryModifier = null, ?Closure $afterCreate = null, array $validate = [], ?Closure $beforeSave = null) { $this->model = $model; $this->identifiers = $identifiers; @@ -72,6 +74,7 @@ class ImportDatabaseHelper $this->afterCreate = $afterCreate; $this->createdCount = 0; $this->validate = $validate; + $this->beforeSave = $beforeSave; } /** @@ -99,6 +102,9 @@ class ImportDatabaseHelper foreach ($data as $key => $value) { $model->{$key} = $value; } + if ($this->beforeSave !== null) { + ($this->beforeSave)($model); + } $model->save(); if ($this->afterCreate !== null) { diff --git a/app/Service/Import/Importers/DefaultImporter.php b/app/Service/Import/Importers/DefaultImporter.php index 1e107f4a..11830f77 100644 --- a/app/Service/Import/Importers/DefaultImporter.php +++ b/app/Service/Import/Importers/DefaultImporter.php @@ -107,7 +107,11 @@ abstract class DefaultImporter implements ImporterContract 'nullable', 'integer', ], - ]); + ], beforeSave: function (Project $project) { + if ($project->billable_rate === 0) { + $project->billable_rate = null; + } + }); $this->projectMemberImportHelper = new ImportDatabaseHelper(ProjectMember::class, ['project_id', 'member_id'], true, function (Builder $builder) { /** @var Builder $builder */ return $builder->whereBelongsToOrganization($this->organization); @@ -116,7 +120,11 @@ abstract class DefaultImporter implements ImporterContract 'nullable', 'integer', ], - ]); + ], beforeSave: function (ProjectMember $projectMember) { + if ($projectMember->billable_rate === 0) { + $projectMember->billable_rate = null; + } + }); $this->tagImportHelper = new ImportDatabaseHelper(Tag::class, ['name', 'organization_id'], true, function (Builder $builder) { return $builder->where('organization_id', $this->organization->id); }, validate: [ diff --git a/database/migrations/2024_06_10_161831_reset_billable_rates_with_zero_as_value.php b/database/migrations/2024_06_10_161831_reset_billable_rates_with_zero_as_value.php new file mode 100644 index 00000000..39b4c006 --- /dev/null +++ b/database/migrations/2024_06_10_161831_reset_billable_rates_with_zero_as_value.php @@ -0,0 +1,35 @@ +where('billable_rate', '=', 0) + ->update(['billable_rate' => null]); + DB::table('project_members') + ->where('billable_rate', '=', 0) + ->update(['billable_rate' => null]); + DB::table('projects') + ->where('billable_rate', '=', 0) + ->update(['billable_rate' => null]); + DB::table('members') + ->where('billable_rate', '=', 0) + ->update(['billable_rate' => null]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +};