From 706389a5c31055d2b2d1af3c9887f189ff1b671c Mon Sep 17 00:00:00 2001 From: Kamil Date: Thu, 6 Jun 2024 14:46:50 +0200 Subject: [PATCH 01/14] #444 - wip: added models and create form for overtime requests --- app/Actions/OvertimeRequest/CreateAction.php | 59 ++++ app/Domain/OvertimeCalculator.php | 18 ++ app/Domain/OvertimeRequestStateManager.php | 74 +++++ app/Domain/OvertimeRequestStatesRetriever.php | 72 +++++ app/Enums/SettlementType.php | 28 ++ .../Controllers/OvertimeRequestController.php | 30 ++ app/Http/Requests/OvertimeRequestRequest.php | 43 +++ app/Models/OvertimeRequest.php | 73 +++++ app/Models/OvertimeRequestActivity.php | 45 +++ app/Models/User.php | 5 + app/Models/YearPeriod.php | 6 + app/Observers/OvertimeRequestObserver.php | 18 ++ app/Providers/ObserverServiceProvider.php | 3 + .../OvertimeRequest/AcceptedByTechnical.php | 10 + app/States/OvertimeRequest/Approved.php | 10 + app/States/OvertimeRequest/Cancelled.php | 10 + app/States/OvertimeRequest/Created.php | 10 + .../OvertimeRequest/OvertimeRequestState.php | 38 +++ app/States/OvertimeRequest/Rejected.php | 10 + app/States/OvertimeRequest/Settled.php | 10 + .../OvertimeRequest/WaitingForTechnical.php | 10 + .../OvertimeRequestActivityFactory.php | 22 ++ database/factories/OvertimeRequestFactory.php | 35 +++ ..._080039_create_overtime_requests_table.php | 35 +++ ...eate_overtime_request_activities_table.php | 28 ++ .../js/Composables/settlementTypeInfo.js | 29 ++ resources/js/Pages/OvertimeRequest/Create.vue | 280 ++++++++++++++++++ resources/js/Shared/SettlementType.vue | 23 ++ routes/web.php | 7 + 29 files changed, 1041 insertions(+) create mode 100644 app/Actions/OvertimeRequest/CreateAction.php create mode 100644 app/Domain/OvertimeCalculator.php create mode 100644 app/Domain/OvertimeRequestStateManager.php create mode 100644 app/Domain/OvertimeRequestStatesRetriever.php create mode 100644 app/Enums/SettlementType.php create mode 100644 app/Http/Controllers/OvertimeRequestController.php create mode 100644 app/Http/Requests/OvertimeRequestRequest.php create mode 100644 app/Models/OvertimeRequest.php create mode 100644 app/Models/OvertimeRequestActivity.php create mode 100644 app/Observers/OvertimeRequestObserver.php create mode 100644 app/States/OvertimeRequest/AcceptedByTechnical.php create mode 100644 app/States/OvertimeRequest/Approved.php create mode 100644 app/States/OvertimeRequest/Cancelled.php create mode 100644 app/States/OvertimeRequest/Created.php create mode 100644 app/States/OvertimeRequest/OvertimeRequestState.php create mode 100644 app/States/OvertimeRequest/Rejected.php create mode 100644 app/States/OvertimeRequest/Settled.php create mode 100644 app/States/OvertimeRequest/WaitingForTechnical.php create mode 100644 database/factories/OvertimeRequestActivityFactory.php create mode 100644 database/factories/OvertimeRequestFactory.php create mode 100644 database/migrations/2024_06_06_080039_create_overtime_requests_table.php create mode 100644 database/migrations/2024_06_06_140009_create_overtime_request_activities_table.php create mode 100644 resources/js/Composables/settlementTypeInfo.js create mode 100644 resources/js/Pages/OvertimeRequest/Create.vue create mode 100644 resources/js/Shared/SettlementType.vue diff --git a/app/Actions/OvertimeRequest/CreateAction.php b/app/Actions/OvertimeRequest/CreateAction.php new file mode 100644 index 00000000..4de92e7e --- /dev/null +++ b/app/Actions/OvertimeRequest/CreateAction.php @@ -0,0 +1,59 @@ +createVacationRequest($data, $creator); + + $this->handleCreatedOvertimeRequest($overtimeRequest); + + return $overtimeRequest; + } + + protected function createVacationRequest(array $data, User $creator): OvertimeRequest + { + /** @var OvertimeRequest $overtimeRequest */ + $overtimeRequest = $creator->createdOvertimeRequests()->make($data); + $overtimeRequest->hours = $this->overtimeCalculator->calculateHours($overtimeRequest->from, $overtimeRequest->to); + + $overtimeRequest->save(); + + $this->stateManager->markAsCreated($overtimeRequest); + + return $overtimeRequest; + } + + protected function handleCreatedOvertimeRequest(OvertimeRequest $overtimeRequest): void + { + } + + protected function notify(VacationRequest $vacationRequest): void + { + $vacationRequest->user->notify(new VacationRequestCreatedNotification($vacationRequest)); + } +} diff --git a/app/Domain/OvertimeCalculator.php b/app/Domain/OvertimeCalculator.php new file mode 100644 index 00000000..c74108f1 --- /dev/null +++ b/app/Domain/OvertimeCalculator.php @@ -0,0 +1,18 @@ +diffInHours($to); + + return (int)$hours; + } +} diff --git a/app/Domain/OvertimeRequestStateManager.php b/app/Domain/OvertimeRequestStateManager.php new file mode 100644 index 00000000..aef32536 --- /dev/null +++ b/app/Domain/OvertimeRequestStateManager.php @@ -0,0 +1,74 @@ +createActivity($overtimeRequest, null, $overtimeRequest->state, $overtimeRequest->creator); + } + + public function approve(OvertimeRequest $overtimeRequest, ?User $user = null): void + { + $this->changeState($overtimeRequest, Approved::class, $user); + } + + public function reject(OvertimeRequest $overtimeRequest, User $user): void + { + $this->changeState($overtimeRequest, Rejected::class, $user); + } + + public function cancel(OvertimeRequest $overtimeRequest, User $user): void + { + $this->changeState($overtimeRequest, Cancelled::class, $user); + } + + public function acceptAsTechnical(OvertimeRequest $overtimeRequest, User $user): void + { + $this->changeState($overtimeRequest, AcceptedByTechnical::class, $user); + } + + public function waitForTechnical(OvertimeRequest $overtimeRequest): void + { + $this->changeState($overtimeRequest, WaitingForTechnical::class); + } + + protected function changeState(OvertimeRequest $overtimeRequest, string $state, ?User $user = null): void + { + $previousState = $overtimeRequest->state; + $overtimeRequest->state->transitionTo($state); + $overtimeRequest->save(); + + $this->createActivity($overtimeRequest, $previousState, $overtimeRequest->state, $user); + } + + protected function createActivity( + OvertimeRequest $overtimeRequest, + ?OvertimeRequestState $from, + OvertimeRequestState $to, + ?User $user = null, + ): void { + $overtimeRequest->activities()->create([ + "from" => $from, + "to" => $to, + "user_id" => $user?->id, + ]); + } +} diff --git a/app/Domain/OvertimeRequestStatesRetriever.php b/app/Domain/OvertimeRequestStatesRetriever.php new file mode 100644 index 00000000..f2d11fb6 --- /dev/null +++ b/app/Domain/OvertimeRequestStatesRetriever.php @@ -0,0 +1,72 @@ +role) { + Role::TechnicalApprover, Role::Administrator => [WaitingForTechnical::class], + default => [], + }; + } + + public static function all(): array + { + return [ + ...self::pendingStates(), + ...self::successStates(), + ...self::failedStates(), + ]; + } + + public static function filterByStatusGroup(string $filter, ?User $user = null): array + { + return match ($filter) { + "pending" => self::pendingStates(), + "success" => self::successStates(), + "failed" => self::failedStates(), + "waiting_for_action" => self::waitingForUserActionStates($user), + default => self::all(), + }; + } +} diff --git a/app/Enums/SettlementType.php b/app/Enums/SettlementType.php new file mode 100644 index 00000000..e7008a7c --- /dev/null +++ b/app/Enums/SettlementType.php @@ -0,0 +1,28 @@ +value); + } + + public static function casesToSelect(): array + { + $cases = collect(SettlementType::cases()); + + return $cases->map( + fn(SettlementType $enum): array => [ + "label" => $enum->label(), + "value" => $enum->value, + ], + )->toArray(); + } +} diff --git a/app/Http/Controllers/OvertimeRequestController.php b/app/Http/Controllers/OvertimeRequestController.php new file mode 100644 index 00000000..c9667b42 --- /dev/null +++ b/app/Http/Controllers/OvertimeRequestController.php @@ -0,0 +1,30 @@ + SettlementType::casesToSelect(), + ]); + } + + public function store(OvertimeRequestRequest $request, CreateAction $createAction): RedirectResponse + { + $overtimeRequest = $createAction->execute($request->data(), $request->user()); + + return redirect() + ->route("overtime.requests.show", $overtimeRequest) + ->with("success", __("Request created.")); + } +} diff --git a/app/Http/Requests/OvertimeRequestRequest.php b/app/Http/Requests/OvertimeRequestRequest.php new file mode 100644 index 00000000..9f5f0bdb --- /dev/null +++ b/app/Http/Requests/OvertimeRequestRequest.php @@ -0,0 +1,43 @@ + ["required", "exists:users,id"], + "from" => ["required", "date_format:Y-m-d H:i", new YearPeriodExists()], + "to" => ["required", "date_format:Y-m-d H:i", new YearPeriodExists()], + "type" => ["required", new Enum(SettlementType::class)], + "comment" => ["nullable"], + ]; + } + + public function data(): array + { + return [ + "user_id" => $this->get("user"), + "from" => $this->get("from"), + "to" => $this->get("to"), + "settlement_type" => $this->get("type"), + "year_period_id" => $this->yearPeriod()->id, + "comment" => $this->get("comment"), + ]; + } + + public function yearPeriod(): YearPeriod + { + return YearPeriod::findByYear(Carbon::create($this->get("from"))->year); + } +} diff --git a/app/Models/OvertimeRequest.php b/app/Models/OvertimeRequest.php new file mode 100644 index 00000000..0c7293e5 --- /dev/null +++ b/app/Models/OvertimeRequest.php @@ -0,0 +1,73 @@ + OvertimeRequestState::class, + "from" => "datetime", + "to" => "datetime", + "settled" => "boolean", + "settlement_type" => SettlementType::class, + ]; + protected $perPage = 50; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class) + ->withTrashed(); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, "creator_id"); + } + + public function yearPeriod(): BelongsTo + { + return $this->belongsTo(YearPeriod::class); + } + + public function activities(): HasMany + { + return $this->hasMany(OvertimeRequestActivity::class); + } + + protected static function newFactory(): OvertimeRequestFactory + { + return OvertimeRequestFactory::new(); + } +} diff --git a/app/Models/OvertimeRequestActivity.php b/app/Models/OvertimeRequestActivity.php new file mode 100644 index 00000000..53700c60 --- /dev/null +++ b/app/Models/OvertimeRequestActivity.php @@ -0,0 +1,45 @@ + OvertimeRequestState::class, + "to" => OvertimeRequestState::class, + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class) + ->withTrashed(); + } + + public function overtimeRequest(): BelongsTo + { + return $this->belongsTo(OvertimeRequest::class); + } + + protected static function newFactory(): OvertimeRequestActivityFactory + { + return OvertimeRequestActivityFactory::new(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index a6027a7e..4266fb00 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -75,6 +75,11 @@ public function createdVacationRequests(): HasMany return $this->hasMany(VacationRequest::class, "creator_id"); } + public function createdOvertimeRequests(): HasMany + { + return $this->hasMany(OvertimeRequest::class, "creator_id"); + } + public function vacations(): HasMany { return $this->hasMany(Vacation::class); diff --git a/app/Models/YearPeriod.php b/app/Models/YearPeriod.php index a4473ef5..edb010fa 100644 --- a/app/Models/YearPeriod.php +++ b/app/Models/YearPeriod.php @@ -16,6 +16,7 @@ * @property int $year * @property Collection $vacationLimits * @property Collection $vacationRequests + * @property Collection $overtimeRequests * @property Collection $holidays */ class YearPeriod extends Model @@ -47,6 +48,11 @@ public function vacationRequests(): HasMany return $this->hasMany(VacationRequest::class); } + public function overtimeRequests(): HasMany + { + return $this->hasMany(OvertimeRequest::class); + } + public function holidays(): HasMany { return $this->hasMany(Holiday::class); diff --git a/app/Observers/OvertimeRequestObserver.php b/app/Observers/OvertimeRequestObserver.php new file mode 100644 index 00000000..12c2ee74 --- /dev/null +++ b/app/Observers/OvertimeRequestObserver.php @@ -0,0 +1,18 @@ +yearPeriod->overtimeRequests()->count(); + $number = $count + 1; + + $overtime->name = "{$number}/{$overtime->yearPeriod->year}"; + } +} diff --git a/app/Providers/ObserverServiceProvider.php b/app/Providers/ObserverServiceProvider.php index 54990af8..15217ad8 100644 --- a/app/Providers/ObserverServiceProvider.php +++ b/app/Providers/ObserverServiceProvider.php @@ -5,9 +5,11 @@ namespace Toby\Providers; use Illuminate\Support\ServiceProvider; +use Toby\Models\OvertimeRequest; use Toby\Models\User; use Toby\Models\VacationLimit; use Toby\Models\VacationRequest; +use Toby\Observers\OvertimeRequestObserver; use Toby\Observers\UserObserver; use Toby\Observers\VacationLimitObserver; use Toby\Observers\VacationRequestObserver; @@ -19,5 +21,6 @@ public function boot(): void User::observe(UserObserver::class); VacationRequest::observe(VacationRequestObserver::class); VacationLimit::observe(VacationLimitObserver::class); + OvertimeRequest::observe(OvertimeRequestObserver::class); } } diff --git a/app/States/OvertimeRequest/AcceptedByTechnical.php b/app/States/OvertimeRequest/AcceptedByTechnical.php new file mode 100644 index 00000000..b0160585 --- /dev/null +++ b/app/States/OvertimeRequest/AcceptedByTechnical.php @@ -0,0 +1,10 @@ +default(Created::class) + ->allowTransition(Created::class, Approved::class) + ->allowTransition(Created::class, WaitingForTechnical::class) + ->allowTransition(WaitingForTechnical::class, Rejected::class) + ->allowTransition(WaitingForTechnical::class, AcceptedByTechnical::class) + ->allowTransition(AcceptedByTechnical::class, Approved::class) + ->allowTransition([ + Created::class, + WaitingForTechnical::class, + AcceptedByTechnical::class, + Approved::class, + ], Cancelled::class) + ->allowTransition(Approved::class, Settled::class); + } + + public function label(): string + { + return __(static::$name); + } +} diff --git a/app/States/OvertimeRequest/Rejected.php b/app/States/OvertimeRequest/Rejected.php new file mode 100644 index 00000000..8be0d9f0 --- /dev/null +++ b/app/States/OvertimeRequest/Rejected.php @@ -0,0 +1,10 @@ + $this->faker->randomElement(OvertimeRequestState::all()), + "to" => $this->faker->randomElement(OvertimeRequestState::all()), + ]; + } +} diff --git a/database/factories/OvertimeRequestFactory.php b/database/factories/OvertimeRequestFactory.php new file mode 100644 index 00000000..e822b282 --- /dev/null +++ b/database/factories/OvertimeRequestFactory.php @@ -0,0 +1,35 @@ +faker->dateTimeThisYear); + $to = $from->addHours($this->faker->numberBetween(1, 10)); + $hours = $to->diffInHours($from); + + return [ + "user_id" => User::factory(), + "creator_id" => fn(array $attributes): int => $attributes["user_id"], + "year_period_id" => YearPeriod::factory(), + "state" => $this->faker->randomElement(OvertimeRequestStatesRetriever::all()), + "from" => $from, + "to" => $to, + "hours" => $hours, + "comment" => $this->faker->boolean ? $this->faker->paragraph() : null, + ]; + } +} diff --git a/database/migrations/2024_06_06_080039_create_overtime_requests_table.php b/database/migrations/2024_06_06_080039_create_overtime_requests_table.php new file mode 100644 index 00000000..aaa50b5f --- /dev/null +++ b/database/migrations/2024_06_06_080039_create_overtime_requests_table.php @@ -0,0 +1,35 @@ +id(); + $table->string("name"); + $table->foreignIdFor(User::class, "creator_id")->constrained("users")->cascadeOnDelete(); + $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(YearPeriod::class)->constrained()->cascadeOnDelete(); + $table->string("state")->nullable(); + $table->string("settlement_type"); + $table->boolean("settled")->default(false); + $table->date("from"); + $table->date("to"); + $table->integer("hours"); + $table->text("comment")->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("overtime_requests"); + } +}; diff --git a/database/migrations/2024_06_06_140009_create_overtime_request_activities_table.php b/database/migrations/2024_06_06_140009_create_overtime_request_activities_table.php new file mode 100644 index 00000000..69328d35 --- /dev/null +++ b/database/migrations/2024_06_06_140009_create_overtime_request_activities_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignIdFor(OvertimeRequest::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(User::class)->nullable()->constrained()->cascadeOnDelete(); + $table->string("from")->nullable(); + $table->string("to"); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("overtime_request_activities"); + } +}; diff --git a/resources/js/Composables/settlementTypeInfo.js b/resources/js/Composables/settlementTypeInfo.js new file mode 100644 index 00000000..9a0018ef --- /dev/null +++ b/resources/js/Composables/settlementTypeInfo.js @@ -0,0 +1,29 @@ +import ClockTimeFourOutlineIcon from 'vue-material-design-icons/ClockTimeFourOutline.vue' +import CurrencyUsdIcon from 'vue-material-design-icons/CurrencyUsd.vue' + +const types = [ + { + text: 'Godzinowe', + value: 'hours', + icon: ClockTimeFourOutlineIcon, + color: 'text-blue-500', + border: 'border-blue-500', + }, + { + text: 'Pieniężne', + value: 'money', + icon: CurrencyUsdIcon, + color: 'text-green-500', + border: 'border-green-500', + }, +] + +export default function useSettlementTypeInfo() { + const getTypes = () => types + const findType = value => types.find(type => type.value === value) + + return { + getTypes, + findType, + } +} diff --git a/resources/js/Pages/OvertimeRequest/Create.vue b/resources/js/Pages/OvertimeRequest/Create.vue new file mode 100644 index 00000000..9c7a5b2c --- /dev/null +++ b/resources/js/Pages/OvertimeRequest/Create.vue @@ -0,0 +1,280 @@ + + +