diff --git a/app/Domain/DailySummaryRetriever.php b/app/Domain/DailySummaryRetriever.php index 7bc0a3cd..7137cc7c 100644 --- a/app/Domain/DailySummaryRetriever.php +++ b/app/Domain/DailySummaryRetriever.php @@ -89,21 +89,6 @@ public function getUpcomingRemoteDays(Carbon $date): Collection ->get(); } - /** - * @return Collection - */ - public function getBirthdays(Carbon $date): Collection - { - return User::query() - ->whereRelation( - "profile", - fn(Builder $query): Builder => $query - ->whereMonth("birthday", $date->month) - ->whereDay("birthday", $date->day), - ) - ->get(); - } - /** * @return Collection */ diff --git a/app/Domain/EmployeesMilestonesRetriever.php b/app/Domain/EmployeesMilestonesRetriever.php new file mode 100644 index 00000000..787c9117 --- /dev/null +++ b/app/Domain/EmployeesMilestonesRetriever.php @@ -0,0 +1,48 @@ + $this->getUpcomingBirthdays($searchText), + "birthday-desc" => $this->getUpcomingBirthdays($searchText, "desc"), + "seniority-asc" => $this->getSeniority($searchText), + "seniority-desc" => $this->getSeniority($searchText, "desc"), + default => User::query() + ->search($searchText) + ->orderByProfileField("last_name") + ->orderByProfileField("first_name") + ->get(), + }; + } + + public function getUpcomingBirthdays(?string $searchText, string $direction = "asc"): Collection + { + $users = User::query() + ->search($searchText) + ->get(); + + return $users->sortBy(fn(User $user): int => $user->upcomingBirthday()->diffInDays(Carbon::today()), descending: $direction !== "asc"); + } + + public function getSeniority(?string $searchText, string $direction = "asc"): Collection + { + return User::query() + ->search($searchText) + ->orderByProfileField("employment_date", $direction) + ->get(); + } +} diff --git a/app/Eloquent/Models/User.php b/app/Eloquent/Models/User.php index 3a92ff54..54f16804 100644 --- a/app/Eloquent/Models/User.php +++ b/app/Eloquent/Models/User.php @@ -108,11 +108,11 @@ public function scopeSearch(Builder $query, ?string $text): Builder ); } - public function scopeOrderByProfileField(Builder $query, string $field): Builder + public function scopeOrderByProfileField(Builder $query, string $field, string $direction = "asc"): Builder { $profileQuery = Profile::query()->select($field)->whereColumn("users.id", "profiles.user_id"); - return $query->orderBy($profileQuery); + return $query->orderBy($profileQuery, $direction); } public function scopeWithVacationLimitIn(Builder $query, YearPeriod $yearPeriod): Builder @@ -135,8 +135,12 @@ public function scopeStatus(Builder $query, ?string $status): Builder }; } - public function upcomingBirthday(): Carbon + public function upcomingBirthday(): ?Carbon { + if (!$this->profile->birthday) { + return null; + } + $today = Carbon::today(); $birthday = $this->profile->birthday->setYear($today->year); @@ -148,11 +152,37 @@ public function upcomingBirthday(): Carbon return $birthday; } + public function seniority(): ?string + { + $employmentDate = $this->profile->employment_date; + + if ($employmentDate->isFuture() || $employmentDate->isToday()) { + return null; + } + + return $employmentDate->longAbsoluteDiffForHumans(Carbon::today(), 2); + } + public function routeNotificationForSlack() { return $this->profile->slack_id; } + public function isWorkAnniversaryToday(): bool + { + $today = Carbon::now(); + + $employmentDate = $this->profile->employment_date; + + if ($employmentDate->isToday()) { + return false; + } + + $workAnniversary = $employmentDate->setYear($today->year); + + return $workAnniversary->isToday(); + } + protected static function newFactory(): UserFactory { return UserFactory::new(); diff --git a/app/Infrastructure/Http/Controllers/EmployeesMilestonesController.php b/app/Infrastructure/Http/Controllers/EmployeesMilestonesController.php new file mode 100644 index 00000000..c35977e6 --- /dev/null +++ b/app/Infrastructure/Http/Controllers/EmployeesMilestonesController.php @@ -0,0 +1,29 @@ +query("search"); + $sort = $request->query("sort"); + + $users = $employeesMilestoneRetriever->getResults($searchText, $sort); + + return inertia("EmployeesMilestones", [ + "users" => EmployeeMilestoneResource::collection($users), + "filters" => [ + "search" => $searchText, + "sort" => $sort, + ], + ]); + } +} diff --git a/app/Infrastructure/Http/Requests/UserRequest.php b/app/Infrastructure/Http/Requests/UserRequest.php index c0a4e165..5e539127 100644 --- a/app/Infrastructure/Http/Requests/UserRequest.php +++ b/app/Infrastructure/Http/Requests/UserRequest.php @@ -22,7 +22,7 @@ public function rules(): array "position" => ["required"], "employmentForm" => ["required", new Enum(EmploymentForm::class)], "employmentDate" => ["required", "date_format:Y-m-d"], - "birthday" => ["nullable", "date_format:Y-m-d"], + "birthday" => ["required", "date_format:Y-m-d", "before:today"], "slackId" => [], "nextMedicalExamDate" => ["nullable", "after:lastMedicalExamDate"], "nextOhsTrainingDate" => ["nullable", "after:lastOhsTrainingDate"], diff --git a/app/Infrastructure/Http/Resources/EmployeeMilestoneResource.php b/app/Infrastructure/Http/Resources/EmployeeMilestoneResource.php new file mode 100644 index 00000000..a3ffd3bb --- /dev/null +++ b/app/Infrastructure/Http/Resources/EmployeeMilestoneResource.php @@ -0,0 +1,36 @@ +upcomingBirthday(); + $seniority = $this->seniority(); + $isSeniorityAnniversaryToday = $this->isWorkAnniversaryToday(); + + return [ + "user" => new SimpleUserResource($this->resource), + "birthdayDisplayDate" => $upcomingBirthday?->toDisplayString(), + "birthdayRelativeDate" => $upcomingBirthday?->isToday() + ? __("today") + : $upcomingBirthday?->diffForHumans( + Carbon::today(), + ["options" => CarbonInterface::ONE_DAY_WORDS, "syntax" => CarbonInterface::DIFF_RELATIVE_TO_NOW], + ), + "isBirthdayToday" => (bool)$upcomingBirthday?->isToday(), + "seniorityDisplayDate" => $seniority, + "isWorkAnniversaryToday" => $isSeniorityAnniversaryToday, + "employmentDate" => $this->profile->employment_date->toDisplayString(), + ]; + } +} diff --git a/app/Infrastructure/Http/Resources/UserFormDataResource.php b/app/Infrastructure/Http/Resources/UserFormDataResource.php index 145a2883..2a5f0615 100644 --- a/app/Infrastructure/Http/Resources/UserFormDataResource.php +++ b/app/Infrastructure/Http/Resources/UserFormDataResource.php @@ -21,7 +21,7 @@ public function toArray($request): array "position" => $this->profile->position, "employmentForm" => $this->profile->employment_form, "employmentDate" => $this->profile->employment_date->toDateString(), - "birthday" => $this->profile->birthday?->toDateString(), + "birthday" => $this->profile->birthday->toDateString(), "slackId" => $this->profile->slack_id, "lastMedicalExamDate" => $this->profile->last_medical_exam_date?->toDateString(), "nextMedicalExamDate" => $this->profile->next_medical_exam_date?->toDateString(), diff --git a/lang/pl/validation.php b/lang/pl/validation.php index afb575da..cb5e6205 100644 --- a/lang/pl/validation.php +++ b/lang/pl/validation.php @@ -180,6 +180,9 @@ "nextMedicalExamDate" => [ "after" => "Data następnego badania lekarskiego musi być późniejsza od daty ostatniego badania lekarskiego.", ], + "birthday" => [ + "before" => "Data urodzenia musi być datą wcześniejszą od dzisiaj.", + ], ], "attributes" => [ "to" => "do", @@ -196,5 +199,6 @@ "isMobile" => "mobilny", "assignee" => "przydzielona osoba", "assignedAt" => "data przydzielenia", + "birthday" => "data urodzenia", ], ]; diff --git a/resources/js/Pages/EmployeesMilestones.vue b/resources/js/Pages/EmployeesMilestones.vue new file mode 100644 index 00000000..e549e98f --- /dev/null +++ b/resources/js/Pages/EmployeesMilestones.vue @@ -0,0 +1,237 @@ + + + diff --git a/resources/js/Shared/MainMenu.vue b/resources/js/Shared/MainMenu.vue index 3e081c99..e7c6d137 100644 --- a/resources/js/Shared/MainMenu.vue +++ b/resources/js/Shared/MainMenu.vue @@ -29,6 +29,7 @@ import { BanknotesIcon, ComputerDesktopIcon, DocumentDuplicateIcon, + CakeIcon, } from '@heroicons/vue/24/outline' import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/24/solid' @@ -102,6 +103,13 @@ const miscNavigation = computed(() => [ icon: UserGroupIcon, can: props.auth.can.manageUsers, }, + { + name: 'Jubileusze', + href: '/employees-milestones', + section: 'EmployeesMilestones', + icon: CakeIcon, + can: true, + }, { name: 'Klucze', href: '/keys', diff --git a/routes/web.php b/routes/web.php index 006c0fb7..18cc5408 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,6 +8,7 @@ use Toby\Infrastructure\Http\Controllers\BenefitController; use Toby\Infrastructure\Http\Controllers\BenefitsReportController; use Toby\Infrastructure\Http\Controllers\DashboardController; +use Toby\Infrastructure\Http\Controllers\EmployeesMilestonesController; use Toby\Infrastructure\Http\Controllers\EquipmentController; use Toby\Infrastructure\Http\Controllers\EquipmentLabelController; use Toby\Infrastructure\Http\Controllers\GoogleController; @@ -103,6 +104,9 @@ Route::post("/keys/{key}/give", [KeysController::class, "give"]); Route::post("/keys/{key}/leave-in-the-office", [KeysController::class, "leaveInTheOffice"]); + Route::get("/employees-milestones", [EmployeesMilestonesController::class, "index"]) + ->name("employees-milestones"); + Route::post("/year-periods/{yearPeriod}/select", SelectYearPeriodController::class) ->whereNumber("yearPeriod") ->name("year-periods.select"); diff --git a/tests/Feature/EmployeesMilestonesTest.php b/tests/Feature/EmployeesMilestonesTest.php new file mode 100644 index 00000000..a5d3054d --- /dev/null +++ b/tests/Feature/EmployeesMilestonesTest.php @@ -0,0 +1,102 @@ +employeesMilestonesRetriever = $this->app->make(EmployeesMilestonesRetriever::class); + } + + public function testUserCanSeeEmployeesMilestonesList(): void + { + $user = User::factory()->create(); + + User::factory()->count(9)->create(); + + $this->actingAs($user) + ->get("/employees-milestones") + ->assertOk() + ->assertInertia( + fn(Assert $page) => $page + ->component("EmployeesMilestones") + ->has("users.data", 10), + ); + } + + public function testSortingByBirthdays(): void + { + $user1 = User::factory() + ->hasProfile(["birthday" => Carbon::createFromDate(1998, 1, 1)]) + ->employee() + ->create(); + + $user2 = User::factory() + ->hasProfile(["birthday" => Carbon::createFromDate(2000, 12, 30)]) + ->employee() + ->create(); + + $user3 = User::factory() + ->hasProfile(["birthday" => Carbon::createFromDate(1997, 5, 22)]) + ->employee() + ->create(); + + $sortedUsersByUpcomingBirthday = $this->employeesMilestonesRetriever->getResults(null, "birthday-asc")->values(); + + $this->assertEquals($user1->id, $sortedUsersByUpcomingBirthday[0]->id); + $this->assertEquals($user3->id, $sortedUsersByUpcomingBirthday[1]->id); + $this->assertEquals($user2->id, $sortedUsersByUpcomingBirthday[2]->id); + + $sortedUsersByFurthestBirthday = $this->employeesMilestonesRetriever->getResults(null, "birthday-desc")->values(); + + $this->assertEquals($user2->id, $sortedUsersByFurthestBirthday[0]->id); + $this->assertEquals($user3->id, $sortedUsersByFurthestBirthday[1]->id); + $this->assertEquals($user1->id, $sortedUsersByFurthestBirthday[2]->id); + } + + public function testSortingBySeniority(): void + { + $user1 = User::factory() + ->hasProfile(["employment_date" => Carbon::createFromDate(2023, 1, 31)]) + ->employee() + ->create(); + + $user2 = User::factory() + ->hasProfile(["employment_date" => Carbon::createFromDate(2022, 1, 1)]) + ->employee() + ->create(); + + $user3 = User::factory() + ->hasProfile(["employment_date" => Carbon::createFromDate(2021, 10, 4)]) + ->employee() + ->create(); + + $sortedUsersByLongestSeniority = $this->employeesMilestonesRetriever->getResults(null, "seniority-asc")->values(); + + $this->assertEquals($user3->id, $sortedUsersByLongestSeniority[0]->id); + $this->assertEquals($user2->id, $sortedUsersByLongestSeniority[1]->id); + $this->assertEquals($user1->id, $sortedUsersByLongestSeniority[2]->id); + + $sortedUsersByShortestSeniority = $this->employeesMilestonesRetriever->getResults(null, "seniority-desc")->values(); + + $this->assertEquals($user1->id, $sortedUsersByShortestSeniority[0]->id); + $this->assertEquals($user2->id, $sortedUsersByShortestSeniority[1]->id); + $this->assertEquals($user3->id, $sortedUsersByShortestSeniority[2]->id); + } +} diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 3326be34..c52b15bf 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -101,6 +101,7 @@ public function testAdminCanCreateUser(): void "email" => "john.doe@example.com", "employmentForm" => EmploymentForm::B2bContract->value, "employmentDate" => Carbon::now()->toDateString(), + "birthday" => Carbon::create(1990, 5, 31)->toDateString(), ]) ->assertSessionHasNoErrors(); @@ -121,6 +122,7 @@ public function testAdminCanCreateUser(): void "position" => "Test position", "employment_form" => EmploymentForm::B2bContract->value, "employment_date" => Carbon::now()->toDateString(), + "birthday" => Carbon::create(1990, 5, 31)->toDateString(), ]); } @@ -137,6 +139,7 @@ public function testItCreatesProperPermissionsWhileCreatingUser(): void "email" => "john.doe@example.com", "employmentForm" => EmploymentForm::B2bContract->value, "employmentDate" => Carbon::now()->toDateString(), + "birthday" => Carbon::create(1990, 5, 31)->toDateString(), ]) ->assertSessionHasNoErrors(); @@ -165,6 +168,7 @@ public function testAdminCanEditUser(): void "last_name" => $user->profile->last_name, "employment_form" => $user->profile->employment_form->value, "employment_date" => $user->profile->employment_date->toDateString(), + "birthday" => $user->profile->birthday->toDateString(), ]); $this->actingAs($admin) @@ -176,6 +180,7 @@ public function testAdminCanEditUser(): void "position" => "Test position", "employmentForm" => EmploymentForm::B2bContract->value, "employmentDate" => Carbon::now()->toDateString(), + "birthday" => Carbon::create(1990, 5, 31)->toDateString(), ]) ->assertSessionHasNoErrors(); @@ -192,6 +197,7 @@ public function testAdminCanEditUser(): void "position" => "Test position", "employment_form" => EmploymentForm::B2bContract->value, "employment_date" => Carbon::now()->toDateString(), + "birthday" => Carbon::create(1990, 5, 31)->toDateString(), ]); } @@ -239,6 +245,7 @@ public function testChangingUserRoleUpdatesPermissions(): void "position" => "Test position", "employmentForm" => EmploymentForm::B2bContract->value, "employmentDate" => Carbon::now()->toDateString(), + "birthday" => Carbon::create(1990, 5, 31)->toDateString(), ]) ->assertSessionHasNoErrors();