Compare commits
	
		
			63 Commits
		
	
	
		
			#17-dusk-t
			...
			26220f1025
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 26220f1025 | |||
| e39827df20 | |||
| b64f2ce269 | |||
| 600a1a369f | |||
|  | 902af64833 | ||
|  | d1047e8262 | ||
| 643f546142 | |||
| 995c0b6696 | |||
| 9147b859d3 | |||
|  | 2f9ef0ff12 | ||
|  | 68e32ad930 | ||
|  | 31a6d287c8 | ||
| 77d3b4df0d | |||
| 2fb2415203 | |||
| fe639f264d | |||
|  | 4b06e6c02b | ||
| 4309e8104b | |||
|  | 3af92b2085 | ||
|  | 0bebe2ecf1 | ||
|  | a3b8b18384 | ||
|  | 431262dfb7 | ||
|  | 7154caa340 | ||
|  | 74f33d6c0b | ||
|  | 88b1543071 | ||
|  | f4d928c6ae | ||
|  | 497f47068c | ||
|  | e1f449fb52 | ||
|  | 3404bf1da8 | ||
|  | 94da727fe4 | ||
|  | 06863b854a | ||
|  | 6b2556c1da | ||
|  | c69866bb52 | ||
|  | d60dc75f99 | ||
|  | c95d08c861 | ||
|  | cc981b02b4 | ||
|  | 459b62500e | ||
|  | 4af0482a93 | ||
|  | ff8d6aade6 | ||
|  | 84403a762a | ||
|  | 6af4380fe6 | ||
|  | fa244b96cd | ||
|  | 3ab02f1df4 | ||
|  | 1ae23bd7cb | ||
|  | b52d206554 | ||
|  | 172eab162d | ||
|  | 794e8df3ea | ||
|  | b49fcadbba | ||
|  | 08421b8a69 | ||
|  | ab16af1ca9 | ||
|  | fdbc374d7e | ||
|  | 720d2c4e7b | ||
|  | 957b07b3eb | ||
|  | dcda8c6255 | ||
|  | 95f5ed44d6 | ||
|  | d8ac2bd61f | ||
|  | a0e60a3160 | ||
|  | 8c1819aa01 | ||
|  | afb1a5e9ff | ||
|  | 43870fa060 | ||
|  | 0076c04e88 | ||
|  | 287c6c19ab | ||
|  | 8a54403318 | ||
|  | 6d62c8b776 | 
							
								
								
									
										13
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								.env.example
									
									
									
									
									
								
							| @@ -57,6 +57,19 @@ DOCKER_INSTALL_XDEBUG=false | ||||
|  | ||||
| GOOGLE_CLIENT_ID= | ||||
| GOOGLE_CLIENT_SECRET= | ||||
| GOOGLE_CALENDAR_ENABLED=true | ||||
| GOOGLE_REDIRECT=http://localhost/login/google/end | ||||
| GOOGLE_CALENDAR_ID= | ||||
| LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE= | ||||
|  | ||||
| SLACK_URL=https://slack.com/api | ||||
| SLACK_ENABLED=true | ||||
| SLACK_CLIENT_TOKEN= | ||||
| SLACK_SIGNING_SECRET= | ||||
| SLACK_DEFAULT_CHANNEL="#general" | ||||
|  | ||||
| HEROKU_RELEASE_VERSION=v01 | ||||
| HEROKU_SLUG_DESCRIPTION="Dev 643f546" | ||||
| HEROKU_RELEASE_CREATED_AT=2022-07-04T18:05:46Z | ||||
| HEROKU_SLUG_COMMIT=643f546142b8a2db342f0ae46f01e61dce03e97c | ||||
| GITHUB_REPO_URL=https://github.com/blumilksoftware/toby/ | ||||
|   | ||||
| @@ -13,5 +13,8 @@ module.exports = { | ||||
|     indent: ['error', 2], | ||||
|     'vue/html-indent': ['error', 2], | ||||
|     'comma-dangle': ['error', 'always-multiline'], | ||||
|     'object-curly-spacing': ['error', 'always'], | ||||
|     'vue/require-default-prop': 0, | ||||
|     'vue/multi-word-component-names': 0, | ||||
|   }, | ||||
| } | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| * @blumilksoftware/toby | ||||
							
								
								
									
										17
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| name: Deploy | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - v* | ||||
|  | ||||
| jobs: | ||||
|   deploy: | ||||
|     runs-on: ubuntu-20.04 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: akhileshns/heroku-deploy@v3.12.12 | ||||
|         with: | ||||
|           heroku_api_key: ${{secrets.HEROKU_API_KEY}} | ||||
|           heroku_app_name: ${{secrets.HEROKU_APP_NAME}} | ||||
|           heroku_email: ${{secrets.HEROKU_EMAIL}} | ||||
							
								
								
									
										2
									
								
								.github/workflows/test-and-lint-php.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test-and-lint-php.yml
									
									
									
									
										vendored
									
									
								
							| @@ -45,7 +45,7 @@ jobs: | ||||
|         run: composer install --prefer-dist --no-interaction --no-suggest | ||||
|  | ||||
|       - name: Run PHP linter | ||||
|         run: composer ecs | ||||
|         run: composer cs | ||||
|  | ||||
|       - name: Execute tests | ||||
|         run: php artisan test --env=ci | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -15,5 +15,6 @@ Homestead.json | ||||
| Homestead.yaml | ||||
| npm-debug.log | ||||
| yarn-error.log | ||||
| google-credentials.json | ||||
| .idea/ | ||||
| .composer | ||||
|   | ||||
							
								
								
									
										3
									
								
								Procfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Procfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| web: vendor/bin/heroku-php-nginx -C environment/prod/nginx.conf public/ | ||||
| release: php artisan config:cache && php artisan route:cache && php artisan migrate --force | ||||
| worker: php artisan queue:work | ||||
| @@ -5,6 +5,9 @@ declare(strict_types=1); | ||||
| namespace Toby\Architecture; | ||||
|  | ||||
| use Illuminate\Foundation\Exceptions\Handler; | ||||
| use Inertia\Inertia; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| use Throwable; | ||||
|  | ||||
| class ExceptionHandler extends Handler | ||||
| { | ||||
| @@ -13,4 +16,36 @@ class ExceptionHandler extends Handler | ||||
|         "password", | ||||
|         "password_confirmation", | ||||
|     ]; | ||||
|     protected array $handleByInertia = [ | ||||
|         Response::HTTP_INTERNAL_SERVER_ERROR, | ||||
|         Response::HTTP_SERVICE_UNAVAILABLE, | ||||
|         Response::HTTP_TOO_MANY_REQUESTS, | ||||
|         419, // CSRF | ||||
|         Response::HTTP_NOT_FOUND, | ||||
|         Response::HTTP_FORBIDDEN, | ||||
|         Response::HTTP_UNAUTHORIZED, | ||||
|     ]; | ||||
|  | ||||
|     public function render($request, Throwable $e): Response | ||||
|     { | ||||
|         $response = parent::render($request, $e); | ||||
|  | ||||
|         if (!app()->environment("production")) { | ||||
|             return $response; | ||||
|         } | ||||
|  | ||||
|         if ($response->status() === Response::HTTP_METHOD_NOT_ALLOWED) { | ||||
|             $response->setStatusCode(Response::HTTP_NOT_FOUND); | ||||
|         } | ||||
|  | ||||
|         if (in_array($response->status(), $this->handleByInertia, true)) { | ||||
|             return Inertia::render("Error", [ | ||||
|                 "status" => $response->status(), | ||||
|             ]) | ||||
|                 ->toResponse($request) | ||||
|                 ->setStatusCode($response->status()); | ||||
|         } | ||||
|  | ||||
|         return $response; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,13 +4,24 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Architecture\Providers; | ||||
|  | ||||
| use Illuminate\Contracts\Foundation\Application; | ||||
| use Illuminate\Notifications\ChannelManager; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Facades\Notification; | ||||
| use Illuminate\Support\ServiceProvider; | ||||
| use Toby\Infrastructure\Slack\Channels\SlackApiChannel; | ||||
|  | ||||
| class AppServiceProvider extends ServiceProvider | ||||
| { | ||||
|     public function register(): void | ||||
|     { | ||||
|         Notification::resolved(function (ChannelManager $service): void { | ||||
|             $service->extend("slack", fn(Application $app): SlackApiChannel => $app->make(SlackApiChannel::class)); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public function boot(): void | ||||
|     { | ||||
|         Carbon::macro("toDisplayString", fn() => $this->translatedFormat("d.m.Y")); | ||||
|         Carbon::macro("toDisplayString", fn(): string => $this->translatedFormat("d.m.Y")); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,9 @@ namespace Toby\Architecture\Providers; | ||||
| use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; | ||||
| use Illuminate\Support\Facades\Gate; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Policies\KeyPolicy; | ||||
| use Toby\Domain\Policies\VacationRequestPolicy; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| @@ -15,6 +17,7 @@ class AuthServiceProvider extends ServiceProvider | ||||
| { | ||||
|     protected $policies = [ | ||||
|         VacationRequest::class => VacationRequestPolicy::class, | ||||
|         Key::class => KeyPolicy::class, | ||||
|     ]; | ||||
|  | ||||
|     public function boot(): void | ||||
| @@ -27,9 +30,11 @@ class AuthServiceProvider extends ServiceProvider | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         Gate::define("manageUsers", fn(User $user) => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageHolidays", fn(User $user) => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageVacationLimits", fn(User $user) => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("generateTimesheet", fn(User $user) => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageUsers", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageHolidays", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageVacationLimits", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("generateTimesheet", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("listMonthlyUsage", fn(User $user): bool => $user->role === Role::AdministrativeApprover); | ||||
|         Gate::define("manageResumes", fn(User $user): bool => $user->role === Role::TechnicalApprover); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,39 +5,8 @@ declare(strict_types=1); | ||||
| namespace Toby\Architecture\Providers; | ||||
|  | ||||
| use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; | ||||
| use Toby\Domain\Events\VacationRequestAcceptedByAdministrative; | ||||
| use Toby\Domain\Events\VacationRequestAcceptedByTechnical; | ||||
| use Toby\Domain\Events\VacationRequestApproved; | ||||
| use Toby\Domain\Events\VacationRequestCancelled; | ||||
| use Toby\Domain\Events\VacationRequestCreated; | ||||
| use Toby\Domain\Events\VacationRequestRejected; | ||||
| use Toby\Domain\Events\VacationRequestStateChanged; | ||||
| use Toby\Domain\Events\VacationRequestWaitsForAdminApproval; | ||||
| use Toby\Domain\Events\VacationRequestWaitsForTechApproval; | ||||
| use Toby\Domain\Listeners\CreateVacationRequestActivity; | ||||
| use Toby\Domain\Listeners\HandleAcceptedByAdministrativeVacationRequest; | ||||
| use Toby\Domain\Listeners\HandleAcceptedByTechnicalVacationRequest; | ||||
| use Toby\Domain\Listeners\HandleApprovedVacationRequest; | ||||
| use Toby\Domain\Listeners\HandleCancelledVacationRequest; | ||||
| use Toby\Domain\Listeners\HandleCreatedVacationRequest; | ||||
| use Toby\Domain\Listeners\SendApprovedVacationRequestNotification; | ||||
| use Toby\Domain\Listeners\SendCancelledVacationRequestNotification; | ||||
| use Toby\Domain\Listeners\SendCreatedVacationRequestNotification; | ||||
| use Toby\Domain\Listeners\SendRejectedVacationRequestNotification; | ||||
| use Toby\Domain\Listeners\SendWaitedForAdministrativeVacationRequestNotification; | ||||
| use Toby\Domain\Listeners\SendWaitedForTechnicalVacationRequestNotification; | ||||
|  | ||||
| class EventServiceProvider extends ServiceProvider | ||||
| { | ||||
|     protected $listen = [ | ||||
|         VacationRequestStateChanged::class => [CreateVacationRequestActivity::class], | ||||
|         VacationRequestCreated::class => [HandleCreatedVacationRequest::class, SendCreatedVacationRequestNotification::class], | ||||
|         VacationRequestAcceptedByTechnical::class => [HandleAcceptedByTechnicalVacationRequest::class], | ||||
|         VacationRequestAcceptedByAdministrative::class => [HandleAcceptedByAdministrativeVacationRequest::class], | ||||
|         VacationRequestApproved::class => [HandleApprovedVacationRequest::class, SendApprovedVacationRequestNotification::class], | ||||
|         VacationRequestRejected::class => [SendRejectedVacationRequestNotification::class], | ||||
|         VacationRequestCancelled::class => [HandleCancelledVacationRequest::class, SendCancelledVacationRequestNotification::class], | ||||
|         VacationRequestWaitsForTechApproval::class => [SendWaitedForTechnicalVacationRequestNotification::class], | ||||
|         VacationRequestWaitsForAdminApproval::class => [SendWaitedForAdministrativeVacationRequestNotification::class], | ||||
|     ]; | ||||
|     protected $listen = []; | ||||
| } | ||||
|   | ||||
| @@ -7,17 +7,14 @@ namespace Toby\Architecture\Providers; | ||||
| use Illuminate\Support\ServiceProvider; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
| use Toby\Eloquent\Observers\UserObserver; | ||||
| use Toby\Eloquent\Observers\VacationRequestObserver; | ||||
| use Toby\Eloquent\Observers\YearPeriodObserver; | ||||
|  | ||||
| class ObserverServiceProvider extends ServiceProvider | ||||
| { | ||||
|     public function boot(): void | ||||
|     { | ||||
|         User::observe(UserObserver::class); | ||||
|         YearPeriod::observe(YearPeriodObserver::class); | ||||
|         VacationRequest::observe(VacationRequestObserver::class); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -28,6 +28,6 @@ class RouteServiceProvider extends ServiceProvider | ||||
|  | ||||
|     protected function configureRateLimiting(): void | ||||
|     { | ||||
|         RateLimiter::for("api", fn(Request $request) => Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip())); | ||||
|         RateLimiter::for("api", fn(Request $request): Limit => Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip())); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										35
									
								
								app/Domain/Actions/CreateUserAction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/Domain/Actions/CreateUserAction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions; | ||||
|  | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
|  | ||||
| class CreateUserAction | ||||
| { | ||||
|     public function execute(array $userData, array $profileData): User | ||||
|     { | ||||
|         $user = new User($userData); | ||||
|  | ||||
|         $user->save(); | ||||
|  | ||||
|         $user->profile()->create($profileData); | ||||
|  | ||||
|         $this->createVacationLimitsFor($user); | ||||
|  | ||||
|         return $user; | ||||
|     } | ||||
|  | ||||
|     protected function createVacationLimitsFor(User $user): void | ||||
|     { | ||||
|         $yearPeriods = YearPeriod::all(); | ||||
|  | ||||
|         foreach ($yearPeriods as $yearPeriod) { | ||||
|             $user->vacationLimits()->create([ | ||||
|                 "year_period_id" => $yearPeriod->id, | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -2,22 +2,30 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace Toby\Eloquent\Observers; | ||||
| namespace Toby\Domain\Actions; | ||||
| 
 | ||||
| use Toby\Domain\PolishHolidaysRetriever; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
| 
 | ||||
| class YearPeriodObserver | ||||
| class CreateYearPeriodAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected PolishHolidaysRetriever $polishHolidaysRetriever, | ||||
|     ) {} | ||||
| 
 | ||||
|     public function created(YearPeriod $yearPeriod): void | ||||
|     public function execute(int $year): YearPeriod | ||||
|     { | ||||
|         $yearPeriod = new YearPeriod([ | ||||
|             "year" => $year, | ||||
|         ]); | ||||
| 
 | ||||
|         $yearPeriod->save(); | ||||
| 
 | ||||
|         $this->createVacationLimitsFor($yearPeriod); | ||||
|         $this->createHolidaysFor($yearPeriod); | ||||
| 
 | ||||
|         return $yearPeriod; | ||||
|     } | ||||
| 
 | ||||
|     protected function createVacationLimitsFor(YearPeriod $yearPeriod): void | ||||
							
								
								
									
										19
									
								
								app/Domain/Actions/UpdateUserAction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/Domain/Actions/UpdateUserAction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions; | ||||
|  | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class UpdateUserAction | ||||
| { | ||||
|     public function execute(User $user, array $userData, array $profileData): User | ||||
|     { | ||||
|         $user->update($userData); | ||||
|  | ||||
|         $user->profile->update($profileData); | ||||
|  | ||||
|         return $user; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class AcceptAsAdministrativeAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|         protected ApproveAction $approveAction, | ||||
|     ) {} | ||||
|  | ||||
|     public function execute(VacationRequest $vacationRequest, User $user): void | ||||
|     { | ||||
|         $this->stateManager->acceptAsAdministrative($vacationRequest, $user); | ||||
|  | ||||
|         $this->approveAction->execute($vacationRequest); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class AcceptAsTechnicalAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|         protected WaitForAdminApprovalAction $waitForAdminApprovalAction, | ||||
|         protected ApproveAction $approveAction, | ||||
|     ) {} | ||||
|  | ||||
|     public function execute(VacationRequest $vacationRequest, User $user): void | ||||
|     { | ||||
|         $this->stateManager->acceptAsTechnical($vacationRequest, $user); | ||||
|  | ||||
|         if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) { | ||||
|             $this->waitForAdminApprovalAction->execute($vacationRequest); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $this->approveAction->execute($vacationRequest); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										46
									
								
								app/Domain/Actions/VacationRequest/ApproveAction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/Domain/Actions/VacationRequest/ApproveAction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\VacationRequestStatusChangedNotification; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Infrastructure\Jobs\SendVacationRequestDaysToGoogleCalendar; | ||||
|  | ||||
| class ApproveAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|     ) {} | ||||
|  | ||||
|     public function execute(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     { | ||||
|         $this->stateManager->approve($vacationRequest, $user); | ||||
|  | ||||
|         if ($this->configRetriever->isVacation($vacationRequest->type)) { | ||||
|             SendVacationRequestDaysToGoogleCalendar::dispatch($vacationRequest); | ||||
|  | ||||
|             $this->notify($vacationRequest); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function notify(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $users = User::query() | ||||
|             ->where("id", "!=", $vacationRequest->user->id) | ||||
|             ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator]) | ||||
|             ->get(); | ||||
|  | ||||
|         foreach ($users as $user) { | ||||
|             $user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user)); | ||||
|         } | ||||
|  | ||||
|         $vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user)); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										46
									
								
								app/Domain/Actions/VacationRequest/CancelAction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/Domain/Actions/VacationRequest/CancelAction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\VacationRequestStatusChangedNotification; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Infrastructure\Jobs\ClearVacationRequestDaysInGoogleCalendar; | ||||
|  | ||||
| class CancelAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|     ) {} | ||||
|  | ||||
|     public function execute(VacationRequest $vacationRequest, User $user): void | ||||
|     { | ||||
|         $this->stateManager->cancel($vacationRequest, $user); | ||||
|  | ||||
|         ClearVacationRequestDaysInGoogleCalendar::dispatch($vacationRequest); | ||||
|  | ||||
|         if ($this->configRetriever->isVacation($vacationRequest->type)) { | ||||
|             $this->notify($vacationRequest); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function notify(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $users = User::query() | ||||
|             ->where("id", "!=", $vacationRequest->user->id) | ||||
|             ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator]) | ||||
|             ->get(); | ||||
|  | ||||
|         foreach ($users as $user) { | ||||
|             $user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user)); | ||||
|         } | ||||
|  | ||||
|         $vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user)); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										97
									
								
								app/Domain/Actions/VacationRequest/CreateAction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								app/Domain/Actions/VacationRequest/CreateAction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Illuminate\Validation\ValidationException; | ||||
| use Toby\Domain\Notifications\VacationRequestCreatedNotification; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Domain\Validation\VacationRequestValidator; | ||||
| use Toby\Domain\WorkDaysCalculator; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class CreateAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|         protected VacationRequestValidator $vacationRequestValidator, | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|         protected WorkDaysCalculator $workDaysCalculator, | ||||
|         protected WaitForTechApprovalAction $waitForTechApprovalAction, | ||||
|         protected WaitForAdminApprovalAction $waitForAdminApprovalAction, | ||||
|         protected ApproveAction $approveAction, | ||||
|     ) {} | ||||
|  | ||||
|     /** | ||||
|      * @throws ValidationException | ||||
|      */ | ||||
|     public function execute(array $data, User $creator): VacationRequest | ||||
|     { | ||||
|         $vacationRequest = $this->createVacationRequest($data, $creator); | ||||
|         $this->handleCreatedVacationRequest($vacationRequest); | ||||
|  | ||||
|         if ($this->configRetriever->isVacation($vacationRequest->type)) { | ||||
|             $this->notify($vacationRequest); | ||||
|         } | ||||
|  | ||||
|         return $vacationRequest; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws ValidationException | ||||
|      */ | ||||
|     protected function createVacationRequest(array $data, User $creator): VacationRequest | ||||
|     { | ||||
|         /** @var VacationRequest $vacationRequest */ | ||||
|         $vacationRequest = $creator->createdVacationRequests()->make($data); | ||||
|  | ||||
|         $this->vacationRequestValidator->validate($vacationRequest); | ||||
|  | ||||
|         $vacationRequest->save(); | ||||
|  | ||||
|         $days = $this->workDaysCalculator->calculateDays($vacationRequest->from, $vacationRequest->to); | ||||
|  | ||||
|         foreach ($days as $day) { | ||||
|             $vacationRequest->vacations()->create([ | ||||
|                 "date" => $day, | ||||
|                 "user_id" => $vacationRequest->user->id, | ||||
|                 "year_period_id" => $vacationRequest->yearPeriod->id, | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         $this->stateManager->markAsCreated($vacationRequest); | ||||
|  | ||||
|         return $vacationRequest; | ||||
|     } | ||||
|  | ||||
|     protected function handleCreatedVacationRequest(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         if ($vacationRequest->hasFlowSkipped()) { | ||||
|             $this->approveAction->execute($vacationRequest); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) { | ||||
|             $this->waitForTechApprovalAction->execute($vacationRequest); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) { | ||||
|             $this->waitForAdminApprovalAction->execute($vacationRequest); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $this->stateManager->approve($vacationRequest); | ||||
|     } | ||||
|  | ||||
|     protected function notify(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $vacationRequest->user->notify(new VacationRequestCreatedNotification($vacationRequest)); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										39
									
								
								app/Domain/Actions/VacationRequest/RejectAction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/Domain/Actions/VacationRequest/RejectAction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\VacationRequestStatusChangedNotification; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class RejectAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|     ) {} | ||||
|  | ||||
|     public function execute(VacationRequest $vacationRequest, User $user): void | ||||
|     { | ||||
|         $this->stateManager->reject($vacationRequest, $user); | ||||
|  | ||||
|         $this->notify($vacationRequest); | ||||
|     } | ||||
|  | ||||
|     protected function notify(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $users = User::query() | ||||
|             ->where("id", "!=", $vacationRequest->user->id) | ||||
|             ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator]) | ||||
|             ->get(); | ||||
|  | ||||
|         foreach ($users as $user) { | ||||
|             $user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user)); | ||||
|         } | ||||
|  | ||||
|         $vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user)); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\VacationRequestWaitsForApprovalNotification; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class WaitForAdminApprovalAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|         protected ApproveAction $approveAction, | ||||
|     ) {} | ||||
|  | ||||
|     public function execute(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $this->stateManager->waitForAdministrative($vacationRequest); | ||||
|  | ||||
|         if ($this->configRetriever->isVacation($vacationRequest->type)) { | ||||
|             $this->notifyAdminApprovers($vacationRequest); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function notifyAdminApprovers(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $users = User::query() | ||||
|             ->whereIn("role", [Role::AdministrativeApprover, Role::Administrator]) | ||||
|             ->get(); | ||||
|  | ||||
|         foreach ($users as $user) { | ||||
|             $user->notify(new VacationRequestWaitsForApprovalNotification($vacationRequest, $user)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\VacationRequestWaitsForApprovalNotification; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class WaitForTechApprovalAction | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|         protected ApproveAction $approveAction, | ||||
|     ) {} | ||||
|  | ||||
|     public function execute(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $this->stateManager->waitForTechnical($vacationRequest); | ||||
|  | ||||
|         if ($this->configRetriever->isVacation($vacationRequest->type)) { | ||||
|             $this->notifyTechApprovers($vacationRequest); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function notifyTechApprovers(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $users = User::query() | ||||
|             ->whereIn("role", [Role::TechnicalApprover, Role::Administrator]) | ||||
|             ->get(); | ||||
|  | ||||
|         foreach ($users as $user) { | ||||
|             $user->notify(new VacationRequestWaitsForApprovalNotification($vacationRequest, $user)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -5,7 +5,6 @@ declare(strict_types=1); | ||||
| namespace Toby\Domain; | ||||
|  | ||||
| use Carbon\CarbonPeriod; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Eloquent\Helpers\YearPeriodRetriever; | ||||
| @@ -44,6 +43,7 @@ class CalendarGenerator | ||||
|                 "isWeekend" => $day->isWeekend(), | ||||
|                 "isHoliday" => $holidays->contains($day), | ||||
|                 "vacations" => $vacationsForDay->pluck("user_id"), | ||||
|                 "vacationTypes" => $vacationsForDay->pluck("vacationRequest.type", "user_id"), | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
| @@ -54,8 +54,9 @@ class CalendarGenerator | ||||
|     { | ||||
|         return Vacation::query() | ||||
|             ->whereBetween("date", [$period->start, $period->end]) | ||||
|             ->whereRelation("vacationRequest", fn(Builder $query) => $query->states(VacationRequestStatesRetriever::successStates())) | ||||
|             ->approved() | ||||
|             ->with("vacationRequest") | ||||
|             ->get() | ||||
|             ->groupBy(fn(Vacation $vacation) => $vacation->date->toDateString()); | ||||
|             ->groupBy(fn(Vacation $vacation): string => $vacation->date->toDateString()); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										49
									
								
								app/Domain/DailySummaryRetriever.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/Domain/DailySummaryRetriever.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain; | ||||
|  | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\VacationType; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\Vacation; | ||||
|  | ||||
| class DailySummaryRetriever | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|     ) {} | ||||
|  | ||||
|     public function getAbsences(Carbon $date): Collection | ||||
|     { | ||||
|         return Vacation::query() | ||||
|             ->with(["user", "vacationRequest"]) | ||||
|             ->whereDate("date", $date) | ||||
|             ->approved() | ||||
|             ->whereTypes( | ||||
|                 VacationType::all()->filter(fn(VacationType $type): bool => $this->configRetriever->isVacation($type)), | ||||
|             ) | ||||
|             ->get(); | ||||
|     } | ||||
|  | ||||
|     public function getRemoteDays(Carbon $date): Collection | ||||
|     { | ||||
|         return Vacation::query() | ||||
|             ->with(["user", "vacationRequest"]) | ||||
|             ->whereDate("date", $date) | ||||
|             ->approved() | ||||
|             ->whereTypes( | ||||
|                 VacationType::all()->filter(fn(VacationType $type): bool => !$this->configRetriever->isVacation($type)), | ||||
|             ) | ||||
|             ->get(); | ||||
|     } | ||||
|  | ||||
|     public function getBirthdays(Carbon $date): Collection | ||||
|     { | ||||
|         return User::query() | ||||
|             ->whereRelation("profile", "birthday", $date) | ||||
|             ->get(); | ||||
|     } | ||||
| } | ||||
| @@ -21,7 +21,7 @@ enum EmploymentForm: string | ||||
|         $cases = collect(EmploymentForm::cases()); | ||||
|  | ||||
|         return $cases->map( | ||||
|             fn(EmploymentForm $enum) => [ | ||||
|             fn(EmploymentForm $enum): array => [ | ||||
|                 "label" => $enum->label(), | ||||
|                 "value" => $enum->value, | ||||
|             ], | ||||
|   | ||||
| @@ -21,7 +21,7 @@ enum Role: string | ||||
|         $cases = collect(Role::cases()); | ||||
|  | ||||
|         return $cases->map( | ||||
|             fn(Role $enum) => [ | ||||
|             fn(Role $enum): array => [ | ||||
|                 "label" => $enum->label(), | ||||
|                 "value" => $enum->value, | ||||
|             ], | ||||
|   | ||||
| @@ -4,6 +4,8 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Enums; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| enum VacationType: string | ||||
| { | ||||
|     case Vacation = "vacation"; | ||||
| @@ -15,6 +17,8 @@ enum VacationType: string | ||||
|     case Volunteering = "volunteering_vacation"; | ||||
|     case TimeInLieu = "time_in_lieu"; | ||||
|     case Sick = "sick_vacation"; | ||||
|     case Absence = "absence"; | ||||
|     case HomeOffice = "home_office"; | ||||
|  | ||||
|     public function label(): string | ||||
|     { | ||||
| @@ -23,13 +27,18 @@ enum VacationType: string | ||||
|  | ||||
|     public static function casesToSelect(): array | ||||
|     { | ||||
|         $cases = collect(VacationType::cases()); | ||||
|         $cases = VacationType::all(); | ||||
|  | ||||
|         return $cases->map( | ||||
|             fn(VacationType $enum) => [ | ||||
|             fn(VacationType $enum): array => [ | ||||
|                 "label" => $enum->label(), | ||||
|                 "value" => $enum->value, | ||||
|             ], | ||||
|         )->toArray(); | ||||
|     } | ||||
|  | ||||
|     public static function all(): Collection | ||||
|     { | ||||
|         return new Collection(VacationType::cases()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestAcceptedByAdministrative | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestAcceptedByTechnical | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestApproved | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestCancelled | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestCreated | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestRejected | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,24 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Domain\States\VacationRequest\VacationRequestState; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestStateChanged | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|         public ?VacationRequestState $from, | ||||
|         public VacationRequestState $to, | ||||
|         public ?User $user = null, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestWaitsForAdminApproval | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Events; | ||||
|  | ||||
| use Illuminate\Foundation\Events\Dispatchable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestWaitsForTechApproval | ||||
| { | ||||
|     use Dispatchable; | ||||
|     use SerializesModels; | ||||
|  | ||||
|     public function __construct( | ||||
|         public VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Toby\Domain\Events\VacationRequestStateChanged; | ||||
|  | ||||
| class CreateVacationRequestActivity | ||||
| { | ||||
|     public function handle(VacationRequestStateChanged $event): void | ||||
|     { | ||||
|         $event->vacationRequest->activities()->create([ | ||||
|             "from" => $event->from, | ||||
|             "to" => $event->to, | ||||
|             "user_id" => $event->user?->id, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Toby\Domain\Events\VacationRequestAcceptedByAdministrative; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
|  | ||||
| class HandleAcceptedByAdministrativeVacationRequest | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestAcceptedByAdministrative $event): void | ||||
|     { | ||||
|         $this->stateManager->approve($event->vacationRequest); | ||||
|     } | ||||
| } | ||||
| @@ -1,30 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Toby\Domain\Events\VacationRequestAcceptedByTechnical; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
|  | ||||
| class HandleAcceptedByTechnicalVacationRequest | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestAcceptedByTechnical $event): void | ||||
|     { | ||||
|         $vacationRequest = $event->vacationRequest; | ||||
|  | ||||
|         if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) { | ||||
|             $this->stateManager->waitForAdministrative($vacationRequest); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $this->stateManager->approve($vacationRequest); | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Toby\Domain\Events\VacationRequestApproved; | ||||
| use Toby\Infrastructure\Jobs\SendVacationRequestDaysToGoogleCalendar; | ||||
|  | ||||
| class HandleApprovedVacationRequest | ||||
| { | ||||
|     public function handle(VacationRequestApproved $event): void | ||||
|     { | ||||
|         SendVacationRequestDaysToGoogleCalendar::dispatch($event->vacationRequest); | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Toby\Domain\Events\VacationRequestCancelled; | ||||
| use Toby\Infrastructure\Jobs\ClearVacationRequestDaysInGoogleCalendar; | ||||
|  | ||||
| class HandleCancelledVacationRequest | ||||
| { | ||||
|     public function handle(VacationRequestCancelled $event): void | ||||
|     { | ||||
|         ClearVacationRequestDaysInGoogleCalendar::dispatch($event->vacationRequest); | ||||
|     } | ||||
| } | ||||
| @@ -1,42 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Toby\Domain\Events\VacationRequestCreated; | ||||
| use Toby\Domain\VacationRequestStateManager; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
|  | ||||
| class HandleCreatedVacationRequest | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestCreated $event): void | ||||
|     { | ||||
|         $vacationRequest = $event->vacationRequest; | ||||
|  | ||||
|         if ($vacationRequest->hasFlowSkipped()) { | ||||
|             $this->stateManager->approve($vacationRequest); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) { | ||||
|             $this->stateManager->waitForTechnical($vacationRequest); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) { | ||||
|             $this->stateManager->waitForAdministrative($vacationRequest); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $this->stateManager->approve($vacationRequest); | ||||
|     } | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Events\VacationRequestApproved; | ||||
| use Toby\Domain\Notifications\VacationRequestApprovedNotification; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class SendApprovedVacationRequestNotification | ||||
| { | ||||
|     public function __construct( | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestApproved $event): void | ||||
|     { | ||||
|         foreach ($this->getUsersForNotifications() as $user) { | ||||
|             $user->notify(new VacationRequestApprovedNotification($event->vacationRequest, $user)); | ||||
|         } | ||||
|  | ||||
|         $event->vacationRequest->user->notify(new VacationRequestApprovedNotification($event->vacationRequest, $event->vacationRequest->user)); | ||||
|     } | ||||
|  | ||||
|     protected function getUsersForNotifications(): Collection | ||||
|     { | ||||
|         return User::query() | ||||
|             ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover]) | ||||
|             ->get(); | ||||
|     } | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Events\VacationRequestCancelled; | ||||
| use Toby\Domain\Notifications\VacationRequestCancelledNotification; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class SendCancelledVacationRequestNotification | ||||
| { | ||||
|     public function __construct( | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestCancelled $event): void | ||||
|     { | ||||
|         foreach ($this->getUsersForNotifications() as $user) { | ||||
|             $user->notify(new VacationRequestCancelledNotification($event->vacationRequest, $user)); | ||||
|         } | ||||
|  | ||||
|         $event->vacationRequest->user->notify(new VacationRequestCancelledNotification($event->vacationRequest, $event->vacationRequest->user)); | ||||
|     } | ||||
|  | ||||
|     protected function getUsersForNotifications(): Collection | ||||
|     { | ||||
|         return User::query() | ||||
|             ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover]) | ||||
|             ->get(); | ||||
|     } | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Toby\Domain\Events\VacationRequestCreated; | ||||
| use Toby\Domain\Notifications\VacationRequestCreatedNotification; | ||||
| use Toby\Domain\Notifications\VacationRequestCreatedOnEmployeeBehalf; | ||||
|  | ||||
| class SendCreatedVacationRequestNotification | ||||
| { | ||||
|     public function __construct( | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestCreated $event): void | ||||
|     { | ||||
|         $vacationRequest = $event->vacationRequest; | ||||
|  | ||||
|         if ($vacationRequest->creator->is($vacationRequest->user)) { | ||||
|             $vacationRequest->user->notify(new VacationRequestCreatedNotification($vacationRequest)); | ||||
|         } else { | ||||
|             $vacationRequest->user->notify(new VacationRequestCreatedOnEmployeeBehalf($vacationRequest)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Events\VacationRequestRejected; | ||||
| use Toby\Domain\Notifications\VacationRequestRejectedNotification; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class SendRejectedVacationRequestNotification | ||||
| { | ||||
|     public function __construct( | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestRejected $event): void | ||||
|     { | ||||
|         foreach ($this->getUsersForNotifications() as $user) { | ||||
|             $user->notify(new VacationRequestRejectedNotification($event->vacationRequest, $user)); | ||||
|         } | ||||
|  | ||||
|         $event->vacationRequest->user->notify(new VacationRequestRejectedNotification($event->vacationRequest, $event->vacationRequest->user)); | ||||
|     } | ||||
|  | ||||
|     protected function getUsersForNotifications(): Collection | ||||
|     { | ||||
|         return User::query() | ||||
|             ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover]) | ||||
|             ->get(); | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Events\VacationRequestWaitsForAdminApproval; | ||||
| use Toby\Domain\Notifications\VacationRequestWaitsForAdminApprovalNotification; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class SendWaitedForAdministrativeVacationRequestNotification | ||||
| { | ||||
|     public function __construct( | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestWaitsForAdminApproval $event): void | ||||
|     { | ||||
|         foreach ($this->getUsersForNotifications() as $user) { | ||||
|             $user->notify(new VacationRequestWaitsForAdminApprovalNotification($event->vacationRequest, $user)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function getUsersForNotifications(): Collection | ||||
|     { | ||||
|         return User::query() | ||||
|             ->where("role", [Role::AdministrativeApprover]) | ||||
|             ->get(); | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Listeners; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Events\VacationRequestWaitsForTechApproval; | ||||
| use Toby\Domain\Notifications\VacationRequestWaitsForTechApprovalNotification; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class SendWaitedForTechnicalVacationRequestNotification | ||||
| { | ||||
|     public function __construct( | ||||
|     ) {} | ||||
|  | ||||
|     public function handle(VacationRequestWaitsForTechApproval $event): void | ||||
|     { | ||||
|         foreach ($this->getUsersForNotifications() as $user) { | ||||
|             $user->notify(new VacationRequestWaitsForTechApprovalNotification($event->vacationRequest, $user)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function getUsersForNotifications(): Collection | ||||
|     { | ||||
|         return User::query() | ||||
|             ->where("role", [Role::TechnicalApprover]) | ||||
|             ->get(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								app/Domain/Notifications/Channels.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/Domain/Notifications/Channels.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| class Channels | ||||
| { | ||||
|     public const MAIL = "mail"; | ||||
|     public const SLACK = "slack"; | ||||
| } | ||||
							
								
								
									
										44
									
								
								app/Domain/Notifications/KeyHasBeenGivenNotification.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/Domain/Notifications/KeyHasBeenGivenNotification.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| use Illuminate\Bus\Queueable; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Infrastructure\Slack\Elements\SlackMessage; | ||||
|  | ||||
| class KeyHasBeenGivenNotification extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
|  | ||||
|     public function __construct( | ||||
|         protected User $sender, | ||||
|         protected User $recipient, | ||||
|     ) {} | ||||
|  | ||||
|     public function via(): array | ||||
|     { | ||||
|         return [Channels::SLACK]; | ||||
|     } | ||||
|  | ||||
|     public function toSlack(Notifiable $notifiable): SlackMessage | ||||
|     { | ||||
|         return (new SlackMessage()) | ||||
|             ->text(__(":sender gives key no :key to :recipient", [ | ||||
|                 "sender" => $this->getName($this->sender), | ||||
|                 "recipient" => $this->getName($this->recipient), | ||||
|                 "key" => $notifiable->id, | ||||
|             ])); | ||||
|     } | ||||
|  | ||||
|     protected function getName(User $user): string | ||||
|     { | ||||
|         if ($user->profile->slack_id !== null) { | ||||
|             return "<@{$user->profile->slack_id}>"; | ||||
|         } | ||||
|  | ||||
|         return $user->profile->full_name; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								app/Domain/Notifications/KeyHasBeenTakenNotification.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/Domain/Notifications/KeyHasBeenTakenNotification.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| use Illuminate\Bus\Queueable; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Infrastructure\Slack\Elements\SlackMessage; | ||||
|  | ||||
| class KeyHasBeenTakenNotification extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
|  | ||||
|     public function __construct( | ||||
|         protected User $recipient, | ||||
|         protected User $sender, | ||||
|     ) {} | ||||
|  | ||||
|     public function via(): array | ||||
|     { | ||||
|         return [Channels::SLACK]; | ||||
|     } | ||||
|  | ||||
|     public function toSlack(Notifiable $notifiable): SlackMessage | ||||
|     { | ||||
|         return (new SlackMessage()) | ||||
|             ->text(__(":recipient takes key no :key from :sender", [ | ||||
|                 "recipient" => $this->getName($this->recipient), | ||||
|                 "sender" => $this->getName($this->sender), | ||||
|                 "key" => $notifiable->id, | ||||
|             ])); | ||||
|     } | ||||
|  | ||||
|     protected function getName(User $user): string | ||||
|     { | ||||
|         if ($user->profile->slack_id !== null) { | ||||
|             return "<@{$user->profile->slack_id}>"; | ||||
|         } | ||||
|  | ||||
|         return $user->profile->full_name; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								app/Domain/Notifications/Notifiable.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/Domain/Notifications/Notifiable.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| interface Notifiable | ||||
| { | ||||
|     public function notify($instance); | ||||
| } | ||||
| @@ -9,6 +9,7 @@ use Illuminate\Notifications\Messages\MailMessage; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use InvalidArgumentException; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Infrastructure\Slack\Elements\SlackMessage; | ||||
|  | ||||
| class VacationRequestCreatedNotification extends Notification | ||||
| { | ||||
| @@ -20,7 +21,16 @@ class VacationRequestCreatedNotification extends Notification | ||||
|  | ||||
|     public function via(): array | ||||
|     { | ||||
|         return ["mail"]; | ||||
|         return [Channels::MAIL, Channels::SLACK]; | ||||
|     } | ||||
|  | ||||
|     public function toSlack(): SlackMessage | ||||
|     { | ||||
|         $url = route("vacation.requests.show", ["vacationRequest" => $this->vacationRequest->id]); | ||||
|         $seeDetails = __("See details"); | ||||
|  | ||||
|         return (new SlackMessage()) | ||||
|             ->text("{$this->buildDescription()}\n <${url}|${seeDetails}>"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -39,33 +49,64 @@ class VacationRequestCreatedNotification extends Notification | ||||
|  | ||||
|     protected function buildMailMessage(string $url): MailMessage | ||||
|     { | ||||
|         $user = $this->vacationRequest->user->first_name; | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $user = $this->vacationRequest->user->profile->first_name; | ||||
|         $type = $this->vacationRequest->type->label(); | ||||
|         $from = $this->vacationRequest->from->toDisplayString(); | ||||
|         $to = $this->vacationRequest->to->toDisplayString(); | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|         $appName = config("app.name"); | ||||
|  | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|             ->greeting( | ||||
|                 __("Hi :user!", [ | ||||
|                     "user" => $user, | ||||
|             ])) | ||||
|             ->subject(__("Vacation request :title has been created", [ | ||||
|                 "title" => $title, | ||||
|             ])) | ||||
|             ->line(__("The vacation request :title has been created correctly in the :appName.", [ | ||||
|                 "title" => $title, | ||||
|                 "appName" => $appName, | ||||
|             ])) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 ]), | ||||
|             ) | ||||
|             ->subject($this->buildSubject()) | ||||
|             ->line($this->buildDescription()) | ||||
|             ->line( | ||||
|                 __("Vacation type: :type", [ | ||||
|                     "type" => $type, | ||||
|             ])) | ||||
|             ->line(__("From :from to :to (number of days: :days)", [ | ||||
|                 ]), | ||||
|             ) | ||||
|             ->line( | ||||
|                 __("From :from to :to (number of days: :days)", [ | ||||
|                     "from" => $from, | ||||
|                     "to" => $to, | ||||
|                     "days" => $days, | ||||
|             ])) | ||||
|                 ]), | ||||
|             ) | ||||
|             ->action(__("Click here for details"), $url); | ||||
|     } | ||||
|  | ||||
|     protected function buildSubject(): string | ||||
|     { | ||||
|         $name = $this->vacationRequest->name; | ||||
|  | ||||
|         if ($this->vacationRequest->creator()->is($this->vacationRequest->user)) { | ||||
|             return __("Vacation request :title has been created", [ | ||||
|                 "title" => $name, | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         return __("Vacation request :title has been created on your behalf", [ | ||||
|             "title" => $name, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     protected function buildDescription(): string | ||||
|     { | ||||
|         $name = $this->vacationRequest->name; | ||||
|  | ||||
|         if ($this->vacationRequest->creator()->is($this->vacationRequest->user)) { | ||||
|             return __("The vacation request :title has been created successfully.", [ | ||||
|                 "requester" => $this->vacationRequest->user->profile->full_name, | ||||
|                 "title" => $name, | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         return __("The vacation request :title has been created successfully by user :creator on your behalf.", [ | ||||
|             "title" => $this->vacationRequest->name, | ||||
|             "creator" => $this->vacationRequest->creator->profile->full_name, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,73 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| use Illuminate\Bus\Queueable; | ||||
| use Illuminate\Notifications\Messages\MailMessage; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use InvalidArgumentException; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestCreatedOnEmployeeBehalf extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
|  | ||||
|     public function __construct( | ||||
|         protected VacationRequest $vacationRequest, | ||||
|     ) {} | ||||
|  | ||||
|     public function via(): array | ||||
|     { | ||||
|         return ["mail"]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws InvalidArgumentException | ||||
|      */ | ||||
|     public function toMail(): MailMessage | ||||
|     { | ||||
|         $url = route( | ||||
|             "vacation.requests.show", | ||||
|             [ | ||||
|                 "vacationRequest" => $this->vacationRequest, | ||||
|             ], | ||||
|         ); | ||||
|         return $this->buildMailMessage($url); | ||||
|     } | ||||
|  | ||||
|     protected function buildMailMessage(string $url): MailMessage | ||||
|     { | ||||
|         $creator = $this->vacationRequest->creator->fullName; | ||||
|         $user = $this->vacationRequest->user->first_name; | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $type = $this->vacationRequest->type->label(); | ||||
|         $from = $this->vacationRequest->from->toDisplayString(); | ||||
|         $to = $this->vacationRequest->to->toDisplayString(); | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|         $appName = config("app.name"); | ||||
|  | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|                 "user" => $user, | ||||
|             ])) | ||||
|             ->subject(__("Vacation request :title has been created on your behalf", [ | ||||
|                 "title" => $title, | ||||
|             ])) | ||||
|             ->line(__("The vacation request :title has been created correctly by user :creator on your behalf in the :appName.", [ | ||||
|                 "title" => $title, | ||||
|                 "appName" => $appName, | ||||
|                 "creator" => $creator, | ||||
|             ])) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 "type" => $type, | ||||
|             ])) | ||||
|             ->line(__("From :from to :to (number of days: :days)", [ | ||||
|                 "from" => $from, | ||||
|                 "to" => $to, | ||||
|                 "days" => $days, | ||||
|             ])) | ||||
|             ->action(__("Click here for details"), $url); | ||||
|     } | ||||
| } | ||||
| @@ -1,74 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| use Illuminate\Bus\Queueable; | ||||
| use Illuminate\Notifications\Messages\MailMessage; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use InvalidArgumentException; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestRejectedNotification extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
|  | ||||
|     public function __construct( | ||||
|         protected VacationRequest $vacationRequest, | ||||
|         protected User $user, | ||||
|     ) {} | ||||
|  | ||||
|     public function via(): array | ||||
|     { | ||||
|         return ["mail"]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws InvalidArgumentException | ||||
|      */ | ||||
|     public function toMail(): MailMessage | ||||
|     { | ||||
|         $url = route( | ||||
|             "vacation.requests.show", | ||||
|             [ | ||||
|                 "vacationRequest" => $this->vacationRequest, | ||||
|             ], | ||||
|         ); | ||||
|  | ||||
|         return $this->buildMailMessage($url); | ||||
|     } | ||||
|  | ||||
|     protected function buildMailMessage(string $url): MailMessage | ||||
|     { | ||||
|         $user = $this->user->first_name; | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $type = $this->vacationRequest->type->label(); | ||||
|         $from = $this->vacationRequest->from->toDisplayString(); | ||||
|         $to = $this->vacationRequest->to->toDisplayString(); | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|         $requester = $this->vacationRequest->user->fullName; | ||||
|  | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|                 "user" => $user, | ||||
|             ])) | ||||
|             ->subject(__("Vacation request :title has been rejected", [ | ||||
|                 "title" => $title, | ||||
|             ])) | ||||
|             ->line(__("The vacation request :title for user :requester has been rejected.", [ | ||||
|                 "title" => $title, | ||||
|                 "requester" => $requester, | ||||
|             ])) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 "type" => $type, | ||||
|             ])) | ||||
|             ->line(__("From :from to :to (number of days: :days)", [ | ||||
|                 "from" => $from, | ||||
|                 "to" => $to, | ||||
|                 "days" => $days, | ||||
|             ])) | ||||
|             ->action(__("Click here for details"), $url); | ||||
|     } | ||||
| } | ||||
| @@ -10,8 +10,9 @@ use Illuminate\Notifications\Notification; | ||||
| use InvalidArgumentException; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Infrastructure\Slack\Elements\SlackMessage; | ||||
| 
 | ||||
| class VacationRequestApprovedNotification extends Notification | ||||
| class VacationRequestStatusChangedNotification extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
| 
 | ||||
| @@ -22,7 +23,16 @@ class VacationRequestApprovedNotification extends Notification | ||||
| 
 | ||||
|     public function via(): array | ||||
|     { | ||||
|         return ["mail"]; | ||||
|         return [Channels::MAIL, Channels::SLACK]; | ||||
|     } | ||||
| 
 | ||||
|     public function toSlack(): SlackMessage | ||||
|     { | ||||
|         $url = route("vacation.requests.show", ["vacationRequest" => $this->vacationRequest->id]); | ||||
|         $seeDetails = __("See details"); | ||||
| 
 | ||||
|         return (new SlackMessage()) | ||||
|             ->text("{$this->buildDescription()}\n <${url}|${seeDetails}>"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -42,25 +52,18 @@ class VacationRequestApprovedNotification extends Notification | ||||
| 
 | ||||
|     protected function buildMailMessage(string $url): MailMessage | ||||
|     { | ||||
|         $user = $this->user->first_name; | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $user = $this->user->profile->first_name; | ||||
|         $type = $this->vacationRequest->type->label(); | ||||
|         $from = $this->vacationRequest->from->toDisplayString(); | ||||
|         $to = $this->vacationRequest->to->toDisplayString(); | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|         $requester = $this->vacationRequest->user->fullName; | ||||
| 
 | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|                 "user" => $user, | ||||
|             ])) | ||||
|             ->subject(__("Vacation request :title has been approved", [ | ||||
|                 "title" => $title, | ||||
|             ])) | ||||
|             ->line(__("The vacation request :title for user :requester has been approved.", [ | ||||
|                 "title" => $title, | ||||
|                 "requester" => $requester, | ||||
|             ])) | ||||
|             ->subject($this->buildSubject()) | ||||
|             ->line($this->buildDescription()) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 "type" => $type, | ||||
|             ])) | ||||
| @@ -71,4 +74,21 @@ class VacationRequestApprovedNotification extends Notification | ||||
|             ])) | ||||
|             ->action(__("Click here for details"), $url); | ||||
|     } | ||||
| 
 | ||||
|     protected function buildSubject(): string | ||||
|     { | ||||
|         return __("Vacation request :title has been :status", [ | ||||
|             "title" => $this->vacationRequest->name, | ||||
|             "status" => $this->vacationRequest->state->label(), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     protected function buildDescription(): string | ||||
|     { | ||||
|         return __("The vacation request :title from user :requester has been :status.", [ | ||||
|             "title" => $this->vacationRequest->name, | ||||
|             "requester" => $this->vacationRequest->user->profile->full_name, | ||||
|             "status" => $this->vacationRequest->state->label(), | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
| @@ -1,74 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| use Illuminate\Bus\Queueable; | ||||
| use Illuminate\Notifications\Messages\MailMessage; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use InvalidArgumentException; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestWaitsForAdminApprovalNotification extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
|  | ||||
|     public function __construct( | ||||
|         protected VacationRequest $vacationRequest, | ||||
|         protected User $user, | ||||
|     ) {} | ||||
|  | ||||
|     public function via(): array | ||||
|     { | ||||
|         return ["mail"]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws InvalidArgumentException | ||||
|      */ | ||||
|     public function toMail(): MailMessage | ||||
|     { | ||||
|         $url = route( | ||||
|             "vacation.requests.show", | ||||
|             [ | ||||
|                 "vacationRequest" => $this->vacationRequest, | ||||
|             ], | ||||
|         ); | ||||
|  | ||||
|         return $this->buildMailMessage($url); | ||||
|     } | ||||
|  | ||||
|     protected function buildMailMessage(string $url): MailMessage | ||||
|     { | ||||
|         $user = $this->user->first_name; | ||||
|         $requester = $this->vacationRequest->user->fullName; | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $type = $this->vacationRequest->type->label(); | ||||
|         $from = $this->vacationRequest->from->toDisplayString(); | ||||
|         $to = $this->vacationRequest->to->toDisplayString(); | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|  | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|                 "user" => $user, | ||||
|             ])) | ||||
|             ->subject(__("Vacation request :title is waiting for your approval", [ | ||||
|                 "title" => $title, | ||||
|             ])) | ||||
|             ->line(__("The vacation request :title from user: :requester is waiting for your approval.", [ | ||||
|                 "title" => $title, | ||||
|                 "requester" => $requester, | ||||
|             ])) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 "type" => $type, | ||||
|             ])) | ||||
|             ->line(__("From :from to :to (number of days: :days)", [ | ||||
|                 "from" => $from, | ||||
|                 "to" => $to, | ||||
|                 "days" => $days, | ||||
|             ])) | ||||
|             ->action(__("Click here for details"), $url); | ||||
|     } | ||||
| } | ||||
| @@ -8,10 +8,12 @@ use Illuminate\Bus\Queueable; | ||||
| use Illuminate\Notifications\Messages\MailMessage; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use InvalidArgumentException; | ||||
| use Toby\Domain\States\VacationRequest\WaitingForTechnical; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Infrastructure\Slack\Elements\SlackMessage; | ||||
| 
 | ||||
| class VacationRequestCancelledNotification extends Notification | ||||
| class VacationRequestWaitsForApprovalNotification extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
| 
 | ||||
| @@ -22,7 +24,16 @@ class VacationRequestCancelledNotification extends Notification | ||||
| 
 | ||||
|     public function via(): array | ||||
|     { | ||||
|         return ["mail"]; | ||||
|         return [Channels::MAIL, Channels::SLACK]; | ||||
|     } | ||||
| 
 | ||||
|     public function toSlack(): SlackMessage | ||||
|     { | ||||
|         $url = route("vacation.requests.show", ["vacationRequest" => $this->vacationRequest->id]); | ||||
|         $seeDetails = __("See details"); | ||||
| 
 | ||||
|         return (new SlackMessage()) | ||||
|             ->text("{$this->buildDescription()}\n <${url}|${seeDetails}>"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -42,25 +53,18 @@ class VacationRequestCancelledNotification extends Notification | ||||
| 
 | ||||
|     protected function buildMailMessage(string $url): MailMessage | ||||
|     { | ||||
|         $user = $this->user->first_name; | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $user = $this->user->profile->first_name; | ||||
|         $type = $this->vacationRequest->type->label(); | ||||
|         $from = $this->vacationRequest->from->toDisplayString(); | ||||
|         $to = $this->vacationRequest->to->toDisplayString(); | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|         $requester = $this->vacationRequest->user->fullName; | ||||
| 
 | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|                 "user" => $user, | ||||
|             ])) | ||||
|             ->subject(__("Vacation request :title has been cancelled", [ | ||||
|                 "title" => $title, | ||||
|             ])) | ||||
|             ->line(__("The vacation request :title for user :requester has been cancelled.", [ | ||||
|                 "title" => $title, | ||||
|                 "requester" => $requester, | ||||
|             ])) | ||||
|             ->subject($this->buildSubject()) | ||||
|             ->line($this->buildDescription()) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 "type" => $type, | ||||
|             ])) | ||||
| @@ -71,4 +75,37 @@ class VacationRequestCancelledNotification extends Notification | ||||
|             ])) | ||||
|             ->action(__("Click here for details"), $url); | ||||
|     } | ||||
| 
 | ||||
|     protected function buildSubject(): string | ||||
|     { | ||||
|         $title = $this->vacationRequest->name; | ||||
| 
 | ||||
|         if ($this->vacationRequest->state->equals(WaitingForTechnical::class)) { | ||||
|             return __("Vacation request :title is waiting for your technical approval", [ | ||||
|                 "title" => $title, | ||||
|             ]); | ||||
|         } | ||||
| 
 | ||||
|         return __("Vacation request :title is waiting for your administrative approval", [ | ||||
|             "title" => $title, | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     protected function buildDescription(): string | ||||
|     { | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $requester = $this->vacationRequest->user->profile->full_name; | ||||
| 
 | ||||
|         if ($this->vacationRequest->state->equals(WaitingForTechnical::class)) { | ||||
|             return __("The vacation request :title from user :requester is waiting for your technical approval.", [ | ||||
|                 "title" => $title, | ||||
|                 "requester" => $requester, | ||||
|             ]); | ||||
|         } | ||||
| 
 | ||||
|         return __("The vacation request :title from user :requester is waiting for your administrative approval.", [ | ||||
|             "title" => $title, | ||||
|             "requester" => $requester, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
| @@ -1,74 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| use Illuminate\Bus\Queueable; | ||||
| use Illuminate\Notifications\Messages\MailMessage; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use InvalidArgumentException; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestWaitsForTechApprovalNotification extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
|  | ||||
|     public function __construct( | ||||
|         protected VacationRequest $vacationRequest, | ||||
|         protected User $user, | ||||
|     ) {} | ||||
|  | ||||
|     public function via(): array | ||||
|     { | ||||
|         return ["mail"]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws InvalidArgumentException | ||||
|      */ | ||||
|     public function toMail(): MailMessage | ||||
|     { | ||||
|         $url = route( | ||||
|             "vacation.requests.show", | ||||
|             [ | ||||
|                 "vacationRequest" => $this->vacationRequest, | ||||
|             ], | ||||
|         ); | ||||
|  | ||||
|         return $this->buildMailMessage($url); | ||||
|     } | ||||
|  | ||||
|     protected function buildMailMessage(string $url): MailMessage | ||||
|     { | ||||
|         $user = $this->user->first_name; | ||||
|         $requester = $this->vacationRequest->user->fullName; | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $type = $this->vacationRequest->type->label(); | ||||
|         $from = $this->vacationRequest->from->toDisplayString(); | ||||
|         $to = $this->vacationRequest->to->toDisplayString(); | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|  | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|                 "user" => $user, | ||||
|             ])) | ||||
|             ->subject(__("Vacation request :title is waiting for your approval", [ | ||||
|                 "title" => $title, | ||||
|             ])) | ||||
|             ->line(__("The vacation request :title from user: :requester is waiting for your approval.", [ | ||||
|                 "title" => $title, | ||||
|                 "requester" => $requester, | ||||
|             ])) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 "type" => $type, | ||||
|             ])) | ||||
|             ->line(__("From :from to :to (number of days: :days)", [ | ||||
|                 "from" => $from, | ||||
|                 "to" => $to, | ||||
|                 "days" => $days, | ||||
|             ])) | ||||
|             ->action(__("Click here for details"), $url); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,72 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Notifications; | ||||
|  | ||||
| use Illuminate\Bus\Queueable; | ||||
| use Illuminate\Notifications\Messages\MailMessage; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Infrastructure\Slack\Elements\SlackMessage; | ||||
| use Toby\Infrastructure\Slack\Elements\VacationRequestsAttachment; | ||||
|  | ||||
| class VacationRequestsSummaryNotification extends Notification | ||||
| { | ||||
|     use Queueable; | ||||
|  | ||||
|     public function __construct( | ||||
|         protected Carbon $day, | ||||
|         protected Collection $vacationRequests, | ||||
|     ) {} | ||||
|  | ||||
|     public function via(): array | ||||
|     { | ||||
|         return [Channels::MAIL, Channels::SLACK]; | ||||
|     } | ||||
|  | ||||
|     public function toSlack(): SlackMessage | ||||
|     { | ||||
|         return (new SlackMessage()) | ||||
|             ->text(__("Requests wait for your approval - status for day :date:", ["date" => $this->day->toDisplayString()])) | ||||
|             ->withAttachment(new VacationRequestsAttachment($this->vacationRequests)); | ||||
|     } | ||||
|  | ||||
|     public function toMail(Notifiable $notifiable): MailMessage | ||||
|     { | ||||
|         $url = route( | ||||
|             "vacation.requests.indexForApprovers", | ||||
|             [ | ||||
|                 "status" => "waiting_for_action", | ||||
|             ], | ||||
|         ); | ||||
|  | ||||
|         return $this->buildMailMessage($notifiable, $url); | ||||
|     } | ||||
|  | ||||
|     protected function buildMailMessage(Notifiable $notifiable, string $url): MailMessage | ||||
|     { | ||||
|         $user = $notifiable->profile->first_name; | ||||
|  | ||||
|         $message = (new MailMessage()) | ||||
|             ->greeting( | ||||
|                 __("Hi :user!", [ | ||||
|                     "user" => $user, | ||||
|                 ]), | ||||
|             ) | ||||
|             ->line  (__("Requests list waits for your approval - status for day :date:", ["date" => $this->day->toDisplayString()])) | ||||
|             ->subject(__("Requests wait for your approval - status for day :date:", ["date" => $this->day->toDisplayString()])); | ||||
|  | ||||
|         foreach ($this->vacationRequests as $request) { | ||||
|             $url = route("vacation.requests.show", ["vacationRequest" => $request->id]); | ||||
|  | ||||
|             $message->line( | ||||
|                 __("- [request no. :request](:url) of user :user (:startDate - :endDate)", ["request" => $request->name, "url" => $url, "user" => $request->user->profile->full_name, "startDate" => $request->from->toDisplayString(), "endDate" => $request->to->toDisplayString()]), | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return $message | ||||
|             ->action(__("Go to requests"), $url); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										26
									
								
								app/Domain/Policies/KeyPolicy.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/Domain/Policies/KeyPolicy.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Policies; | ||||
|  | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class KeyPolicy | ||||
| { | ||||
|     public function manage(User $user): bool | ||||
|     { | ||||
|         return $user->role === Role::AdministrativeApprover; | ||||
|     } | ||||
|  | ||||
|     public function give(User $user, Key $key): bool | ||||
|     { | ||||
|         if ($key->user()->is($user)) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         return $user->role === Role::AdministrativeApprover; | ||||
|     } | ||||
| } | ||||
| @@ -5,6 +5,9 @@ declare(strict_types=1); | ||||
| namespace Toby\Domain\Policies; | ||||
|  | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\States\VacationRequest\Created; | ||||
| use Toby\Domain\States\VacationRequest\WaitingForAdministrative; | ||||
| use Toby\Domain\States\VacationRequest\WaitingForTechnical; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| @@ -40,8 +43,16 @@ class VacationRequestPolicy | ||||
|         return in_array($user->role, [Role::AdministrativeApprover, Role::TechnicalApprover], true); | ||||
|     } | ||||
|  | ||||
|     public function cancel(User $user): bool | ||||
|     public function cancel(User $user, VacationRequest $vacationRequest): bool | ||||
|     { | ||||
|         if ($vacationRequest->user->is($user) && $vacationRequest->state->equals( | ||||
|             Created::class, | ||||
|             WaitingForAdministrative::class, | ||||
|             WaitingForTechnical::class, | ||||
|         )) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         return $user->role === Role::AdministrativeApprover; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class PolishHolidaysRetriever | ||||
|  | ||||
|     protected function prepareHolidays(array $holidays): Collection | ||||
|     { | ||||
|         return collect($holidays)->map(fn(Holiday $holiday) => [ | ||||
|         return collect($holidays)->map(fn(Holiday $holiday): array => [ | ||||
|             "name" => $holiday->getName([static::LANG_KEY]), | ||||
|             "date" => Carbon::createFromTimestamp($holiday->getTimestamp()), | ||||
|         ])->values(); | ||||
|   | ||||
							
								
								
									
										113
									
								
								app/Domain/ResumeGenerator.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								app/Domain/ResumeGenerator.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain; | ||||
|  | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Str; | ||||
| use PhpOffice\PhpWord\TemplateProcessor; | ||||
| use Toby\Eloquent\Models\Resume; | ||||
|  | ||||
| class ResumeGenerator | ||||
| { | ||||
|     public function generate(Resume $resume): string | ||||
|     { | ||||
|         $processor = new TemplateProcessor($this->getTemplate()); | ||||
|  | ||||
|         $processor->setValue("id", $resume->id); | ||||
|         $processor->setValue("name", $resume->user ? $resume->user->profile->full_name : $resume->name); | ||||
|  | ||||
|         $this->fillTechnologies($processor, $resume); | ||||
|         $this->fillLanguages($processor, $resume); | ||||
|         $this->fillEducation($processor, $resume); | ||||
|         $this->fillProjects($processor, $resume); | ||||
|  | ||||
|         return $processor->save(); | ||||
|     } | ||||
|  | ||||
|     public function getTemplate(): string | ||||
|     { | ||||
|         return resource_path("views/docx/resume_eng.docx"); | ||||
|     } | ||||
|  | ||||
|     protected function fillTechnologies(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         $processor->cloneBlock("technologies", 0, true, false, $this->getTechnologies($resume)); | ||||
|     } | ||||
|  | ||||
|     protected function fillLanguages(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         $processor->cloneBlock("languages", 0, true, false, $this->getLanguages($resume)); | ||||
|     } | ||||
|  | ||||
|     protected function fillEducation(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         $processor->cloneBlock("education", 0, true, false, $this->getEducation($resume)); | ||||
|     } | ||||
|  | ||||
|     protected function fillProjects(TemplateProcessor $processor, Resume $resume): void | ||||
|     { | ||||
|         $processor->cloneBlock("projects", $resume->projects->count(), true, true); | ||||
|  | ||||
|         foreach ($resume->projects as $index => $project) { | ||||
|             ++$index; | ||||
|             $processor->setValues($this->getProject($project, $index)); | ||||
|  | ||||
|             $processor->cloneBlock("project_technologies#{$index}", 0, true, false, $this->getProjectTechnologies($project, $index)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function getProject(array $project, int $index): array | ||||
|     { | ||||
|         return [ | ||||
|             "index#{$index}" => $index, | ||||
|             "start_date#{$index}" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"), | ||||
|             "end_date#{$index}" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"), | ||||
|             "description#{$index}" => $project["description"], | ||||
|             "tasks#{$index}" => $this->withNewLines($project["tasks"]), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     protected function withNewLines(string $text): string | ||||
|     { | ||||
|         return Str::replace("\n", "</w:t><w:br/><w:t>", $text); | ||||
|     } | ||||
|  | ||||
|     protected function getProjectTechnologies(array $project, int $index): array | ||||
|     { | ||||
|         $technologies = new Collection($project["technologies"] ?? []); | ||||
|  | ||||
|         return $technologies->map(fn(string $name) => [ | ||||
|             "technology#{$index}" => $name, | ||||
|         ])->all(); | ||||
|     } | ||||
|  | ||||
|     protected function getTechnologies(Resume $resume): array | ||||
|     { | ||||
|         return $resume->technologies->map(fn(array $technology): array => [ | ||||
|             "technology_name" => $technology["name"], | ||||
|             "technology_level" => __("resume.technology_levels.{$technology["level"]}"), | ||||
|         ])->all(); | ||||
|     } | ||||
|  | ||||
|     protected function getLanguages(Resume $resume): array | ||||
|     { | ||||
|         return $resume->languages->map(fn(array $language): array => [ | ||||
|             "language_name" => $language["name"], | ||||
|             "language_level" => __("resume.language_levels.{$language["level"]}"), | ||||
|         ])->all(); | ||||
|     } | ||||
|  | ||||
|     protected function getEducation(Resume $resume): array | ||||
|     { | ||||
|         return $resume->education->map(fn(array $project, int $index): array => [ | ||||
|             "start_date" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"), | ||||
|             "end_date" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"), | ||||
|             "school" => $project["school"], | ||||
|             "field_of_study" => $project["fieldOfStudy"], | ||||
|             "degree" => $project["degree"], | ||||
|         ])->all(); | ||||
|     } | ||||
| } | ||||
| @@ -36,4 +36,9 @@ abstract class VacationRequestState extends State | ||||
|                 Approved::class, | ||||
|             ], Cancelled::class); | ||||
|     } | ||||
|  | ||||
|     public function label(): string | ||||
|     { | ||||
|         return __(static::$name); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,20 +4,21 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Collection; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Maatwebsite\Excel\Concerns\WithMultipleSheets; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class TimesheetExport implements WithMultipleSheets | ||||
| { | ||||
|     protected Collection $users; | ||||
|     protected Collection $types; | ||||
|     protected Carbon $month; | ||||
|  | ||||
|     public function sheets(): array | ||||
|     { | ||||
|         return $this->users | ||||
|             ->map(fn(User $user) => new TimesheetPerUserSheet($user, $this->month)) | ||||
|             ->map(fn(User $user): TimesheetPerUserSheet => new TimesheetPerUserSheet($user, $this->month, $this->types)) | ||||
|             ->toArray(); | ||||
|     } | ||||
|  | ||||
| @@ -34,4 +35,11 @@ class TimesheetExport implements WithMultipleSheets | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function forVacationTypes(Collection $types): static | ||||
|     { | ||||
|         $this->types = $types; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,7 @@ namespace Toby\Domain; | ||||
| use Carbon\CarbonInterface; | ||||
| use Carbon\CarbonPeriod; | ||||
| use Generator; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Maatwebsite\Excel\Concerns\FromGenerator; | ||||
| @@ -25,7 +26,6 @@ use PhpOffice\PhpSpreadsheet\Style\Fill; | ||||
| use PhpOffice\PhpSpreadsheet\Style\NumberFormat; | ||||
| use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; | ||||
| use Toby\Domain\Enums\VacationType; | ||||
| use Toby\Domain\States\VacationRequest\Approved; | ||||
| use Toby\Eloquent\Models\Holiday; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\Vacation; | ||||
| @@ -41,17 +41,16 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With | ||||
|     public function __construct( | ||||
|         protected User $user, | ||||
|         protected Carbon $month, | ||||
|         protected Collection $types, | ||||
|     ) {} | ||||
|  | ||||
|     public function title(): string | ||||
|     { | ||||
|         return $this->user->fullName; | ||||
|         return $this->user->profile->full_name; | ||||
|     } | ||||
|  | ||||
|     public function headings(): array | ||||
|     { | ||||
|         $types = VacationType::cases(); | ||||
|  | ||||
|         $headings = [ | ||||
|             __("Date"), | ||||
|             __("Day of week"), | ||||
| @@ -60,7 +59,7 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With | ||||
|             __("Worked hours"), | ||||
|         ]; | ||||
|  | ||||
|         foreach ($types as $type) { | ||||
|         foreach ($this->types as $type) { | ||||
|             $headings[] = $type->label(); | ||||
|         } | ||||
|  | ||||
| @@ -188,13 +187,14 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With | ||||
|     { | ||||
|         return $user->vacations() | ||||
|             ->with("vacationRequest") | ||||
|             ->whereRelation("vacationRequest", fn(Builder $query): Builder => $query->whereIn("type", $this->types)) | ||||
|             ->whereBetween("date", [$period->start, $period->end]) | ||||
|             ->whereRelation("vacationRequest", "state", Approved::$name) | ||||
|             ->approved() | ||||
|             ->get() | ||||
|             ->groupBy( | ||||
|                 [ | ||||
|                     fn(Vacation $vacation) => $vacation->date->toDateString(), | ||||
|                     fn(Vacation $vacation) => $vacation->vacationRequest->type->value, | ||||
|                     fn(Vacation $vacation): string => $vacation->date->toDateString(), | ||||
|                     fn(Vacation $vacation): string => $vacation->vacationRequest->type->value, | ||||
|                 ], | ||||
|             ); | ||||
|     } | ||||
|   | ||||
| @@ -5,9 +5,10 @@ declare(strict_types=1); | ||||
| namespace Toby\Domain; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Collection; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\VacationType; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\Vacation; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
|  | ||||
| class UserVacationStatsRetriever | ||||
| @@ -20,24 +21,39 @@ class UserVacationStatsRetriever | ||||
|     { | ||||
|         return $user | ||||
|             ->vacations() | ||||
|             ->where("year_period_id", $yearPeriod->id) | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->whereRelation( | ||||
|                 "vacationRequest", | ||||
|                 fn(Builder $query) => $query | ||||
|                 fn(Builder $query): Builder => $query | ||||
|                     ->whereIn("type", $this->getLimitableVacationTypes()) | ||||
|                     ->states(VacationRequestStatesRetriever::successStates()), | ||||
|             ) | ||||
|             ->count(); | ||||
|     } | ||||
|  | ||||
|     public function getUsedVacationDaysByMonth(User $user, YearPeriod $yearPeriod): Collection | ||||
|     { | ||||
|         return $user->vacations() | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->whereRelation( | ||||
|                 "vacationRequest", | ||||
|                 fn(Builder $query): Builder => $query | ||||
|                     ->whereIn("type", $this->getLimitableVacationTypes()) | ||||
|                     ->states(VacationRequestStatesRetriever::successStates()), | ||||
|             ) | ||||
|             ->get() | ||||
|             ->groupBy(fn(Vacation $vacation): string => strtolower($vacation->date->englishMonth)) | ||||
|             ->map(fn(Collection $items): int => $items->count()); | ||||
|     } | ||||
|  | ||||
|     public function getPendingVacationDays(User $user, YearPeriod $yearPeriod): int | ||||
|     { | ||||
|         return $user | ||||
|             ->vacations() | ||||
|             ->where("year_period_id", $yearPeriod->id) | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->whereRelation( | ||||
|                 "vacationRequest", | ||||
|                 fn(Builder $query) => $query | ||||
|                 fn(Builder $query): Builder => $query | ||||
|                     ->whereIn("type", $this->getLimitableVacationTypes()) | ||||
|                     ->states(VacationRequestStatesRetriever::pendingStates()), | ||||
|             ) | ||||
| @@ -48,16 +64,26 @@ class UserVacationStatsRetriever | ||||
|     { | ||||
|         return $user | ||||
|             ->vacations() | ||||
|             ->where("year_period_id", $yearPeriod->id) | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->whereRelation( | ||||
|                 "vacationRequest", | ||||
|                 fn(Builder $query) => $query | ||||
|                 fn(Builder $query): Builder => $query | ||||
|                     ->whereIn("type", $this->getNotLimitableVacationTypes()) | ||||
|                     ->whereNot("type", VacationType::HomeOffice) | ||||
|                     ->states(VacationRequestStatesRetriever::successStates()), | ||||
|             ) | ||||
|             ->count(); | ||||
|     } | ||||
|  | ||||
|     public function getHomeOfficeDays(User $user, YearPeriod $yearPeriod): int | ||||
|     { | ||||
|         return $user | ||||
|             ->vacations() | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->whereRelation("vacationRequest", "type", VacationType::HomeOffice) | ||||
|             ->count(); | ||||
|     } | ||||
|  | ||||
|     public function getRemainingVacationDays(User $user, YearPeriod $yearPeriod): int | ||||
|     { | ||||
|         $limit = $this->getVacationDaysLimit($user, $yearPeriod); | ||||
| @@ -70,24 +96,24 @@ class UserVacationStatsRetriever | ||||
|     public function getVacationDaysLimit(User $user, YearPeriod $yearPeriod): int | ||||
|     { | ||||
|         $limit = $user->vacationLimits() | ||||
|             ->where("year_period_id", $yearPeriod->id) | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->first() | ||||
|             ->days; | ||||
|             ?->days; | ||||
|  | ||||
|         return $limit ?? 0; | ||||
|     } | ||||
|  | ||||
|     protected function getLimitableVacationTypes(): Collection | ||||
|     { | ||||
|         $types = new Collection(VacationType::cases()); | ||||
|         $types = VacationType::all(); | ||||
|  | ||||
|         return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type)); | ||||
|         return $types->filter(fn(VacationType $type): bool => $this->configRetriever->hasLimit($type)); | ||||
|     } | ||||
|  | ||||
|     protected function getNotLimitableVacationTypes(): Collection | ||||
|     { | ||||
|         $types = new Collection(VacationType::cases()); | ||||
|         $types = VacationType::all(); | ||||
|  | ||||
|         return $types->filter(fn(VacationType $type) => !$this->configRetriever->hasLimit($type)); | ||||
|         return $types->filter(fn(VacationType $type): bool => !$this->configRetriever->hasLimit($type)); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,17 +4,7 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain; | ||||
|  | ||||
| use Illuminate\Contracts\Auth\Factory as Auth; | ||||
| use Illuminate\Contracts\Events\Dispatcher; | ||||
| use Toby\Domain\Events\VacationRequestAcceptedByAdministrative; | ||||
| use Toby\Domain\Events\VacationRequestAcceptedByTechnical; | ||||
| use Toby\Domain\Events\VacationRequestApproved; | ||||
| use Toby\Domain\Events\VacationRequestCancelled; | ||||
| use Toby\Domain\Events\VacationRequestCreated; | ||||
| use Toby\Domain\Events\VacationRequestRejected; | ||||
| use Toby\Domain\Events\VacationRequestStateChanged; | ||||
| use Toby\Domain\Events\VacationRequestWaitsForAdminApproval; | ||||
| use Toby\Domain\Events\VacationRequestWaitsForTechApproval; | ||||
| use Toby\Domain\States\VacationRequest\AcceptedByAdministrative; | ||||
| use Toby\Domain\States\VacationRequest\AcceptedByTechnical; | ||||
| use Toby\Domain\States\VacationRequest\Approved; | ||||
| @@ -29,63 +19,47 @@ use Toby\Eloquent\Models\VacationRequest; | ||||
| class VacationRequestStateManager | ||||
| { | ||||
|     public function __construct( | ||||
|         protected Auth $auth, | ||||
|         protected Dispatcher $dispatcher, | ||||
|     ) {} | ||||
|  | ||||
|     public function markAsCreated(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     public function markAsCreated(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $this->fireStateChangedEvent($vacationRequest, null, $vacationRequest->state, $user); | ||||
|  | ||||
|         $this->dispatcher->dispatch(new VacationRequestCreated($vacationRequest)); | ||||
|         $this->createActivity($vacationRequest, null, $vacationRequest->state, $vacationRequest->creator); | ||||
|     } | ||||
|  | ||||
|     public function approve(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     { | ||||
|         $this->changeState($vacationRequest, Approved::class, $user); | ||||
|  | ||||
|         $this->dispatcher->dispatch(new VacationRequestApproved($vacationRequest)); | ||||
|     } | ||||
|  | ||||
|     public function reject(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     public function reject(VacationRequest $vacationRequest, User $user): void | ||||
|     { | ||||
|         $this->changeState($vacationRequest, Rejected::class, $user); | ||||
|         $this->dispatcher->dispatch(new VacationRequestRejected($vacationRequest)); | ||||
|     } | ||||
|  | ||||
|     public function cancel(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     public function cancel(VacationRequest $vacationRequest, User $user): void | ||||
|     { | ||||
|         $this->changeState($vacationRequest, Cancelled::class, $user); | ||||
|  | ||||
|         $this->dispatcher->dispatch(new VacationRequestCancelled($vacationRequest)); | ||||
|     } | ||||
|  | ||||
|     public function acceptAsTechnical(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     public function acceptAsTechnical(VacationRequest $vacationRequest, User $user): void | ||||
|     { | ||||
|         $this->changeState($vacationRequest, AcceptedByTechnical::class, $user); | ||||
|  | ||||
|         $this->dispatcher->dispatch(new VacationRequestAcceptedByTechnical($vacationRequest)); | ||||
|     } | ||||
|  | ||||
|     public function acceptAsAdministrative(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     public function acceptAsAdministrative(VacationRequest $vacationRequest, User $user): void | ||||
|     { | ||||
|         $this->changeState($vacationRequest, AcceptedByAdministrative::class, $user); | ||||
|  | ||||
|         $this->dispatcher->dispatch(new VacationRequestAcceptedByAdministrative($vacationRequest)); | ||||
|     } | ||||
|  | ||||
|     public function waitForTechnical(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     public function waitForTechnical(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $this->changeState($vacationRequest, WaitingForTechnical::class, $user); | ||||
|  | ||||
|         $this->dispatcher->dispatch(new VacationRequestWaitsForTechApproval($vacationRequest)); | ||||
|         $this->changeState($vacationRequest, WaitingForTechnical::class); | ||||
|     } | ||||
|  | ||||
|     public function waitForAdministrative(VacationRequest $vacationRequest, ?User $user = null): void | ||||
|     public function waitForAdministrative(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $this->changeState($vacationRequest, WaitingForAdministrative::class, $user); | ||||
|  | ||||
|         $this->dispatcher->dispatch(new VacationRequestWaitsForAdminApproval($vacationRequest)); | ||||
|         $this->changeState($vacationRequest, WaitingForAdministrative::class); | ||||
|     } | ||||
|  | ||||
|     protected function changeState(VacationRequest $vacationRequest, string $state, ?User $user = null): void | ||||
| @@ -94,16 +68,19 @@ class VacationRequestStateManager | ||||
|         $vacationRequest->state->transitionTo($state); | ||||
|         $vacationRequest->save(); | ||||
|  | ||||
|         $this->fireStateChangedEvent($vacationRequest, $previousState, $vacationRequest->state, $user); | ||||
|         $this->createActivity($vacationRequest, $previousState, $vacationRequest->state, $user); | ||||
|     } | ||||
|  | ||||
|     protected function fireStateChangedEvent( | ||||
|     protected function createActivity( | ||||
|         VacationRequest $vacationRequest, | ||||
|         ?VacationRequestState $from, | ||||
|         VacationRequestState $to, | ||||
|         ?User $user = null, | ||||
|     ): void { | ||||
|         $event = new VacationRequestStateChanged($vacationRequest, $from, $to, $user); | ||||
|         $this->dispatcher->dispatch($event); | ||||
|         $vacationRequest->activities()->create([ | ||||
|             "from" => $from, | ||||
|             "to" => $to, | ||||
|             "user_id" => $user?->id, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ declare(strict_types=1); | ||||
| namespace Toby\Domain; | ||||
|  | ||||
| use Illuminate\Contracts\Config\Repository; | ||||
| use Toby\Domain\Enums\EmploymentForm; | ||||
| use Toby\Domain\Enums\VacationType; | ||||
|  | ||||
| class VacationTypeConfigRetriever | ||||
| @@ -13,6 +14,8 @@ class VacationTypeConfigRetriever | ||||
|     public const KEY_ADMINISTRATIVE_APPROVAL = "administrative_approval"; | ||||
|     public const KEY_BILLABLE = "billable"; | ||||
|     public const KEY_HAS_LIMIT = "has_limit"; | ||||
|     public const KEY_AVAILABLE_FOR = "available_for"; | ||||
|     public const KEY_IS_VACATION = "is_vacation"; | ||||
|  | ||||
|     public function __construct( | ||||
|         protected Repository $config, | ||||
| @@ -38,6 +41,16 @@ class VacationTypeConfigRetriever | ||||
|         return $this->getConfigFor($type)[static::KEY_HAS_LIMIT]; | ||||
|     } | ||||
|  | ||||
|     public function isVacation(VacationType $type): bool | ||||
|     { | ||||
|         return $this->getConfigFor($type)[static::KEY_IS_VACATION]; | ||||
|     } | ||||
|  | ||||
|     public function isAvailableFor(VacationType $type, EmploymentForm $employmentForm): bool | ||||
|     { | ||||
|         return in_array($employmentForm, $this->getConfigFor($type)[static::KEY_AVAILABLE_FOR], true); | ||||
|     } | ||||
|  | ||||
|     protected function getConfigFor(VacationType $type): array | ||||
|     { | ||||
|         return $this->config->get("vacation_types.{$type->value}"); | ||||
|   | ||||
| @@ -5,11 +5,11 @@ declare(strict_types=1); | ||||
| namespace Toby\Domain\Validation\Rules; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Collection; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\VacationType; | ||||
| use Toby\Domain\VacationDaysCalculator; | ||||
| use Toby\Domain\VacationRequestStatesRetriever; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Domain\WorkDaysCalculator; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
| @@ -18,7 +18,7 @@ class DoesNotExceedLimitRule implements VacationRequestRule | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|         protected VacationDaysCalculator $vacationDaysCalculator, | ||||
|         protected WorkDaysCalculator $workDaysCalculator, | ||||
|     ) {} | ||||
|  | ||||
|     public function check(VacationRequest $vacationRequest): bool | ||||
| @@ -29,7 +29,9 @@ class DoesNotExceedLimitRule implements VacationRequestRule | ||||
|  | ||||
|         $limit = $this->getUserVacationLimit($vacationRequest->user, $vacationRequest->yearPeriod); | ||||
|         $vacationDays = $this->getVacationDaysWithLimit($vacationRequest->user, $vacationRequest->yearPeriod); | ||||
|         $estimatedDays = $this->vacationDaysCalculator->calculateDays($vacationRequest->yearPeriod, $vacationRequest->from, $vacationRequest->to)->count(); | ||||
|         $estimatedDays = $this->workDaysCalculator | ||||
|             ->calculateDays($vacationRequest->from, $vacationRequest->to) | ||||
|             ->count(); | ||||
|  | ||||
|         return $limit >= ($vacationDays + $estimatedDays); | ||||
|     } | ||||
| @@ -41,16 +43,19 @@ class DoesNotExceedLimitRule implements VacationRequestRule | ||||
|  | ||||
|     protected function getUserVacationLimit(User $user, YearPeriod $yearPeriod): int | ||||
|     { | ||||
|         return $user->vacationLimits()->where("year_period_id", $yearPeriod->id)->first()->days ?? 0; | ||||
|         return $user->vacationLimits() | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->first() | ||||
|             ?->days ?? 0; | ||||
|     } | ||||
|  | ||||
|     protected function getVacationDaysWithLimit(User $user, YearPeriod $yearPeriod): int | ||||
|     { | ||||
|         return $user->vacations() | ||||
|             ->where("year_period_id", $yearPeriod->id) | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->whereRelation( | ||||
|                 "vacationRequest", | ||||
|                 fn(Builder $query) => $query | ||||
|                 fn(Builder $query): Builder => $query | ||||
|                     ->whereIn("type", $this->getLimitableVacationTypes()) | ||||
|                     ->noStates(VacationRequestStatesRetriever::failedStates()), | ||||
|             ) | ||||
| @@ -59,8 +64,8 @@ class DoesNotExceedLimitRule implements VacationRequestRule | ||||
|  | ||||
|     protected function getLimitableVacationTypes(): Collection | ||||
|     { | ||||
|         $types = new Collection(VacationType::cases()); | ||||
|         $types = VacationType::all(); | ||||
|  | ||||
|         return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type)); | ||||
|         return $types->filter(fn(VacationType $type): bool => $this->configRetriever->hasLimit($type)); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,19 +4,19 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Validation\Rules; | ||||
|  | ||||
| use Toby\Domain\VacationDaysCalculator; | ||||
| use Toby\Domain\WorkDaysCalculator; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class MinimumOneVacationDayRule implements VacationRequestRule | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationDaysCalculator $vacationDaysCalculator, | ||||
|         protected WorkDaysCalculator $workDaysCalculator, | ||||
|     ) {} | ||||
|  | ||||
|     public function check(VacationRequest $vacationRequest): bool | ||||
|     { | ||||
|         return $this->vacationDaysCalculator | ||||
|             ->calculateDays($vacationRequest->yearPeriod, $vacationRequest->from, $vacationRequest->to) | ||||
|         return $this->workDaysCalculator | ||||
|             ->calculateDays($vacationRequest->from, $vacationRequest->to) | ||||
|             ->isNotEmpty(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -9,5 +9,6 @@ use Toby\Eloquent\Models\VacationRequest; | ||||
| interface VacationRequestRule | ||||
| { | ||||
|     public function check(VacationRequest $vacationRequest): bool; | ||||
|  | ||||
|     public function errorMessage(): string; | ||||
| } | ||||
|   | ||||
							
								
								
									
										31
									
								
								app/Domain/Validation/Rules/VacationTypeCanBeSelected.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/Domain/Validation/Rules/VacationTypeCanBeSelected.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Domain\Validation\Rules; | ||||
|  | ||||
| use Toby\Domain\Enums\VacationType; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationTypeCanBeSelected implements VacationRequestRule | ||||
| { | ||||
|     public function __construct( | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|     ) {} | ||||
|  | ||||
|     public function check(VacationRequest $vacationRequest): bool | ||||
|     { | ||||
|         $employmentForm = $vacationRequest->user->profile->employment_form; | ||||
|  | ||||
|         $availableTypes = VacationType::all() | ||||
|             ->filter(fn(VacationType $type): bool => $this->configRetriever->isAvailableFor($type, $employmentForm)); | ||||
|  | ||||
|         return $availableTypes->contains($vacationRequest->type); | ||||
|     } | ||||
|  | ||||
|     public function errorMessage(): string | ||||
|     { | ||||
|         return __("You cannot create vacation request of this type."); | ||||
|     } | ||||
| } | ||||
| @@ -12,6 +12,7 @@ use Toby\Domain\Validation\Rules\NoApprovedVacationRequestsInRange; | ||||
| use Toby\Domain\Validation\Rules\NoPendingVacationRequestInRange; | ||||
| use Toby\Domain\Validation\Rules\VacationRangeIsInTheSameYearRule; | ||||
| use Toby\Domain\Validation\Rules\VacationRequestRule; | ||||
| use Toby\Domain\Validation\Rules\VacationTypeCanBeSelected; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestValidator | ||||
| @@ -19,6 +20,7 @@ class VacationRequestValidator | ||||
|     protected array $rules = [ | ||||
|         VacationRangeIsInTheSameYearRule::class, | ||||
|         MinimumOneVacationDayRule::class, | ||||
|         VacationTypeCanBeSelected::class, | ||||
|         DoesNotExceedLimitRule::class, | ||||
|         NoPendingVacationRequestInRange::class, | ||||
|         NoApprovedVacationRequestsInRange::class, | ||||
|   | ||||
| @@ -9,11 +9,12 @@ use Carbon\CarbonPeriod; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
| 
 | ||||
| class VacationDaysCalculator | ||||
| class WorkDaysCalculator | ||||
| { | ||||
|     public function calculateDays(YearPeriod $yearPeriod, CarbonInterface $from, CarbonInterface $to): Collection | ||||
|     public function calculateDays(CarbonInterface $from, CarbonInterface $to): Collection | ||||
|     { | ||||
|         $period = CarbonPeriod::create($from, $to); | ||||
|         $yearPeriod = YearPeriod::findByYear($from->year); | ||||
|         $holidays = $yearPeriod->holidays()->pluck("date"); | ||||
| 
 | ||||
|         $validDays = new Collection(); | ||||
							
								
								
									
										33
									
								
								app/Eloquent/Helpers/ColorGenerator.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/Eloquent/Helpers/ColorGenerator.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Helpers; | ||||
|  | ||||
| class ColorGenerator | ||||
| { | ||||
|     public static function generate(string $text): string | ||||
|     { | ||||
|         $colors = config("colors"); | ||||
|         $hash = static::calculateHash($text); | ||||
|  | ||||
|         $index = $hash - count($colors) * floor($hash / count($colors)); | ||||
|  | ||||
|         return $colors[$index]; | ||||
|     } | ||||
|  | ||||
|     protected static function calculateHash(string $text): int | ||||
|     { | ||||
|         $hash = 0; | ||||
|  | ||||
|         if (empty($text)) { | ||||
|             return $hash; | ||||
|         } | ||||
|  | ||||
|         for ($i = 0; $i < mb_strlen($text); $i++) { | ||||
|             $hash = abs((int)(($hash << 2) - $hash) + mb_ord($text[$i])); | ||||
|         } | ||||
|  | ||||
|         return $hash; | ||||
|     } | ||||
| } | ||||
| @@ -30,22 +30,20 @@ class YearPeriodRetriever | ||||
|  | ||||
|     public function links(): array | ||||
|     { | ||||
|         $current = $this->selected(); | ||||
|         $selected = $this->selected(); | ||||
|         $current = $this->current(); | ||||
|  | ||||
|         $years = YearPeriod::query()->whereIn("year", $this->offset($current->year))->get(); | ||||
|         $navigation = $years->map(fn(YearPeriod $yearPeriod) => $this->toNavigation($yearPeriod)); | ||||
|         $years = YearPeriod::all(); | ||||
|  | ||||
|         $navigation = $years->map(fn(YearPeriod $yearPeriod): array => $this->toNavigation($yearPeriod)); | ||||
|  | ||||
|         return [ | ||||
|             "current" => $current->year, | ||||
|             "current" => $this->toNavigation($current), | ||||
|             "selected" => $this->toNavigation($selected), | ||||
|             "navigation" => $navigation->toArray(), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     protected function offset(int $year): array | ||||
|     { | ||||
|         return range($year - 2, $year + 2); | ||||
|     } | ||||
|  | ||||
|     protected function toNavigation(YearPeriod $yearPeriod): array | ||||
|     { | ||||
|         return [ | ||||
|   | ||||
| @@ -21,7 +21,6 @@ class Holiday extends Model | ||||
|     use HasFactory; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
|     protected $casts = [ | ||||
|         "date" => "date", | ||||
|     ]; | ||||
|   | ||||
							
								
								
									
										39
									
								
								app/Eloquent/Models/Key.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/Eloquent/Models/Key.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Database\Factories\KeyFactory; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| use Illuminate\Notifications\Notifiable; | ||||
| use Toby\Domain\Notifications\Notifiable as NotifiableInterface; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
|  * @property User $user | ||||
|  */ | ||||
| class Key extends Model implements NotifiableInterface | ||||
| { | ||||
|     use HasFactory; | ||||
|     use Notifiable; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
|     public function user(): BelongsTo | ||||
|     { | ||||
|         return $this->belongsTo(User::class); | ||||
|     } | ||||
|  | ||||
|     public function routeNotificationForSlack(): string | ||||
|     { | ||||
|         return config("services.slack.default_channel"); | ||||
|     } | ||||
|  | ||||
|     protected static function newFactory(): KeyFactory | ||||
|     { | ||||
|         return KeyFactory::new(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										63
									
								
								app/Eloquent/Models/Profile.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								app/Eloquent/Models/Profile.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Database\Factories\ProfileFactory; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Rackbeat\UIAvatars\HasAvatar; | ||||
| use Toby\Domain\Enums\EmploymentForm; | ||||
| use Toby\Eloquent\Helpers\ColorGenerator; | ||||
|  | ||||
| /** | ||||
|  * @property string $first_name | ||||
|  * @property string $last_name | ||||
|  * @property string $position | ||||
|  * @property EmploymentForm $employment_form | ||||
|  * @property Carbon $employment_date | ||||
|  * @property Carbon $birthday | ||||
|  */ | ||||
| class Profile extends Model | ||||
| { | ||||
|     use HasFactory; | ||||
|     use HasAvatar; | ||||
|  | ||||
|     protected $primaryKey = "user_id"; | ||||
|     protected $guarded = []; | ||||
|     protected $casts = [ | ||||
|         "employment_form" => EmploymentForm::class, | ||||
|         "employment_date" => "date", | ||||
|         "birthday" => "date", | ||||
|     ]; | ||||
|  | ||||
|     public function user(): BelongsTo | ||||
|     { | ||||
|         return $this->belongsTo(User::class); | ||||
|     } | ||||
|  | ||||
|     public function getAvatar(): string | ||||
|     { | ||||
|         return $this->getAvatarGenerator() | ||||
|             ->backgroundColor(ColorGenerator::generate($this->full_name)) | ||||
|             ->image(); | ||||
|     } | ||||
|  | ||||
|     public function getfullNameAttribute(): string | ||||
|     { | ||||
|         return "{$this->first_name} {$this->last_name}"; | ||||
|     } | ||||
|  | ||||
|     protected function getAvatarName(): string | ||||
|     { | ||||
|         return mb_substr($this->first_name, 0, 1) . mb_substr($this->last_name, 0, 1); | ||||
|     } | ||||
|  | ||||
|     protected static function newFactory(): ProfileFactory | ||||
|     { | ||||
|         return ProfileFactory::new(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										45
									
								
								app/Eloquent/Models/Resume.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/Eloquent/Models/Resume.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Database\Factories\ResumeFactory; | ||||
| use Illuminate\Database\Eloquent\Casts\AsCollection; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
|  * @property ?User $user | ||||
|  * @property string $name | ||||
|  * @property Collection $education | ||||
|  * @property Collection $languages | ||||
|  * @property Collection $technologies | ||||
|  * @property Collection $projects | ||||
|  */ | ||||
| class Resume extends Model | ||||
| { | ||||
|     use HasFactory; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|     protected $casts = [ | ||||
|         "education" => AsCollection::class, | ||||
|         "languages" => AsCollection::class, | ||||
|         "technologies" => AsCollection::class, | ||||
|         "projects" => AsCollection::class, | ||||
|     ]; | ||||
|     protected $perPage = 50; | ||||
|  | ||||
|     public function user(): BelongsTo | ||||
|     { | ||||
|         return $this->belongsTo(User::class); | ||||
|     } | ||||
|  | ||||
|     protected static function newFactory(): ResumeFactory | ||||
|     { | ||||
|         return ResumeFactory::new(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										25
									
								
								app/Eloquent/Models/Technology.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/Eloquent/Models/Technology.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Database\Factories\TechnologyFactory; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
|  * @property string $name | ||||
|  */ | ||||
| class Technology extends Model | ||||
| { | ||||
|     use HasFactory; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
|     protected static function newFactory(): TechnologyFactory | ||||
|     { | ||||
|         return TechnologyFactory::new(); | ||||
|     } | ||||
| } | ||||
| @@ -8,47 +8,50 @@ use Database\Factories\UserFactory; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Relations\HasMany; | ||||
| use Illuminate\Database\Eloquent\Relations\HasOne; | ||||
| use Illuminate\Database\Eloquent\SoftDeletes; | ||||
| use Illuminate\Foundation\Auth\User as Authenticatable; | ||||
| use Illuminate\Notifications\Notifiable; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Rackbeat\UIAvatars\HasAvatar; | ||||
| use Toby\Domain\Enums\EmploymentForm; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\Notifiable as NotifiableInterface; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
|  * @property string $first_name | ||||
|  * @property string $last_name | ||||
|  * @property string $email | ||||
|  * @property string $avatar | ||||
|  * @property string $position | ||||
|  * @property string $password | ||||
|  * @property Role $role | ||||
|  * @property EmploymentForm $employment_form | ||||
|  * @property Carbon $employment_date | ||||
|  * @property Profile $profile | ||||
|  * @property Collection $vacationLimits | ||||
|  * @property Collection $vacationRequests | ||||
|  * @property Collection $vacations | ||||
|  */ | ||||
| class User extends Authenticatable | ||||
| class User extends Authenticatable implements NotifiableInterface | ||||
| { | ||||
|     use HasFactory; | ||||
|     use Notifiable; | ||||
|     use SoftDeletes; | ||||
|     use HasAvatar; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
|     protected $casts = [ | ||||
|         "role" => Role::class, | ||||
|         "last_active_at" => "datetime", | ||||
|         "employment_form" => EmploymentForm::class, | ||||
|         "employment_date" => "date", | ||||
|     ]; | ||||
|  | ||||
|     protected $hidden = [ | ||||
|         "remember_token", | ||||
|     ]; | ||||
|     protected $with = [ | ||||
|         "profile", | ||||
|     ]; | ||||
|     protected $perPage = 50; | ||||
|  | ||||
|     public function profile(): HasOne | ||||
|     { | ||||
|         return $this->hasOne(Profile::class); | ||||
|     } | ||||
|  | ||||
|     public function vacationLimits(): HasMany | ||||
|     { | ||||
| @@ -70,30 +73,9 @@ class User extends Authenticatable | ||||
|         return $this->hasMany(Vacation::class); | ||||
|     } | ||||
|  | ||||
|     public function scopeSearch(Builder $query, ?string $text): Builder | ||||
|     public function keys(): HasMany | ||||
|     { | ||||
|         if ($text === null) { | ||||
|             return $query; | ||||
|         } | ||||
|  | ||||
|         return $query | ||||
|             ->where("first_name", "ILIKE", $text) | ||||
|             ->orWhere("last_name", "ILIKE", $text) | ||||
|             ->orWhere("email", "ILIKE", $text); | ||||
|     } | ||||
|  | ||||
|     public function getAvatar(): string | ||||
|     { | ||||
|         $colors = config("colors"); | ||||
|  | ||||
|         return $this->getAvatarGenerator() | ||||
|             ->backgroundColor($colors[strlen($this->fullname) % count($colors)]) | ||||
|             ->image(); | ||||
|     } | ||||
|  | ||||
|     public function getFullNameAttribute(): string | ||||
|     { | ||||
|         return "{$this->first_name} {$this->last_name}"; | ||||
|         return $this->hasMany(Key::class); | ||||
|     } | ||||
|  | ||||
|     public function hasRole(Role $role): bool | ||||
| @@ -101,9 +83,50 @@ class User extends Authenticatable | ||||
|         return $this->role === $role; | ||||
|     } | ||||
|  | ||||
|     protected function getAvatarNameKey(): string | ||||
|     public function hasVacationLimit(YearPeriod $yearPeriod): bool | ||||
|     { | ||||
|         return "fullName"; | ||||
|         return $this->vacationLimits() | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->whereNotNull("days") | ||||
|             ->exists(); | ||||
|     } | ||||
|  | ||||
|     public function scopeSearch(Builder $query, ?string $text): Builder | ||||
|     { | ||||
|         if ($text === null) { | ||||
|             return $query; | ||||
|         } | ||||
|  | ||||
|         return $query | ||||
|             ->where("email", "ILIKE", "%{$text}%") | ||||
|             ->orWhereRelation( | ||||
|                 "profile", | ||||
|                 fn(Builder $query): Builder => $query | ||||
|                     ->where("first_name", "ILIKE", "%{$text}%") | ||||
|                     ->orWhere("last_name", "ILIKE", "%{$text}%"), | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     public function scopeOrderByProfileField(Builder $query, string $field): Builder | ||||
|     { | ||||
|         $profileQuery = Profile::query()->select($field)->whereColumn("users.id", "profiles.user_id"); | ||||
|  | ||||
|         return $query->orderBy($profileQuery); | ||||
|     } | ||||
|  | ||||
|     public function scopeWithVacationLimitIn(Builder $query, YearPeriod $yearPeriod): Builder | ||||
|     { | ||||
|         return $query->whereRelation( | ||||
|             "vacationlimits", | ||||
|             fn(Builder $query): Builder => $query | ||||
|                 ->whereBelongsTo($yearPeriod) | ||||
|                 ->whereNotNull("days"), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function routeNotificationForSlack() | ||||
|     { | ||||
|         return $this->profile->slack_id; | ||||
|     } | ||||
|  | ||||
|     protected static function newFactory(): UserFactory | ||||
|   | ||||
| @@ -4,15 +4,17 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\VacationRequestStatesRetriever; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
|  * @property Carbon $date | ||||
|  * @property string $event_id | ||||
|  * @property User $user | ||||
|  * @property VacationRequest $vacationRequest | ||||
|  * @property YearPeriod $yearPeriod | ||||
| @@ -41,4 +43,28 @@ class Vacation extends Model | ||||
|     { | ||||
|         return $this->belongsTo(YearPeriod::class); | ||||
|     } | ||||
|  | ||||
|     public function scopeApproved(Builder $query): Builder | ||||
|     { | ||||
|         return $query->whereRelation( | ||||
|             "vacationRequest", | ||||
|             fn(Builder $query): Builder => $query->states(VacationRequestStatesRetriever::successStates()), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function scopePending(Builder $query): Builder | ||||
|     { | ||||
|         return $query->whereRelation( | ||||
|             "vacationRequest", | ||||
|             fn(Builder $query): Builder => $query->states(VacationRequestStatesRetriever::pendingStates()), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function scopeWhereTypes(Builder $query, Collection $types): Builder | ||||
|     { | ||||
|         return $query->whereRelation( | ||||
|             "vacationRequest", | ||||
|             fn(Builder $query): Builder => $query->whereIn("type", $types), | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ declare(strict_types=1); | ||||
| namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Database\Factories\VacationLimitFactory; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| @@ -37,13 +36,6 @@ class VacationLimit extends Model | ||||
|         return $this->belongsTo(YearPeriod::class); | ||||
|     } | ||||
|  | ||||
|     public function scopeOrderByUserField(Builder $query, string $field): Builder | ||||
|     { | ||||
|         $userQuery = User::query()->select($field)->whereColumn("vacation_limits.user_id", "users.id"); | ||||
|  | ||||
|         return $query->orderBy($userQuery); | ||||
|     } | ||||
|  | ||||
|     protected static function newFactory(): VacationLimitFactory | ||||
|     { | ||||
|         return VacationLimitFactory::new(); | ||||
|   | ||||
| @@ -6,10 +6,12 @@ namespace Toby\Eloquent\Models; | ||||
|  | ||||
| use Database\Factories\VacationRequestFactory; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Casts\AsCollection; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| use Illuminate\Database\Eloquent\Relations\HasMany; | ||||
| use Illuminate\Support\Arr; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Spatie\ModelStates\HasStates; | ||||
| @@ -18,6 +20,7 @@ use Toby\Domain\States\VacationRequest\VacationRequestState; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
|  * @property string $name | ||||
|  * @property VacationType $type | ||||
|  * @property VacationRequestState $state | ||||
|  * @property Carbon $from | ||||
| @@ -29,6 +32,7 @@ use Toby\Domain\States\VacationRequest\VacationRequestState; | ||||
|  * @property YearPeriod $yearPeriod | ||||
|  * @property Collection $activities | ||||
|  * @property Collection $vacations | ||||
|  * @property Collection $event_ids | ||||
|  * @property Carbon $created_at | ||||
|  * @property Carbon $updated_at | ||||
|  */ | ||||
| @@ -38,13 +42,14 @@ class VacationRequest extends Model | ||||
|     use HasStates; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
|     protected $casts = [ | ||||
|         "type" => VacationType::class, | ||||
|         "state" => VacationRequestState::class, | ||||
|         "from" => "date", | ||||
|         "to" => "date", | ||||
|         "event_ids" => AsCollection::class, | ||||
|     ]; | ||||
|     protected $perPage = 50; | ||||
|  | ||||
|     public function user(): BelongsTo | ||||
|     { | ||||
| @@ -81,6 +86,13 @@ class VacationRequest extends Model | ||||
|         return $query->whereNotState("state", $states); | ||||
|     } | ||||
|  | ||||
|     public function scopeType(Builder $query, VacationType|array $types): Builder | ||||
|     { | ||||
|         $types = Arr::wrap($types); | ||||
|  | ||||
|         return $query->whereIn("type", $types); | ||||
|     } | ||||
|  | ||||
|     public function scopeOverlapsWith(Builder $query, self $vacationRequest): Builder | ||||
|     { | ||||
|         return $query->where("from", "<=", $vacationRequest->to) | ||||
|   | ||||
| @@ -22,7 +22,6 @@ class VacationRequestActivity extends Model | ||||
|     use HasFactory; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
|     protected $casts = [ | ||||
|         "from" => VacationRequestState::class, | ||||
|         "to" => VacationRequestState::class, | ||||
|   | ||||
| @@ -15,6 +15,7 @@ use Illuminate\Support\Collection; | ||||
|  * @property int $id | ||||
|  * @property int $year | ||||
|  * @property Collection $vacationLimits | ||||
|  * @property Collection $vacationRequests | ||||
|  * @property Collection $holidays | ||||
|  */ | ||||
| class YearPeriod extends Model | ||||
| @@ -41,6 +42,11 @@ class YearPeriod extends Model | ||||
|         return $this->hasMany(VacationLimit::class); | ||||
|     } | ||||
|  | ||||
|     public function vacationRequests(): HasMany | ||||
|     { | ||||
|         return $this->hasMany(VacationRequest::class); | ||||
|     } | ||||
|  | ||||
|     public function holidays(): HasMany | ||||
|     { | ||||
|         return $this->hasMany(Holiday::class); | ||||
|   | ||||
| @@ -4,19 +4,22 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Observers; | ||||
|  | ||||
| use Toby\Eloquent\Helpers\YearPeriodRetriever; | ||||
| use Illuminate\Contracts\Hashing\Hasher; | ||||
| use Illuminate\Support\Str; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class UserObserver | ||||
| { | ||||
|     public function __construct( | ||||
|         protected YearPeriodRetriever $yearPeriodRetriever, | ||||
|         protected Hasher $hash, | ||||
|     ) {} | ||||
|  | ||||
|     public function created(User $user): void | ||||
|     public function creating(User $user): void | ||||
|     { | ||||
|         $user->vacationLimits()->create([ | ||||
|             "year_period_id" => $this->yearPeriodRetriever->current()->id, | ||||
|         ]); | ||||
|         /** | ||||
|          * A random password for user is generated because AuthenticateSession middleware needs a user's password | ||||
|          * for some checks. Users use Google to login, so they don't need to know the password (GitHub issue #84) | ||||
|          */ | ||||
|         $user->password = $this->hash->make(Str::random(40)); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,25 +4,15 @@ declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Observers; | ||||
|  | ||||
| use Illuminate\Contracts\Auth\Factory as Auth; | ||||
| use Illuminate\Contracts\Events\Dispatcher; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestObserver | ||||
| { | ||||
|     public function __construct( | ||||
|         protected Auth $auth, | ||||
|         protected Dispatcher $dispatcher, | ||||
|     ) {} | ||||
|  | ||||
|     public function creating(VacationRequest $vacationRequest): void | ||||
|     { | ||||
|         $year = $vacationRequest->from->year; | ||||
|         $count = $vacationRequest->yearPeriod->vacationRequests()->count(); | ||||
|         $number = $count + 1; | ||||
|  | ||||
|         $vacationRequestNumber = $vacationRequest->user->vacationRequests() | ||||
|             ->whereYear("from", $year) | ||||
|             ->count() + 1; | ||||
|  | ||||
|         $vacationRequest->name = "{$vacationRequestNumber}/${year}"; | ||||
|         $vacationRequest->name = "{$number}/{$vacationRequest->yearPeriod->year}"; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,22 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Eloquent\Scopes; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Scope; | ||||
| use Toby\Eloquent\Helpers\YearPeriodRetriever; | ||||
|  | ||||
| class SelectedYearPeriodScope implements Scope | ||||
| { | ||||
|     public function __construct( | ||||
|         protected YearPeriodRetriever $yearPeriodRetriever, | ||||
|     ) {} | ||||
|  | ||||
|     public function apply(Builder $builder, Model $model): Builder | ||||
|     { | ||||
|         return $builder->where("year_period_id", $this->yearPeriodRetriever->selected()->id); | ||||
|     } | ||||
| } | ||||
| @@ -1,25 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Console\Commands; | ||||
|  | ||||
| use Illuminate\Console\Command; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class CreateUserCommand extends Command | ||||
| { | ||||
|     protected $signature = "user:create {email : an email for the user}"; | ||||
|     protected $description = "Creates a user"; | ||||
|  | ||||
|     public function handle(): void | ||||
|     { | ||||
|         $email = $this->argument("email"); | ||||
|  | ||||
|         User::factory([ | ||||
|             "email" => $email, | ||||
|         ])->create(); | ||||
|  | ||||
|         $this->info("The user has been created"); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Console\Commands; | ||||
|  | ||||
| use Illuminate\Console\Command; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class MoveUserDataToProfile extends Command | ||||
| { | ||||
|     protected $signature = "toby:move-user-data-to-profile"; | ||||
|     protected $description = "Move user data to their profiles"; | ||||
|  | ||||
|     public function handle(): void | ||||
|     { | ||||
|         $users = User::all(); | ||||
|  | ||||
|         foreach ($users as $user) { | ||||
|             $user->profile()->updateOrCreate(["user_id" => $user->id], [ | ||||
|                 "first_name" => $user->first_name, | ||||
|                 "last_name" => $user->last_name, | ||||
|                 "position" => $user->position, | ||||
|                 "employment_form" => $user->employment_form, | ||||
|                 "employment_date" => $user->employment_date, | ||||
|             ]); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,34 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Console\Commands; | ||||
|  | ||||
| use Illuminate\Console\Command; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
|  | ||||
| class RebuildDocumentNumberingSystem extends Command | ||||
| { | ||||
|     protected $signature = "toby:rebuild-document-numbering-system"; | ||||
|     protected $description = "Rebuilds the document numbering system to {number}/{year}"; | ||||
|  | ||||
|     public function handle(): void | ||||
|     { | ||||
|         $yearPeriods = YearPeriod::all(); | ||||
|  | ||||
|         foreach ($yearPeriods as $yearPeriod) { | ||||
|             $number = 1; | ||||
|  | ||||
|             $vacationRequests = $yearPeriod | ||||
|                 ->vacationRequests() | ||||
|                 ->oldest() | ||||
|                 ->get(); | ||||
|  | ||||
|             foreach ($vacationRequests as $vacationRequest) { | ||||
|                 $vacationRequest->update(["name" => "{$number}/{$yearPeriod->year}"]); | ||||
|  | ||||
|                 $number++; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,79 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Console\Commands; | ||||
|  | ||||
| use Carbon\CarbonInterface; | ||||
| use Illuminate\Console\Command; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Http; | ||||
| use Toby\Domain\DailySummaryRetriever; | ||||
| use Toby\Eloquent\Models\Holiday; | ||||
| use Toby\Infrastructure\Slack\Elements\AbsencesAttachment; | ||||
| use Toby\Infrastructure\Slack\Elements\BirthdaysAttachment; | ||||
| use Toby\Infrastructure\Slack\Elements\RemotesAttachment; | ||||
|  | ||||
| class SendDailySummaryToSlack extends Command | ||||
| { | ||||
|     protected $signature = "toby:slack:daily-summary {--f|force}"; | ||||
|     protected $description = "Sent daily summary to Slack"; | ||||
|  | ||||
|     public function handle(DailySummaryRetriever $dailySummaryRetriever): void | ||||
|     { | ||||
|         $now = Carbon::today(); | ||||
|  | ||||
|         if (!$this->option("force") && !$this->shouldHandle($now)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $attachments = new Collection([ | ||||
|             new AbsencesAttachment($dailySummaryRetriever->getAbsences($now)), | ||||
|             new RemotesAttachment($dailySummaryRetriever->getRemoteDays($now)), | ||||
|             new BirthdaysAttachment($dailySummaryRetriever->getBirthdays($now)), | ||||
|         ]); | ||||
|  | ||||
|         Http::withToken($this->getSlackClientToken()) | ||||
|             ->post($this->getUrl(), [ | ||||
|                 "channel" => $this->getSlackChannel(), | ||||
|                 "text" => __("Daily summary for day :day", ["day" => $now->toDisplayString()]), | ||||
|                 "attachments" => $attachments, | ||||
|             ]); | ||||
|     } | ||||
|  | ||||
|     protected function shouldHandle(CarbonInterface $day): bool | ||||
|     { | ||||
|         $holidays = Holiday::query()->whereDate("date", $day)->pluck("date"); | ||||
|  | ||||
|         if ($day->isWeekend()) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if ($holidays->contains($day)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     protected function getUrl(): string | ||||
|     { | ||||
|         return "{$this->getSlackBaseUrl()}/chat.postMessage"; | ||||
|     } | ||||
|  | ||||
|     protected function getSlackBaseUrl(): ?string | ||||
|     { | ||||
|         return config("services.slack.url"); | ||||
|     } | ||||
|  | ||||
|     protected function getSlackClientToken(): ?string | ||||
|     { | ||||
|         return config("services.slack.client_token"); | ||||
|     } | ||||
|  | ||||
|     protected function getSlackChannel(): ?string | ||||
|     { | ||||
|         return config("services.slack.default_channel"); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,36 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Console\Commands; | ||||
|  | ||||
| use Illuminate\Console\Command; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\VacationRequestsSummaryNotification; | ||||
| use Toby\Domain\VacationRequestStatesRetriever; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class SendVacationRequestSummariesToApprovers extends Command | ||||
| { | ||||
|     protected $signature = "toby:send-vacation-request-reminders"; | ||||
|     protected $description = "Sends vacation request reminders to approvers if they didn't approve"; | ||||
|  | ||||
|     public function handle(): void | ||||
|     { | ||||
|         $users = User::query() | ||||
|             ->whereIn("role", [Role::AdministrativeApprover, Role::TechnicalApprover, Role::Administrator]) | ||||
|             ->get(); | ||||
|  | ||||
|         foreach ($users as $user) { | ||||
|             $vacationRequests = VacationRequest::query() | ||||
|                 ->states(VacationRequestStatesRetriever::waitingForUserActionStates($user)) | ||||
|                 ->get(); | ||||
|  | ||||
|             if ($vacationRequests->isNotEmpty()) { | ||||
|                 $user->notify(new VacationRequestsSummaryNotification(Carbon::today(), $vacationRequests)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,53 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Http\Controllers; | ||||
|  | ||||
| use Illuminate\Http\Request; | ||||
| use Inertia\Response; | ||||
| use Toby\Eloquent\Helpers\YearPeriodRetriever; | ||||
| use Toby\Eloquent\Models\Holiday; | ||||
| use Toby\Eloquent\Models\Vacation; | ||||
| use Toby\Infrastructure\Http\Resources\SimpleVacationRequestResource; | ||||
|  | ||||
| class AnnualSummaryController extends Controller | ||||
| { | ||||
|     public function __invoke(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response | ||||
|     { | ||||
|         $yearPeriod = $yearPeriodRetriever->selected(); | ||||
|  | ||||
|         $holidays = $yearPeriod->holidays() | ||||
|             ->get(); | ||||
|  | ||||
|         $vacations = $request->user() | ||||
|             ->vacations() | ||||
|             ->with("vacationRequest.vacations") | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->approved() | ||||
|             ->get(); | ||||
|  | ||||
|         $pendingVacations = $request->user() | ||||
|             ->vacations() | ||||
|             ->with("vacationRequest.vacations") | ||||
|             ->whereBelongsTo($yearPeriod) | ||||
|             ->pending() | ||||
|             ->get(); | ||||
|  | ||||
|         return inertia("AnnualSummary", [ | ||||
|             "holidays" => $holidays->mapWithKeys( | ||||
|                 fn(Holiday $holiday): array => [$holiday->date->toDateString() => $holiday->name], | ||||
|             ), | ||||
|             "vacations" => $vacations->mapWithKeys( | ||||
|                 fn(Vacation $vacation): array => [ | ||||
|                     $vacation->date->toDateString() => new SimpleVacationRequestResource($vacation->vacationRequest), | ||||
|                 ], | ||||
|             ), | ||||
|             "pendingVacations" => $pendingVacations->mapWithKeys( | ||||
|                 fn(Vacation $vacation): array => [ | ||||
|                     $vacation->date->toDateString() => new SimpleVacationRequestResource($vacation->vacationRequest), | ||||
|                 ], | ||||
|             ), | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user