From e2d7ddd36b583200a5d3f218a62dd0869579b988 Mon Sep 17 00:00:00 2001 From: Kamil Piech Date: Fri, 28 Jun 2024 08:33:42 +0200 Subject: [PATCH] #443 - employment, medical and ohs history (#451) * #443 - added tests for user history * #443 - added console command for migrating data * #443 - cr fixes * #443 - fix: code review fixes * #443 - fix: fixes after code review - fixed validation and edit view * #443 - fix: fixes --- .../MigrateProfileDataIntoUserHistory.php | 49 ++++ app/Enums/UserHistoryType.php | 29 ++ app/Http/Controllers/UserController.php | 1 + .../Controllers/UserHistoryController.php | 86 ++++++ app/Http/Requests/UserHistoryRequest.php | 35 +++ app/Http/Requests/UserRequest.php | 6 - app/Http/Resources/UserHistoryResource.php | 27 ++ app/Http/Resources/UserResource.php | 11 +- app/Models/User.php | 22 ++ app/Models/UserHistory.php | 40 +++ database/factories/UserHistoryFactory.php | 30 ++ ...24_06_14_112722_add_user_history_table.php | 28 ++ lang/pl.json | 8 +- lang/pl/validation.php | 9 + resources/js/Pages/UserHistory/Create.vue | 276 ++++++++++++++++++ resources/js/Pages/UserHistory/Edit.vue | 276 ++++++++++++++++++ resources/js/Pages/UserHistory/Index.vue | 166 +++++++++++ resources/js/Pages/Users/Create.vue | 92 ------ resources/js/Pages/Users/Edit.vue | 92 ------ resources/js/Pages/Users/Index.vue | 13 +- routes/web.php | 14 + tests/Feature/UserHistoryTest.php | 109 +++++++ 22 files changed, 1223 insertions(+), 196 deletions(-) create mode 100644 app/Console/Commands/MigrateProfileDataIntoUserHistory.php create mode 100644 app/Enums/UserHistoryType.php create mode 100644 app/Http/Controllers/UserHistoryController.php create mode 100644 app/Http/Requests/UserHistoryRequest.php create mode 100644 app/Http/Resources/UserHistoryResource.php create mode 100644 app/Models/UserHistory.php create mode 100644 database/factories/UserHistoryFactory.php create mode 100644 database/migrations/2024_06_14_112722_add_user_history_table.php create mode 100644 resources/js/Pages/UserHistory/Create.vue create mode 100644 resources/js/Pages/UserHistory/Edit.vue create mode 100644 resources/js/Pages/UserHistory/Index.vue create mode 100644 tests/Feature/UserHistoryTest.php diff --git a/app/Console/Commands/MigrateProfileDataIntoUserHistory.php b/app/Console/Commands/MigrateProfileDataIntoUserHistory.php new file mode 100644 index 00000000..c46f23c3 --- /dev/null +++ b/app/Console/Commands/MigrateProfileDataIntoUserHistory.php @@ -0,0 +1,49 @@ +with("profile") + ->get(); + + foreach ($users as $user) { + $this->moveMedicalDataToHistory($user); + $this->moveOhsDataToHistory($user); + } + } + + private function moveMedicalDataToHistory(User $user): void + { + if ($user->profile->last_medical_exam_date && $user->profile->next_medical_exam_date) { + $user->histories()->create([ + "from" => $user->profile->last_medical_exam_date, + "to" => $user->profile->next_medical_exam_date, + "type" => UserHistoryType::MedicalExam, + ]); + } + } + + private function moveOhsDataToHistory(User $user): void + { + if ($user->profile->last_ohs_training_date && $user->profile->next_ohs_training_date) { + $user->histories()->create([ + "from" => $user->profile->last_ohs_training_date, + "to" => $user->profile->next_ohs_training_date, + "type" => UserHistoryType::OhsTraining, + ]); + } + } +} diff --git a/app/Enums/UserHistoryType.php b/app/Enums/UserHistoryType.php new file mode 100644 index 00000000..6b4a2b9e --- /dev/null +++ b/app/Enums/UserHistoryType.php @@ -0,0 +1,29 @@ +value); + } + + public static function casesToSelect(): array + { + $cases = collect(UserHistoryType::cases()); + + return $cases->map( + fn(UserHistoryType $enum): array => [ + "label" => $enum->label(), + "value" => $enum->value, + ], + )->toArray(); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 659f70d6..72e81526 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -31,6 +31,7 @@ public function index(Request $request): Response $status = $request->query("status", "active"); $users = User::query() + ->with("histories") ->search($searchText) ->status($status) ->orderByProfileField("last_name") diff --git a/app/Http/Controllers/UserHistoryController.php b/app/Http/Controllers/UserHistoryController.php new file mode 100644 index 00000000..ffe8d00e --- /dev/null +++ b/app/Http/Controllers/UserHistoryController.php @@ -0,0 +1,86 @@ +authorize("manageUsers"); + + $history = $user->histories() + ->orderBy("from", "desc") + ->get(); + + return inertia("UserHistory/Index", [ + "history" => UserHistoryResource::collection($history), + "userId" => $user->id, + ]); + } + + public function create(User $user): Response + { + $this->authorize("manageUsers"); + + return inertia("UserHistory/Create", [ + "types" => UserHistoryType::casesToSelect(), + "employmentForms" => EmploymentForm::casesToSelect(), + "userId" => $user->id, + ]); + } + + public function store(UserHistoryRequest $request, User $user): RedirectResponse + { + $this->authorize("manageUsers"); + + $user->histories()->create($request->data()); + + return redirect() + ->route("users.history", $user) + ->with("success", __("User history created.")); + } + + public function edit(UserHistory $history): Response + { + $this->authorize("manageUsers"); + + return inertia("UserHistory/Edit", [ + "history" => UserHistoryResource::make($history), + "types" => UserHistoryType::casesToSelect(), + "employmentForms" => EmploymentForm::casesToSelect(), + ]); + } + + public function update(UserHistoryRequest $request, UserHistory $history): RedirectResponse + { + $this->authorize("manageUsers"); + + $history->update($request->data()); + + return redirect() + ->route("users.history", $history->user_id) + ->with("success", __("User history updated.")); + } + + public function destroy(UserHistory $history): RedirectResponse + { + $this->authorize("manageUsers"); + + $history->delete(); + + return redirect() + ->back() + ->with("success", __("User history deleted.")); + } +} diff --git a/app/Http/Requests/UserHistoryRequest.php b/app/Http/Requests/UserHistoryRequest.php new file mode 100644 index 00000000..c7e3f93f --- /dev/null +++ b/app/Http/Requests/UserHistoryRequest.php @@ -0,0 +1,35 @@ + ["required", "date"], + "to" => ["nullable", "date", "after:from", "required_if:type," . UserHistoryType::MedicalExam->value . "," . UserHistoryType::OhsTraining->value], + "comment" => ["nullable", "string", "max:255"], + "type" => ["required", new Enum(UserHistoryType::class)], + "employmentForm" => [new Enum(EmploymentForm::class), "required_if:type," . UserHistoryType::Employment->value], + ]; + } + + public function data(): array + { + return [ + "from" => $this->get("from"), + "to" => $this->get("to"), + "type" => $this->get("type"), + "employment_form" => $this->get("type") === UserHistoryType::Employment->value ? $this->get("employmentForm") : null, + "comment" => $this->get("comment"), + ]; + } +} diff --git a/app/Http/Requests/UserRequest.php b/app/Http/Requests/UserRequest.php index de04008c..f547cd29 100644 --- a/app/Http/Requests/UserRequest.php +++ b/app/Http/Requests/UserRequest.php @@ -24,8 +24,6 @@ public function rules(): array "employmentDate" => ["required", "date_format:Y-m-d"], "birthday" => ["required", "date_format:Y-m-d", "before:today"], "slackId" => [], - "nextMedicalExamDate" => ["nullable", "after:lastMedicalExamDate"], - "nextOhsTrainingDate" => ["nullable", "after:lastOhsTrainingDate"], ]; } @@ -47,10 +45,6 @@ public function profileData(): array "employment_date" => $this->get("employmentDate"), "birthday" => $this->get("birthday"), "slack_id" => $this->get("slackId"), - "last_medical_exam_date" => $this->get("lastMedicalExamDate"), - "next_medical_exam_date" => $this->get("nextMedicalExamDate"), - "last_ohs_training_date" => $this->get("lastOhsTrainingDate"), - "next_ohs_training_date" => $this->get("nextOhsTrainingDate"), ]; } } diff --git a/app/Http/Resources/UserHistoryResource.php b/app/Http/Resources/UserHistoryResource.php new file mode 100644 index 00000000..52bb80b9 --- /dev/null +++ b/app/Http/Resources/UserHistoryResource.php @@ -0,0 +1,27 @@ + $this->id, + "from" => $this->from->format("d.m.Y"), + "to" => $this->to?->format("d.m.Y"), + "type" => $this->type->value, + "typeLabel" => $this->type->label(), + "employmentFormLabel" => $this->employment_form?->label(), + "employmentForm" => $this->employment_form?->value, + "comment" => $this->comment, + "userId" => $this->user_id, + ]; + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 8360cfa5..52b2f1ed 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -12,6 +12,9 @@ class UserResource extends JsonResource public function toArray($request): array { + $lastMedicalExam = $this->lastMedicalExam(); + $lastOhsTraining = $this->lastOhsTraining(); + return [ "id" => $this->id, "name" => $this->profile->full_name, @@ -23,10 +26,10 @@ public function toArray($request): array "lastActiveAt" => $this->last_active_at?->toDateTimeString(), "employmentForm" => $this->profile->employment_form->label(), "employmentDate" => $this->profile->employment_date->toDisplayString(), - "lastMedicalExamDate" => $this->profile->last_medical_exam_date?->toDisplayString(), - "nextMedicalExamDate" => $this->profile->next_medical_exam_date?->toDisplayString(), - "lastOhsTrainingDate" => $this->profile->last_ohs_training_date?->toDisplayString(), - "nextOhsTrainingDate" => $this->profile->next_ohs_training_date?->toDisplayString(), + "lastMedicalExamDate" => $lastMedicalExam?->from?->toDisplayString(), + "nextMedicalExamDate" => $lastMedicalExam?->to?->toDisplayString(), + "lastOhsTrainingDate" => $lastOhsTraining?->from?->toDisplayString(), + "nextOhsTrainingDate" => $lastOhsTraining?->to?->toDisplayString(), ]; } } diff --git a/app/Models/User.php b/app/Models/User.php index a6027a7e..8ff620e3 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -19,6 +19,7 @@ use Spatie\Permission\Traits\HasRoles; use Toby\Enums\EmploymentForm; use Toby\Enums\Role; +use Toby\Enums\UserHistoryType; use Toby\Notifications\Notifiable as NotifiableInterface; /** @@ -80,6 +81,27 @@ public function vacations(): HasMany return $this->hasMany(Vacation::class); } + public function histories(): HasMany + { + return $this->hasMany(UserHistory::class); + } + + public function lastMedicalExam(): ?UserHistory + { + return $this->histories() + ->where("type", UserHistoryType::MedicalExam) + ->orderBy("from", "desc") + ->first(); + } + + public function lastOhsTraining(): ?UserHistory + { + return $this->histories() + ->where("type", UserHistoryType::OhsTraining) + ->orderBy("from", "desc") + ->first(); + } + public function keys(): HasMany { return $this->hasMany(Key::class); diff --git a/app/Models/UserHistory.php b/app/Models/UserHistory.php new file mode 100644 index 00000000..740d1ef3 --- /dev/null +++ b/app/Models/UserHistory.php @@ -0,0 +1,40 @@ + "date:Y-m-d", + "to" => "date:Y-m-d", + "type" => UserHistoryType::class, + "employment_form" => EmploymentForm::class, + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/database/factories/UserHistoryFactory.php b/database/factories/UserHistoryFactory.php new file mode 100644 index 00000000..88432e92 --- /dev/null +++ b/database/factories/UserHistoryFactory.php @@ -0,0 +1,30 @@ +faker->randomElement(UserHistoryType::cases()); + + return [ + "user_id" => User::factory(), + "from" => $this->faker->date(), + "to" => $this->faker->date(), + "type" => $type, + "employment_form" => $type->is(UserHistoryType::Employment) ? $this->faker->randomElement(EmploymentForm::cases()) : null, + "comment" => $this->faker->sentence(), + ]; + } +} diff --git a/database/migrations/2024_06_14_112722_add_user_history_table.php b/database/migrations/2024_06_14_112722_add_user_history_table.php new file mode 100644 index 00000000..bb33d06e --- /dev/null +++ b/database/migrations/2024_06_14_112722_add_user_history_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId("user_id")->constrained()->cascadeOnDelete(); + $table->string("comment")->nullable(); + $table->date("from"); + $table->date("to")->nullable(); + $table->string("type"); + $table->string("employment_form")->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("user_histories"); + } +}; diff --git a/lang/pl.json b/lang/pl.json index c17ba212..fb471647 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -3,6 +3,9 @@ "employment_contract": "Umowa o pracę", "commission_contract": "Umowa zlecenie", "b2b_contract": "Kontrakt B2B", + "employment": "Zatrudnienie", + "medical_exam": "Badanie lekarskie", + "ohs_training": "Szkolenie BHP", "board_member_contract": "Członek zarządu", "vacation": "Urlop wypoczynkowy", "vacation_on_request": "Urlop na żądanie", @@ -182,5 +185,8 @@ "Labels": "Etykiety", "Is mobile": "Czy mobilny", "Assignee": "Przypisany do", - "Assigned at": "Przypisany od" + "Assigned at": "Przypisany od", + "User history created.": "Historia użytkownika utworzona.", + "User history updated.": "Historia użytkownika zaktualizowana.", + "User history deleted.": "Historia użytkownika usunięta." } diff --git a/lang/pl/validation.php b/lang/pl/validation.php index cb5e6205..0cf194ee 100644 --- a/lang/pl/validation.php +++ b/lang/pl/validation.php @@ -183,6 +183,12 @@ "birthday" => [ "before" => "Data urodzenia musi być datą wcześniejszą od dzisiaj.", ], + "employmentForm" => [ + "required_if" => "Forma zatrudnienia jest wymagana.", + ], + "to" => [ + "required_if" => "Data do jest wymagane.", + ], ], "attributes" => [ "to" => "do", @@ -200,5 +206,8 @@ "assignee" => "przydzielona osoba", "assignedAt" => "data przydzielenia", "birthday" => "data urodzenia", + "type" => "typ wpisu", + "employmentForm" => "forma zatrudnienia", + "comment" => "komentarz", ], ]; diff --git a/resources/js/Pages/UserHistory/Create.vue b/resources/js/Pages/UserHistory/Create.vue new file mode 100644 index 00000000..f87e02b7 --- /dev/null +++ b/resources/js/Pages/UserHistory/Create.vue @@ -0,0 +1,276 @@ + + + diff --git a/resources/js/Pages/UserHistory/Edit.vue b/resources/js/Pages/UserHistory/Edit.vue new file mode 100644 index 00000000..0341f3dd --- /dev/null +++ b/resources/js/Pages/UserHistory/Edit.vue @@ -0,0 +1,276 @@ + + + diff --git a/resources/js/Pages/UserHistory/Index.vue b/resources/js/Pages/UserHistory/Index.vue new file mode 100644 index 00000000..24991510 --- /dev/null +++ b/resources/js/Pages/UserHistory/Index.vue @@ -0,0 +1,166 @@ + + + diff --git a/resources/js/Pages/Users/Create.vue b/resources/js/Pages/Users/Create.vue index d4d27519..aabce72f 100644 --- a/resources/js/Pages/Users/Create.vue +++ b/resources/js/Pages/Users/Create.vue @@ -19,10 +19,6 @@ const form = useForm({ employmentDate: null, birthday: null, slackId: null, - lastMedicalExamDate: null, - nextMedicalExamDate: null, - lastOhsTrainingDate: null, - nextOhsTrainingDate: null, }) function createUser() { @@ -316,94 +312,6 @@ function createUser() {

-
- -
- -

- {{ form.errors.lastMedicalExamDate }} -

-
-
-
- -
- -

- {{ form.errors.nextMedicalExamDate }} -

-
-
-
- -
- -

- {{ form.errors.lastOhsTrainingDate }} -

-
-
-
- -
- -

- {{ form.errors.nextOhsTrainingDate }} -

-
-
-
- -
- -

- {{ form.errors.lastMedicalExamDate }} -

-
-
-
- -
- -

- {{ form.errors.nextMedicalExamDate }} -

-
-
-
- -
- -

- {{ form.errors.lastOhsTrainingDate }} -

-
-
-
- -
- -

- {{ form.errors.nextOhsTrainingDate }} -

-
-
{ Uprawnienia + + + Historia + + whereNumber("user"); Route::patch("/users/{user}/permissions", [PermissionController::class, "update"]) ->whereNumber("user"); + Route::get("/users/{user}/history", [UserHistoryController::class, "index"]) + ->whereNumber("user") + ->name("users.history"); + Route::get("/users/{user}/history/create", [UserHistoryController::class, "create"]) + ->whereNumber("user"); + Route::post("/users/{user}/history", [UserHistoryController::class, "store"]) + ->whereNumber("user"); + Route::get("/users/history/{history}", [UserHistoryController::class, "edit"]) + ->whereNumber("history"); + Route::put("/users/history/{history}", [UserHistoryController::class, "update"]) + ->whereNumber("history"); + Route::delete("/users/history/{history}", [UserHistoryController::class, "destroy"]) + ->whereNumber("history"); Route::resource("equipment-items", EquipmentController::class) ->except("show") diff --git a/tests/Feature/UserHistoryTest.php b/tests/Feature/UserHistoryTest.php new file mode 100644 index 00000000..7bae4dac --- /dev/null +++ b/tests/Feature/UserHistoryTest.php @@ -0,0 +1,109 @@ +admin = User::factory() + ->admin() + ->create(); + $this->user = User::factory() + ->create(); + $this->userHistory = $this->user->histories()->create([ + "from" => Carbon::now()->subDays(10), + "to" => Carbon::now()->subDays(5), + "type" => UserHistoryType::Employment, + "employment_form" => EmploymentForm::EmploymentContract, + ]); + } + + public function testAdminCanSeeUserHistoryList(): void + { + $this->actingAs($this->admin) + ->get("/users/{$this->user->id}/history") + ->assertInertia( + fn(Assert $page) => $page + ->component("UserHistory/Index") + ->has("history.data", 1), + ); + } + + public function testAdminCanCreateUserHistory(): void + { + $this->actingAs($this->admin) + ->post("/users/{$this->user->id}/history", [ + "from" => Carbon::now()->subDays(100)->format("Y-m-d"), + "to" => Carbon::now()->subDays(50)->format("Y-m-d"), + "type" => UserHistoryType::Employment->value, + "employmentForm" => EmploymentForm::EmploymentContract->value, + "comment" => "Test comment", + ]) + ->assertRedirect("/users/{$this->user->id}/history"); + + $this->assertDatabaseHas("user_histories", [ + "user_id" => $this->user->id, + "from" => Carbon::now()->subDays(100)->format("Y-m-d"), + "to" => Carbon::now()->subDays(50)->format("Y-m-d"), + "type" => UserHistoryType::Employment->value, + "employment_form" => EmploymentForm::EmploymentContract->value, + "comment" => "Test comment", + ]); + } + + public function testAdminCanEditUserHistory(): void + { + $this->actingAs($this->admin) + ->get("/users/history/{$this->userHistory->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("UserHistory/Edit") + ->has("history"), + ); + } + + public function testAdminCanUpdateUserHistory(): void + { + $this->actingAs($this->admin) + ->put("/users/history/{$this->userHistory->id}", [ + "from" => Carbon::now()->subDays(200)->format("Y-m-d"), + "to" => Carbon::now()->subDays(70)->format("Y-m-d"), + "type" => UserHistoryType::MedicalExam->value, + "comment" => "Test comment", + ]) + ->assertRedirect("/users/{$this->user->id}/history"); + + $this->assertDatabaseHas("user_histories", [ + "user_id" => $this->user->id, + "from" => Carbon::now()->subDays(200)->format("Y-m-d"), + "to" => Carbon::now()->subDays(70)->format("Y-m-d"), + "type" => UserHistoryType::MedicalExam->value, + "comment" => "Test comment", + ]); + } + + public function testAdminCanDeleteUserHistory(): void + { + $this->actingAs($this->admin) + ->delete("/users/history/{$this->userHistory->id}"); + + $this->assertDatabaseMissing("user_histories", [ + "id" => $this->userHistory->id, + ]); + } +}