#134 - fill users data for resume #144
| @@ -60,3 +60,8 @@ GOOGLE_CLIENT_SECRET= | ||||
| GOOGLE_REDIRECT=http://localhost/login/google/end | ||||
| GOOGLE_CALENDAR_ID= | ||||
| LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE= | ||||
|  | ||||
| SLACK_URL=https://slack.com/api | ||||
| SLACK_CLIENT_TOKEN= | ||||
| SLACK_SIGNING_SECRET= | ||||
| SLACK_DEFAULT_CHANNEL="#general" | ||||
|   | ||||
| @@ -15,5 +15,6 @@ module.exports = { | ||||
|     'comma-dangle': ['error', 'always-multiline'], | ||||
|     'object-curly-spacing': ['error', 'always'], | ||||
|     'vue/require-default-prop': 0, | ||||
|     'vue/multi-word-component-names': 0, | ||||
|   }, | ||||
| } | ||||
|   | ||||
							
								
								
									
										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}} | ||||
| @@ -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")); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,10 +30,10 @@ 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("listMonthlyUsage", 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); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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())); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,10 +6,10 @@ namespace Toby\Domain\Actions\VacationRequest; | ||||
|  | ||||
| use Illuminate\Validation\ValidationException; | ||||
| use Toby\Domain\Notifications\VacationRequestCreatedNotification; | ||||
| use Toby\Domain\VacationDaysCalculator; | ||||
| 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; | ||||
|  | ||||
| @@ -19,7 +19,7 @@ class CreateAction | ||||
|         protected VacationRequestStateManager $stateManager, | ||||
|         protected VacationRequestValidator $vacationRequestValidator, | ||||
|         protected VacationTypeConfigRetriever $configRetriever, | ||||
|         protected VacationDaysCalculator $vacationDaysCalculator, | ||||
|         protected WorkDaysCalculator $workDaysCalculator, | ||||
|         protected WaitForTechApprovalAction $waitForTechApprovalAction, | ||||
|         protected WaitForAdminApprovalAction $waitForAdminApprovalAction, | ||||
|         protected ApproveAction $approveAction, | ||||
| @@ -52,11 +52,7 @@ class CreateAction | ||||
|  | ||||
|         $vacationRequest->save(); | ||||
|  | ||||
|         $days = $this->vacationDaysCalculator->calculateDays( | ||||
|             $vacationRequest->yearPeriod, | ||||
|             $vacationRequest->from, | ||||
|             $vacationRequest->to, | ||||
|         ); | ||||
|         $days = $this->workDaysCalculator->calculateDays($vacationRequest->from, $vacationRequest->to); | ||||
|  | ||||
|         foreach ($days as $day) { | ||||
|             $vacationRequest->vacations()->create([ | ||||
|   | ||||
| @@ -57,6 +57,6 @@ class CalendarGenerator | ||||
|             ->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, | ||||
|             ], | ||||
|   | ||||
| @@ -30,7 +30,7 @@ enum VacationType: string | ||||
|         $cases = VacationType::all(); | ||||
|  | ||||
|         return $cases->map( | ||||
|             fn(VacationType $enum) => [ | ||||
|             fn(VacationType $enum): array => [ | ||||
|                 "label" => $enum->label(), | ||||
|                 "value" => $enum->value, | ||||
|             ], | ||||
|   | ||||
							
								
								
									
										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,15 @@ 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]); | ||||
|  | ||||
|         return (new SlackMessage()) | ||||
|             ->text("{$this->buildDescription()}\n <${url}|Zobacz szczegóły>"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -46,19 +55,25 @@ class VacationRequestCreatedNotification extends Notification | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|  | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|                 "user" => $user, | ||||
|             ])) | ||||
|             ->greeting( | ||||
|                 __("Hi :user!", [ | ||||
|                     "user" => $user, | ||||
|                 ]), | ||||
|             ) | ||||
|             ->subject($this->buildSubject()) | ||||
|             ->line($this->buildDescription()) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 "type" => $type, | ||||
|             ])) | ||||
|             ->line(__("From :from to :to (number of days: :days)", [ | ||||
|                 "from" => $from, | ||||
|                 "to" => $to, | ||||
|                 "days" => $days, | ||||
|             ])) | ||||
|             ->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); | ||||
|     } | ||||
|  | ||||
| @@ -80,18 +95,16 @@ class VacationRequestCreatedNotification extends Notification | ||||
|     protected function buildDescription(): string | ||||
|     { | ||||
|         $name = $this->vacationRequest->name; | ||||
|         $appName = config("app.name"); | ||||
|  | ||||
|         if ($this->vacationRequest->creator()->is($this->vacationRequest->user)) { | ||||
|             return __("The vacation request :title has been created correctly in the :appName.", [ | ||||
|             return __("The vacation request :title from user :user has been created successfully.", [ | ||||
|                 "user" => $this->vacationRequest->user->profile->full_name, | ||||
|                 "title" => $name, | ||||
|                 "appName" => $appName, | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         return __("The vacation request :title has been created correctly by user :creator on your behalf in the :appName.", [ | ||||
|         return __("The vacation request :title has been created successfully by user :creator on your behalf.", [ | ||||
|             "title" => $this->vacationRequest->name, | ||||
|             "appName" => $appName, | ||||
|             "creator" => $this->vacationRequest->creator->profile->full_name, | ||||
|         ]); | ||||
|     } | ||||
|   | ||||
| @@ -10,6 +10,7 @@ use Illuminate\Notifications\Notification; | ||||
| use InvalidArgumentException; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Infrastructure\Slack\Elements\SlackMessage; | ||||
|  | ||||
| class VacationRequestStatusChangedNotification extends Notification | ||||
| { | ||||
| @@ -22,7 +23,15 @@ class VacationRequestStatusChangedNotification 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]); | ||||
|  | ||||
|         return (new SlackMessage()) | ||||
|             ->text("{$this->buildDescription()}\n <${url}|Zobacz szczegóły>"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -43,27 +52,17 @@ class VacationRequestStatusChangedNotification extends Notification | ||||
|     protected function buildMailMessage(string $url): MailMessage | ||||
|     { | ||||
|         $user = $this->user->profile->first_name; | ||||
|         $title = $this->vacationRequest->name; | ||||
|         $type = $this->vacationRequest->type->label(); | ||||
|         $status = $this->vacationRequest->state->label(); | ||||
|         $from = $this->vacationRequest->from->toDisplayString(); | ||||
|         $to = $this->vacationRequest->to->toDisplayString(); | ||||
|         $days = $this->vacationRequest->vacations()->count(); | ||||
|         $requester = $this->vacationRequest->user->profile->full_name; | ||||
|  | ||||
|         return (new MailMessage()) | ||||
|             ->greeting(__("Hi :user!", [ | ||||
|                 "user" => $user, | ||||
|             ])) | ||||
|             ->subject(__("Vacation request :title has been :status", [ | ||||
|                 "title" => $title, | ||||
|                 "status" => $status, | ||||
|             ])) | ||||
|             ->line(__("The vacation request :title from user :requester has been :status.", [ | ||||
|                 "title" => $title, | ||||
|                 "requester" => $requester, | ||||
|                 "status" => $status, | ||||
|             ])) | ||||
|             ->subject($this->buildSubject()) | ||||
|             ->line($this->buildDescription()) | ||||
|             ->line(__("Vacation type: :type", [ | ||||
|                 "type" => $type, | ||||
|             ])) | ||||
| @@ -74,4 +73,21 @@ class VacationRequestStatusChangedNotification 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(), | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ 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 VacationRequestWaitsForApprovalNotification extends Notification | ||||
| { | ||||
| @@ -23,7 +24,15 @@ class VacationRequestWaitsForApprovalNotification 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]); | ||||
|  | ||||
|         return (new SlackMessage()) | ||||
|             ->text("{$this->buildDescription()}\n <${url}|Zobacz szczegóły>"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -0,0 +1,70 @@ | ||||
| <?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("Wnioski oczekujące na Twoją akcję - stan na dzień {$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("Lista wniosków oczekujących na Twoją akcję - stan na dzień {$this->day->toDisplayString()}:") | ||||
|             ->subject("Wnioski oczekujące na akcje - stan na dzień {$this->day->toDisplayString()}"); | ||||
|  | ||||
|         foreach ($this->vacationRequests as $request) { | ||||
|             $message->line( | ||||
|                 "Wniosek nr {$request->name} użytkownika {$request->user->profile->full_name} ({$request->from->toDisplayString()} - {$request->to->toDisplayString()})", | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return $message | ||||
|             ->action("Przejdź do wniosków", $url); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ class TimesheetExport implements WithMultipleSheets | ||||
|     public function sheets(): array | ||||
|     { | ||||
|         return $this->users | ||||
|             ->map(fn(User $user) => new TimesheetPerUserSheet($user, $this->month, $this->types)) | ||||
|             ->map(fn(User $user): TimesheetPerUserSheet => new TimesheetPerUserSheet($user, $this->month, $this->types)) | ||||
|             ->toArray(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -193,8 +193,8 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With | ||||
|             ->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, | ||||
|                 ], | ||||
|             ); | ||||
|     } | ||||
|   | ||||
| @@ -42,8 +42,8 @@ class UserVacationStatsRetriever | ||||
|                     ->states(VacationRequestStatesRetriever::successStates()), | ||||
|             ) | ||||
|             ->get() | ||||
|             ->groupBy(fn(Vacation $vacation) => strtolower($vacation->date->englishMonth)) | ||||
|             ->map(fn(Collection $items) => $items->count()); | ||||
|             ->groupBy(fn(Vacation $vacation): string => strtolower($vacation->date->englishMonth)) | ||||
|             ->map(fn(Collection $items): int => $items->count()); | ||||
|     } | ||||
|  | ||||
|     public function getPendingVacationDays(User $user, YearPeriod $yearPeriod): int | ||||
| @@ -107,13 +107,13 @@ class UserVacationStatsRetriever | ||||
|     { | ||||
|         $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 = VacationType::all(); | ||||
|  | ||||
|         return $types->filter(fn(VacationType $type) => !$this->configRetriever->hasLimit($type)); | ||||
|         return $types->filter(fn(VacationType $type): bool => !$this->configRetriever->hasLimit($type)); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,9 +7,9 @@ namespace Toby\Domain\Validation\Rules; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| 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); | ||||
|     } | ||||
| @@ -64,6 +66,6 @@ class DoesNotExceedLimitRule implements VacationRequestRule | ||||
|     { | ||||
|         $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(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,7 @@ class VacationTypeCanBeSelected implements VacationRequestRule | ||||
|         $employmentForm = $vacationRequest->user->profile->employment_form; | ||||
|  | ||||
|         $availableTypes = VacationType::all() | ||||
|             ->filter(fn(VacationType $type) => $this->configRetriever->isAvailableFor($type, $employmentForm)); | ||||
|             ->filter(fn(VacationType $type): bool => $this->configRetriever->isAvailableFor($type, $employmentForm)); | ||||
|  | ||||
|         return $availableTypes->contains($vacationRequest->type); | ||||
|     } | ||||
|   | ||||
| @@ -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(); | ||||
| @@ -35,7 +35,7 @@ class YearPeriodRetriever | ||||
|  | ||||
|         $years = YearPeriod::all(); | ||||
|  | ||||
|         $navigation = $years->map(fn(YearPeriod $yearPeriod) => $this->toNavigation($yearPeriod)); | ||||
|         $navigation = $years->map(fn(YearPeriod $yearPeriod): array => $this->toNavigation($yearPeriod)); | ||||
|  | ||||
|         return [ | ||||
|             "current" => $this->toNavigation($current), | ||||
|   | ||||
| @@ -8,14 +8,17 @@ 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 | ||||
| class Key extends Model implements NotifiableInterface | ||||
| { | ||||
|     use HasFactory; | ||||
|     use Notifiable; | ||||
|  | ||||
|     protected $guarded = []; | ||||
|  | ||||
| @@ -24,6 +27,11 @@ class Key extends Model | ||||
|         return $this->belongsTo(User::class); | ||||
|     } | ||||
|  | ||||
|     public function routeNotificationForSlack(): string | ||||
|     { | ||||
|         return config("services.slack.default_channel"); | ||||
|     } | ||||
|  | ||||
|     protected static function newFactory(): KeyFactory | ||||
|     { | ||||
|         return KeyFactory::new(); | ||||
|   | ||||
| @@ -19,6 +19,7 @@ use Toby\Eloquent\Helpers\ColorGenerator; | ||||
|  * @property string $position | ||||
|  * @property EmploymentForm $employment_form | ||||
|  * @property Carbon $employment_date | ||||
|  * @property Carbon $birthday | ||||
|  */ | ||||
| class Profile extends Model | ||||
| { | ||||
| @@ -30,6 +31,7 @@ class Profile extends Model | ||||
|     protected $casts = [ | ||||
|         "employment_form" => EmploymentForm::class, | ||||
|         "employment_date" => "date", | ||||
|         "birthday" => "date", | ||||
|     ]; | ||||
|  | ||||
|     public function user(): BelongsTo | ||||
|   | ||||
| @@ -15,6 +15,7 @@ use Illuminate\Notifications\Notifiable; | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Domain\Enums\EmploymentForm; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\Notifiable as NotifiableInterface; | ||||
|  | ||||
| /** | ||||
|  * @property int $id | ||||
| @@ -26,7 +27,7 @@ use Toby\Domain\Enums\Role; | ||||
|  * @property Collection $vacationRequests | ||||
|  * @property Collection $vacations | ||||
|  */ | ||||
| class User extends Authenticatable | ||||
| class User extends Authenticatable implements NotifiableInterface | ||||
| { | ||||
|     use HasFactory; | ||||
|     use Notifiable; | ||||
| @@ -99,7 +100,7 @@ class User extends Authenticatable | ||||
|             ->where("email", "ILIKE", "%{$text}%") | ||||
|             ->orWhereRelation( | ||||
|                 "profile", | ||||
|                 fn(Builder $query) => $query | ||||
|                 fn(Builder $query): Builder => $query | ||||
|                     ->where("first_name", "ILIKE", "%{$text}%") | ||||
|                     ->orWhere("last_name", "ILIKE", "%{$text}%"), | ||||
|             ); | ||||
| @@ -122,6 +123,11 @@ class User extends Authenticatable | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function routeNotificationForSlack() | ||||
|     { | ||||
|         return $this->profile->slack_id; | ||||
|     } | ||||
|  | ||||
|     protected static function newFactory(): UserFactory | ||||
|     { | ||||
|         return UserFactory::new(); | ||||
|   | ||||
| @@ -11,6 +11,7 @@ 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; | ||||
| @@ -84,6 +85,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) | ||||
|   | ||||
| @@ -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" => "Podsumowanie dla dnia {$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)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -6,16 +6,16 @@ namespace Toby\Infrastructure\Http\Controllers\Api; | ||||
|  | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Toby\Domain\VacationDaysCalculator; | ||||
| use Toby\Domain\WorkDaysCalculator; | ||||
| use Toby\Infrastructure\Http\Controllers\Controller; | ||||
| use Toby\Infrastructure\Http\Requests\Api\CalculateVacationDaysRequest; | ||||
|  | ||||
| class CalculateVacationDaysController extends Controller | ||||
| { | ||||
|     public function __invoke(CalculateVacationDaysRequest $request, VacationDaysCalculator $calculator): JsonResponse | ||||
|     public function __invoke(CalculateVacationDaysRequest $request, WorkDaysCalculator $calculator): JsonResponse | ||||
|     { | ||||
|         $days = $calculator->calculateDays($request->yearPeriod(), $request->from(), $request->to()); | ||||
|         $days = $calculator->calculateDays($request->from(), $request->to()); | ||||
|  | ||||
|         return new JsonResponse($days->map(fn(Carbon $day) => $day->toDateString())->all()); | ||||
|         return new JsonResponse($days->map(fn(Carbon $day): string => $day->toDateString())->all()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -21,8 +21,8 @@ class GetAvailableVacationTypesController extends Controller | ||||
|         $user = User::query()->find($request->get("user")); | ||||
|  | ||||
|         $types = VacationType::all() | ||||
|             ->filter(fn(VacationType $type) => $configRetriever->isAvailableFor($type, $user->profile->employment_form)) | ||||
|             ->map(fn(VacationType $type) => [ | ||||
|             ->filter(fn(VacationType $type): bool => $configRetriever->isAvailableFor($type, $user->profile->employment_form)) | ||||
|             ->map(fn(VacationType $type): array => [ | ||||
|                 "label" => $type->label(), | ||||
|                 "value" => $type->value, | ||||
|             ]) | ||||
|   | ||||
| @@ -7,12 +7,11 @@ namespace Toby\Infrastructure\Http\Controllers; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Inertia\Response; | ||||
| use Toby\Domain\Enums\VacationType; | ||||
| use Toby\Domain\DailySummaryRetriever; | ||||
| use Toby\Domain\UserVacationStatsRetriever; | ||||
| use Toby\Domain\VacationRequestStatesRetriever; | ||||
| use Toby\Domain\VacationTypeConfigRetriever; | ||||
| use Toby\Eloquent\Helpers\YearPeriodRetriever; | ||||
| use Toby\Eloquent\Models\Vacation; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Infrastructure\Http\Resources\HolidayResource; | ||||
| use Toby\Infrastructure\Http\Resources\VacationRequestResource; | ||||
| @@ -25,24 +24,14 @@ class DashboardController extends Controller | ||||
|         YearPeriodRetriever $yearPeriodRetriever, | ||||
|         UserVacationStatsRetriever $vacationStatsRetriever, | ||||
|         VacationTypeConfigRetriever $configRetriever, | ||||
|         DailySummaryRetriever $dailySummaryRetriever, | ||||
|     ): Response { | ||||
|         $user = $request->user(); | ||||
|         $now = Carbon::now(); | ||||
|         $yearPeriod = $yearPeriodRetriever->selected(); | ||||
|  | ||||
|         $absences = Vacation::query() | ||||
|             ->with(["user", "vacationRequest"]) | ||||
|             ->whereDate("date", $now) | ||||
|             ->approved() | ||||
|             ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => $configRetriever->isVacation($type))) | ||||
|             ->get(); | ||||
|  | ||||
|         $remoteDays = Vacation::query() | ||||
|             ->with(["user", "vacationRequest"]) | ||||
|             ->whereDate("date", $now) | ||||
|             ->approved() | ||||
|             ->whereTypes(VacationType::all()->filter(fn(VacationType $type) => !$configRetriever->isVacation($type))) | ||||
|             ->get(); | ||||
|         $absences = $dailySummaryRetriever->getAbsences($now); | ||||
|         $remoteDays = $dailySummaryRetriever->getRemoteDays($now); | ||||
|  | ||||
|         if ($user->can("listAll", VacationRequest::class)) { | ||||
|             $vacationRequests = $yearPeriod->vacationRequests() | ||||
|   | ||||
| @@ -8,6 +8,8 @@ use Illuminate\Auth\Access\AuthorizationException; | ||||
| use Illuminate\Http\Request; | ||||
| use Inertia\Response; | ||||
| use Symfony\Component\HttpFoundation\RedirectResponse; | ||||
| use Toby\Domain\Notifications\KeyHasBeenGivenNotification; | ||||
| use Toby\Domain\Notifications\KeyHasBeenTakenNotification; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Infrastructure\Http\Requests\GiveKeyRequest; | ||||
| @@ -60,6 +62,8 @@ class KeysController extends Controller | ||||
|  | ||||
|         $key->save(); | ||||
|  | ||||
|         $key->notify(new KeyHasBeenTakenNotification($request->user(), $previousUser)); | ||||
|  | ||||
|         return redirect() | ||||
|             ->back() | ||||
|             ->with("success", __("Key no :number has been taken from :user.", [ | ||||
| @@ -81,6 +85,8 @@ class KeysController extends Controller | ||||
|  | ||||
|         $key->save(); | ||||
|  | ||||
|         $key->notify(new KeyHasBeenGivenNotification($request->user(), $recipient)); | ||||
|  | ||||
|         return redirect() | ||||
|             ->back() | ||||
|             ->with("success", __("Key no :number has been given to :user.", [ | ||||
|   | ||||
| @@ -35,8 +35,10 @@ class TimesheetController extends Controller | ||||
|  | ||||
|         $types = VacationType::all() | ||||
|             ->filter( | ||||
|                 fn(VacationType $type) => $configRetriever->isAvailableFor($type, EmploymentForm::EmploymentContract) | ||||
|                     && $configRetriever->isVacation($type), | ||||
|                 fn(VacationType $type): bool => $configRetriever->isAvailableFor( | ||||
|                     $type, | ||||
|                     EmploymentForm::EmploymentContract, | ||||
|                 ) && $configRetriever->isVacation($type), | ||||
|             ); | ||||
|  | ||||
|         $filename = "{$carbonMonth->translatedFormat("F Y")}.xlsx"; | ||||
|   | ||||
| @@ -30,7 +30,7 @@ class VacationLimitController extends Controller | ||||
|             ->sortBy(fn(VacationLimit $limit): string => "{$limit->user->profile->last_name} {$limit->user->profile->first_name}") | ||||
|             ->values(); | ||||
|  | ||||
|         $limitsResource = $limits->map(fn(VacationLimit $limit) => [ | ||||
|         $limitsResource = $limits->map(fn(VacationLimit $limit): array => [ | ||||
|             "id" => $limit->id, | ||||
|             "user" => new UserResource($limit->user), | ||||
|             "hasVacation" => $limit->hasVacation(), | ||||
|   | ||||
| @@ -32,7 +32,7 @@ class HandleInertiaRequests extends Middleware | ||||
|     { | ||||
|         $user = $request->user(); | ||||
|  | ||||
|         return fn() => [ | ||||
|         return fn(): array => [ | ||||
|             "user" => $user ? new UserResource($user) : null, | ||||
|             "can" => [ | ||||
|                 "manageVacationLimits" => $user ? $user->can("manageVacationLimits") : false, | ||||
| @@ -45,7 +45,7 @@ class HandleInertiaRequests extends Middleware | ||||
|  | ||||
|     protected function getFlashData(Request $request): Closure | ||||
|     { | ||||
|         return fn() => [ | ||||
|         return fn(): array => [ | ||||
|             "success" => $request->session()->get("success"), | ||||
|             "error" => $request->session()->get("error"), | ||||
|             "info" => $request->session()->get("info"), | ||||
|   | ||||
| @@ -22,6 +22,8 @@ class UserRequest extends FormRequest | ||||
|             "position" => ["required"], | ||||
|             "employmentForm" => ["required", new Enum(EmploymentForm::class)], | ||||
|             "employmentDate" => ["required", "date_format:Y-m-d"], | ||||
|             "birthday" => ["nullable", "date_format:Y-m-d"], | ||||
|             "slackId" => [], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| @@ -41,6 +43,8 @@ class UserRequest extends FormRequest | ||||
|             "position" => $this->get("position"), | ||||
|             "employment_form" => $this->get("employmentForm"), | ||||
|             "employment_date" => $this->get("employmentDate"), | ||||
|             "birthday" => $this->get("birthday"), | ||||
|             "slack_id" => $this->get("slackId"), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ class HolidayResource extends JsonResource | ||||
|             "id" => $this->id, | ||||
|             "name" => $this->name, | ||||
|             "date" => $this->date->toDateString(), | ||||
|             "isPast" => $this->date->isPast(), | ||||
|             "isPast" => $this->date->endOfDay()->isPast(), | ||||
|             "displayDate" => $this->date->toDisplayString(), | ||||
|             "dayOfWeek" => $this->date->dayName, | ||||
|         ]; | ||||
|   | ||||
| @@ -21,6 +21,8 @@ class UserFormDataResource extends JsonResource | ||||
|             "position" => $this->profile->position, | ||||
|             "employmentForm" => $this->profile->employment_form, | ||||
|             "employmentDate" => $this->profile->employment_date->toDateString(), | ||||
|             "birthday" => $this->profile->birthday?->toDateString(), | ||||
|             "slackId" => $this->profile->slack_id, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										37
									
								
								app/Infrastructure/Slack/Channels/SlackApiChannel.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/Infrastructure/Slack/Channels/SlackApiChannel.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Channels; | ||||
|  | ||||
| use Illuminate\Http\Client\Response; | ||||
| use Illuminate\Notifications\Notification; | ||||
| use Illuminate\Support\Facades\Http; | ||||
| use Toby\Domain\Notifications\Notifiable; | ||||
|  | ||||
| class SlackApiChannel | ||||
| { | ||||
|     public function send(Notifiable $notifiable, Notification $notification): Response | ||||
|     { | ||||
|         $baseUrl = $this->getBaseUrl(); | ||||
|         $url = "{$baseUrl}/chat.postMessage"; | ||||
|         $channel = $notifiable->routeNotificationFor("slack", $notification); | ||||
|  | ||||
|         $message = $notification->toSlack($notifiable); | ||||
|  | ||||
|         return Http::withToken($this->getClientToken()) | ||||
|             ->post($url, array_merge($message->getPayload(), [ | ||||
|                 "channel" => $channel, | ||||
|             ])); | ||||
|     } | ||||
|  | ||||
|     protected function getClientToken(): string | ||||
|     { | ||||
|         return config("services.slack.client_token"); | ||||
|     } | ||||
|  | ||||
|     protected function getBaseUrl(): string | ||||
|     { | ||||
|         return config("services.slack.url"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										56
									
								
								app/Infrastructure/Slack/Controller.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/Infrastructure/Slack/Controller.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack; | ||||
|  | ||||
| use Exception; | ||||
| use Illuminate\Http\Request as IlluminateRequest; | ||||
| use Illuminate\Http\Response as IlluminateResponse; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Validation\ValidationException; | ||||
| use Spatie\SlashCommand\Attachment; | ||||
| use Spatie\SlashCommand\Controller as SlackController; | ||||
| use Spatie\SlashCommand\Exceptions\InvalidRequest; | ||||
| use Spatie\SlashCommand\Exceptions\RequestCouldNotBeHandled; | ||||
| use Spatie\SlashCommand\Exceptions\SlackSlashCommandException; | ||||
| use Spatie\SlashCommand\Response; | ||||
|  | ||||
| class Controller extends SlackController | ||||
| { | ||||
|     /** | ||||
|      * @throws InvalidRequest|RequestCouldNotBeHandled | ||||
|      */ | ||||
|     public function getResponse(IlluminateRequest $request): IlluminateResponse | ||||
|     { | ||||
|         $this->verifyWithSigning($request); | ||||
|  | ||||
|         $handler = $this->determineHandler(); | ||||
|  | ||||
|         try { | ||||
|             $response = $handler->handle($this->request); | ||||
|         } catch (SlackSlashCommandException $exception) { | ||||
|             $response = $exception->getResponse($this->request); | ||||
|         } catch (ValidationException $exception) { | ||||
|             $response = $this->prepareValidationResponse($exception); | ||||
|         } catch (Exception $exception) { | ||||
|             $response = $this->convertToResponse($exception); | ||||
|         } | ||||
|  | ||||
|         return $response->getIlluminateResponse(); | ||||
|     } | ||||
|  | ||||
|     protected function prepareValidationResponse(ValidationException $exception): Response | ||||
|     { | ||||
|         $errors = (new Collection($exception->errors())) | ||||
|             ->map( | ||||
|                 fn(array $message): Attachment => Attachment::create() | ||||
|                     ->setColor("danger") | ||||
|                     ->setText($message[0]), | ||||
|             ); | ||||
|  | ||||
|         return Response::create($this->request) | ||||
|             ->withText(":x: Polecenie `/{$this->request->command} {$this->request->text}` jest niepoprawne:") | ||||
|             ->withAttachments($errors->all()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								app/Infrastructure/Slack/Elements/AbsencesAttachment.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/Infrastructure/Slack/Elements/AbsencesAttachment.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Elements; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Eloquent\Models\Vacation; | ||||
|  | ||||
| class AbsencesAttachment extends ListAttachment | ||||
| { | ||||
|     public function __construct(Collection $absences) | ||||
|     { | ||||
|         parent::__construct(); | ||||
|  | ||||
|         $this | ||||
|             ->setTitle("Nieobecności :palm_tree:") | ||||
|             ->setColor("#eab308") | ||||
|             ->setItems($absences->map(fn(Vacation $vacation): string => $vacation->user->profile->full_name)) | ||||
|             ->setEmptyText("Wszyscy dzisiaj pracują :muscle:"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										12
									
								
								app/Infrastructure/Slack/Elements/Attachment.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/Infrastructure/Slack/Elements/Attachment.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Elements; | ||||
|  | ||||
| use Illuminate\Contracts\Support\Arrayable; | ||||
| use Spatie\SlashCommand\Attachment as BaseAttachment; | ||||
|  | ||||
| class Attachment extends BaseAttachment implements Arrayable | ||||
| { | ||||
| } | ||||
							
								
								
									
										22
									
								
								app/Infrastructure/Slack/Elements/BirthdaysAttachment.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/Infrastructure/Slack/Elements/BirthdaysAttachment.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Elements; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Eloquent\Models\User; | ||||
|  | ||||
| class BirthdaysAttachment extends ListAttachment | ||||
| { | ||||
|     public function __construct(Collection $birthdays) | ||||
|     { | ||||
|         parent::__construct(); | ||||
|  | ||||
|         $this | ||||
|             ->setTitle("Urodziny :birthday:") | ||||
|             ->setColor("#3c5f97") | ||||
|             ->setItems($birthdays->map(fn(User $user): string => $user->profile->full_name)) | ||||
|             ->setEmptyText("Dzisiaj nikt nie ma urodzin :cry:"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								app/Infrastructure/Slack/Elements/KeysAttachment.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/Infrastructure/Slack/Elements/KeysAttachment.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Elements; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Eloquent\Models\Key; | ||||
|  | ||||
| class KeysAttachment extends ListAttachment | ||||
| { | ||||
|     public function __construct(Collection $keys) | ||||
|     { | ||||
|         parent::__construct(); | ||||
|  | ||||
|         $this | ||||
|             ->setColor("#3c5f97") | ||||
|             ->setItems($keys->map(fn(Key $key): string => "Klucz nr {$key->id} - <@{$key->user->profile->slack_id}>")) | ||||
|             ->setEmptyText("Nie ma żadnych kluczy w tobym"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										36
									
								
								app/Infrastructure/Slack/Elements/ListAttachment.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/Infrastructure/Slack/Elements/ListAttachment.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Elements; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| class ListAttachment extends Attachment | ||||
| { | ||||
|     protected Collection $items; | ||||
|     protected string $emptyText = ""; | ||||
|  | ||||
|     public function setItems(Collection $items): static | ||||
|     { | ||||
|         $this->items = $items; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function setEmptyText(string $emptyText): static | ||||
|     { | ||||
|         $this->emptyText = $emptyText; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function toArray(): array | ||||
|     { | ||||
|         $fields = parent::toArray(); | ||||
|  | ||||
|         return array_merge($fields, [ | ||||
|             "text" => $this->items->isNotEmpty() ? $this->items->implode("\n") : $this->emptyText, | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								app/Infrastructure/Slack/Elements/RemotesAttachment.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/Infrastructure/Slack/Elements/RemotesAttachment.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Elements; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Eloquent\Models\Vacation; | ||||
|  | ||||
| class RemotesAttachment extends ListAttachment | ||||
| { | ||||
|     public function __construct(Collection $remoteDays) | ||||
|     { | ||||
|         parent::__construct(); | ||||
|  | ||||
|         $this | ||||
|             ->setTitle("Praca zdalna :house_with_garden:") | ||||
|             ->setColor("#527aba") | ||||
|             ->setItems($remoteDays->map(fn(Vacation $vacation): string => $vacation->user->profile->full_name)) | ||||
|             ->setEmptyText("Wszyscy dzisiaj są w biurze :boom:"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										53
									
								
								app/Infrastructure/Slack/Elements/SlackMessage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/Infrastructure/Slack/Elements/SlackMessage.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Elements; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| class SlackMessage | ||||
| { | ||||
|     protected string $text = ""; | ||||
|     protected Collection $attachments; | ||||
|  | ||||
|     public function __construct() | ||||
|     { | ||||
|         $this->attachments = new Collection(); | ||||
|     } | ||||
|  | ||||
|     public function text(string $text): static | ||||
|     { | ||||
|         $this->text = $text; | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function withAttachment(Attachment $attachment): static | ||||
|     { | ||||
|         $this->attachments->push($attachment); | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function withAttachments(Collection $attachments): static | ||||
|     { | ||||
|         foreach ($attachments as $attachment) { | ||||
|             $this->withAttachment($attachment); | ||||
|         } | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
|  | ||||
|     public function getPayload(): array | ||||
|     { | ||||
|         return [ | ||||
|             "text" => $this->text, | ||||
|             "link_names" => true, | ||||
|             "unfurl_links" => true, | ||||
|             "unfurl_media" => true, | ||||
|             "mrkdwn" => true, | ||||
|             "attachments" => $this->attachments->toArray(), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Elements; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
|  | ||||
| class VacationRequestsAttachment extends ListAttachment | ||||
| { | ||||
|     public function __construct(Collection $vacationRequests) | ||||
|     { | ||||
|         parent::__construct(); | ||||
|  | ||||
|         $this | ||||
|             ->setColor("#527aba") | ||||
|             ->setItems($this->mapVacationRequests($vacationRequests)); | ||||
|     } | ||||
|  | ||||
|     protected function mapVacationRequests(Collection $vacationRequests): Collection | ||||
|     { | ||||
|         return $vacationRequests->map(function (VacationRequest $request): string { | ||||
|             $url = route("vacation.requests.show", ["vacationRequest" => $request->id]); | ||||
|  | ||||
|             $date = $request->from->equalTo($request->to) | ||||
|                 ? "{$request->from->toDisplayString()}" | ||||
|                 : "{$request->from->toDisplayString()} - {$request->to->toDisplayString()}"; | ||||
|  | ||||
|             return "<{$url}|Wniosek nr {$request->name}> użytkownika {$request->user->profile->full_name} ({$date})"; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Exceptions; | ||||
|  | ||||
| use Spatie\SlashCommand\Exceptions\SlackSlashCommandException; | ||||
|  | ||||
| class UserNotFoundException extends SlackSlashCommandException | ||||
| { | ||||
| } | ||||
							
								
								
									
										35
									
								
								app/Infrastructure/Slack/Handlers/CatchAll.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/Infrastructure/Slack/Handlers/CatchAll.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Handlers; | ||||
|  | ||||
| use Spatie\SlashCommand\Handlers\BaseHandler; | ||||
| use Spatie\SlashCommand\Request; | ||||
| use Spatie\SlashCommand\Response; | ||||
| use Toby\Infrastructure\Slack\Elements\Attachment; | ||||
| use Toby\Infrastructure\Slack\Traits\ListsHandlers; | ||||
|  | ||||
| class CatchAll extends BaseHandler | ||||
| { | ||||
|     use ListsHandlers; | ||||
|  | ||||
|     public function canHandle(Request $request): bool | ||||
|     { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public function handle(Request $request): Response | ||||
|     { | ||||
|         $handlers = $this->findAvailableHandlers(); | ||||
|         $attachmentFields = $this->mapHandlersToAttachments($handlers); | ||||
|  | ||||
|         return $this->respondToSlack(":x: Nie rozpoznaję polecenia. Lista wszystkich poleceń:") | ||||
|             ->withAttachment( | ||||
|                 Attachment::create() | ||||
|                     ->setColor("danger") | ||||
|                     ->useMarkdown() | ||||
|                     ->setFields($attachmentFields), | ||||
|             ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										36
									
								
								app/Infrastructure/Slack/Handlers/DailySummary.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/Infrastructure/Slack/Handlers/DailySummary.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Handlers; | ||||
|  | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Collection; | ||||
| use Spatie\SlashCommand\Request; | ||||
| use Spatie\SlashCommand\Response; | ||||
| use Toby\Domain\DailySummaryRetriever; | ||||
| use Toby\Infrastructure\Slack\Elements\AbsencesAttachment; | ||||
| use Toby\Infrastructure\Slack\Elements\BirthdaysAttachment; | ||||
| use Toby\Infrastructure\Slack\Elements\RemotesAttachment; | ||||
|  | ||||
| class DailySummary extends SignatureHandler | ||||
| { | ||||
|     protected $signature = "toby dzisiaj"; | ||||
|     protected $description = "Codzienne podsumowanie"; | ||||
|  | ||||
|     public function handle(Request $request): Response | ||||
|     { | ||||
|         $dailySummaryRetriever = app()->make(DailySummaryRetriever::class); | ||||
|  | ||||
|         $now = Carbon::today(); | ||||
|  | ||||
|         $attachments = new Collection([ | ||||
|             new AbsencesAttachment($dailySummaryRetriever->getAbsences($now)), | ||||
|             new RemotesAttachment($dailySummaryRetriever->getRemoteDays($now)), | ||||
|             new BirthdaysAttachment($dailySummaryRetriever->getBirthdays($now)), | ||||
|         ]); | ||||
|  | ||||
|         return $this->respondToSlack("Podsumowanie dla dnia {$now->toDisplayString()}") | ||||
|             ->withAttachments($attachments->all()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										71
									
								
								app/Infrastructure/Slack/Handlers/GiveKeysTo.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								app/Infrastructure/Slack/Handlers/GiveKeysTo.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Handlers; | ||||
|  | ||||
| use Illuminate\Validation\ValidationException; | ||||
| use Spatie\SlashCommand\Request; | ||||
| use Spatie\SlashCommand\Response; | ||||
| use Toby\Domain\Notifications\KeyHasBeenGivenNotification; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| use Toby\Infrastructure\Slack\Exceptions\UserNotFoundException; | ||||
| use Toby\Infrastructure\Slack\Rules\SlackUserExistsRule; | ||||
| use Toby\Infrastructure\Slack\Traits\FindsUserBySlackId; | ||||
|  | ||||
| class GiveKeysTo extends SignatureHandler | ||||
| { | ||||
|     use FindsUserBySlackId; | ||||
|  | ||||
|     protected $signature = "toby klucze:dla {user}"; | ||||
|     protected $description = "Przekaż klucze wskazanemu użytkownikowi"; | ||||
|  | ||||
|     /** | ||||
|      * @throws UserNotFoundException | ||||
|      * @throws ValidationException | ||||
|      */ | ||||
|     public function handle(Request $request): Response | ||||
|     { | ||||
|         ["user" => $from] = $this->validate(); | ||||
|  | ||||
|         $authUser = $this->findUserBySlackIdOrFail($request->userId); | ||||
|         $user = $this->findUserBySlackId($from); | ||||
|  | ||||
|         /** @var Key $key */ | ||||
|         $key = $authUser->keys()->first(); | ||||
|  | ||||
|         if (!$key) { | ||||
|             throw ValidationException::withMessages(["key" => "Nie masz żadnego klucza do przekazania"]); | ||||
|         } | ||||
|  | ||||
|         if ($user->is($authUser)) { | ||||
|             throw ValidationException::withMessages([ | ||||
|                 "key" => "Nie możesz przekazać sobie kluczy :dzban:", | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         $key->user()->associate($user); | ||||
|  | ||||
|         $key->save(); | ||||
|  | ||||
|         $key->notify(new KeyHasBeenGivenNotification($authUser, $user)); | ||||
|  | ||||
|         return $this->respondToSlack( | ||||
|             ":white_check_mark: Klucz nr {$key->id} został przekazany użytkownikowi <@{$user->profile->slack_id}>", | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     protected function getRules(): array | ||||
|     { | ||||
|         return [ | ||||
|             "user" => ["required", new SlackUserExistsRule()], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     protected function getMessages(): array | ||||
|     { | ||||
|         return [ | ||||
|             "user.required" => "Musisz podać użytkownika, któremu chcesz przekazać klucze", | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										33
									
								
								app/Infrastructure/Slack/Handlers/Help.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/Infrastructure/Slack/Handlers/Help.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Handlers; | ||||
|  | ||||
| use Spatie\SlashCommand\Request; | ||||
| use Spatie\SlashCommand\Response; | ||||
| use Toby\Infrastructure\Slack\Elements\Attachment; | ||||
| use Toby\Infrastructure\Slack\Traits\ListsHandlers; | ||||
|  | ||||
| class Help extends SignatureHandler | ||||
| { | ||||
|     use ListsHandlers; | ||||
|  | ||||
|     protected $signature = "toby pomoc"; | ||||
|     protected $description = "Wyświetl wszystkie dostępne polecenia"; | ||||
|  | ||||
|     public function handle(Request $request): Response | ||||
|     { | ||||
|         $handlers = $this->findAvailableHandlers(); | ||||
|  | ||||
|         $attachmentFields = $this->mapHandlersToAttachments($handlers); | ||||
|  | ||||
|         return $this->respondToSlack("Dostępne polecenia:") | ||||
|             ->withAttachment( | ||||
|                 Attachment::create() | ||||
|                     ->setColor("good") | ||||
|                     ->useMarkdown() | ||||
|                     ->setFields($attachmentFields), | ||||
|             ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										45
									
								
								app/Infrastructure/Slack/Handlers/HomeOffice.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/Infrastructure/Slack/Handlers/HomeOffice.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Handlers; | ||||
|  | ||||
| use Illuminate\Support\Carbon; | ||||
| use Spatie\SlashCommand\Request; | ||||
| use Spatie\SlashCommand\Response; | ||||
| use Toby\Domain\Actions\VacationRequest\CreateAction; | ||||
| use Toby\Domain\Enums\VacationType; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
| use Toby\Infrastructure\Slack\Traits\FindsUserBySlackId; | ||||
|  | ||||
| class HomeOffice extends SignatureHandler | ||||
| { | ||||
|     use FindsUserBySlackId; | ||||
|  | ||||
|     protected $signature = "toby zdalnie"; | ||||
|     protected $description = "Pracuj dzisiaj zdalnie"; | ||||
|  | ||||
|     public function handle(Request $request): Response | ||||
|     { | ||||
|         $user = $this->findUserBySlackId($request->userId); | ||||
|  | ||||
|         $this->createRemoteday($user, Carbon::today()); | ||||
|  | ||||
|         return $this->respondToSlack(":white_check_mark: Pracujesz dzisiaj zdalnie"); | ||||
|     } | ||||
|  | ||||
|     protected function createRemoteday(User $user, Carbon $date): void | ||||
|     { | ||||
|         $yearPeriod = YearPeriod::findByYear($date->year); | ||||
|  | ||||
|         app(CreateAction::class)->execute([ | ||||
|             "user_id" => $user->id, | ||||
|             "type" => VacationType::HomeOffice, | ||||
|             "from" => $date, | ||||
|             "to" => $date, | ||||
|             "year_period_id" => $yearPeriod->id, | ||||
|             "flow_skipped" => false, | ||||
|         ], $user); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										26
									
								
								app/Infrastructure/Slack/Handlers/KeyList.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/Infrastructure/Slack/Handlers/KeyList.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Handlers; | ||||
|  | ||||
| use Spatie\SlashCommand\Request; | ||||
| use Spatie\SlashCommand\Response; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| use Toby\Infrastructure\Slack\Elements\KeysAttachment; | ||||
|  | ||||
| class KeyList extends SignatureHandler | ||||
| { | ||||
|     protected $signature = "toby klucze"; | ||||
|     protected $description = "Lista wszystkich kluczy"; | ||||
|  | ||||
|     public function handle(Request $request): Response | ||||
|     { | ||||
|         $keys = Key::query() | ||||
|             ->orderBy("id") | ||||
|             ->get(); | ||||
|  | ||||
|         return $this->respondToSlack("Lista kluczy :key:") | ||||
|             ->withAttachment(new KeysAttachment($keys)); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										26
									
								
								app/Infrastructure/Slack/Handlers/SignatureHandler.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/Infrastructure/Slack/Handlers/SignatureHandler.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Handlers; | ||||
|  | ||||
| use Illuminate\Support\Facades\Validator; | ||||
| use Spatie\SlashCommand\Handlers\SignatureHandler as BaseSignatureHandler; | ||||
|  | ||||
| abstract class SignatureHandler extends BaseSignatureHandler | ||||
| { | ||||
|     public function validate() | ||||
|     { | ||||
|         return Validator::validate($this->getArguments(), $this->getRules(), $this->getMessages()); | ||||
|     } | ||||
|  | ||||
|     protected function getRules(): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     protected function getMessages(): array | ||||
|     { | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										70
									
								
								app/Infrastructure/Slack/Handlers/TakeKeysFrom.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								app/Infrastructure/Slack/Handlers/TakeKeysFrom.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Handlers; | ||||
|  | ||||
| use Illuminate\Validation\ValidationException; | ||||
| use Spatie\SlashCommand\Request; | ||||
| use Spatie\SlashCommand\Response; | ||||
| use Toby\Domain\Notifications\KeyHasBeenTakenNotification; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| use Toby\Infrastructure\Slack\Exceptions\UserNotFoundException; | ||||
| use Toby\Infrastructure\Slack\Rules\SlackUserExistsRule; | ||||
| use Toby\Infrastructure\Slack\Traits\FindsUserBySlackId; | ||||
|  | ||||
| class TakeKeysFrom extends SignatureHandler | ||||
| { | ||||
|     use FindsUserBySlackId; | ||||
|  | ||||
|     protected $signature = "toby klucze:od {user}"; | ||||
|     protected $description = "Zabierz klucze wskazanemu użytkownikowi"; | ||||
|  | ||||
|     /** | ||||
|      * @throws UserNotFoundException|ValidationException | ||||
|      */ | ||||
|     public function handle(Request $request): Response | ||||
|     { | ||||
|         ["user" => $from] = $this->validate(); | ||||
|  | ||||
|         $authUser = $this->findUserBySlackIdOrFail($request->userId); | ||||
|         $user = $this->findUserBySlackId($from); | ||||
|  | ||||
|         /** @var Key $key */ | ||||
|         $key = $user->keys()->first(); | ||||
|  | ||||
|         if (!$key) { | ||||
|             throw ValidationException::withMessages([ | ||||
|                 "key" => "Użytkownik <@{$user->profile->slack_id}> nie ma żadnych kluczy", | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         if ($key->user()->is($authUser)) { | ||||
|             throw ValidationException::withMessages([ | ||||
|                 "key" => "Nie możesz zabrać sobie kluczy :dzban:", | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         $key->user()->associate($authUser); | ||||
|  | ||||
|         $key->save(); | ||||
|  | ||||
|         $key->notify(new KeyHasBeenTakenNotification($authUser, $user)); | ||||
|  | ||||
|         return $this->respondToSlack(":white_check_mark: Klucz nr {$key->id} został zabrany użytkownikowi <@{$user->profile->slack_id}>"); | ||||
|     } | ||||
|  | ||||
|     protected function getRules(): array | ||||
|     { | ||||
|         return [ | ||||
|             "user" => ["required", new SlackUserExistsRule()], | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     protected function getMessages(): array | ||||
|     { | ||||
|         return [ | ||||
|             "user.required" => "Musisz podać użytkownika, któremu chcesz zabrać klucze", | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								app/Infrastructure/Slack/Rules/SlackUserExistsRule.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/Infrastructure/Slack/Rules/SlackUserExistsRule.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Rules; | ||||
|  | ||||
| use Illuminate\Contracts\Validation\Rule; | ||||
| use Illuminate\Support\Str; | ||||
| use Toby\Eloquent\Models\Profile; | ||||
|  | ||||
| class SlackUserExistsRule implements Rule | ||||
| { | ||||
|     public function passes($attribute, $value): bool | ||||
|     { | ||||
|         $slackId = Str::between($value, "<@", "|"); | ||||
|  | ||||
|         return Profile::query()->where("slack_id", $slackId)->exists(); | ||||
|     } | ||||
|  | ||||
|     public function message(): string | ||||
|     { | ||||
|         return "Użytkownik :input nie istnieje w tobym"; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										43
									
								
								app/Infrastructure/Slack/Traits/FindsUserBySlackId.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/Infrastructure/Slack/Traits/FindsUserBySlackId.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Traits; | ||||
|  | ||||
| use Illuminate\Support\Str; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Infrastructure\Slack\Exceptions\UserNotFoundException; | ||||
|  | ||||
| trait FindsUserBySlackId | ||||
| { | ||||
|     protected function findUserBySlackId(string $slackId): ?User | ||||
|     { | ||||
|         $id = $this->prepareSlackIdFromString($slackId); | ||||
|  | ||||
|         /** @var User $user */ | ||||
|         $user = User::query() | ||||
|             ->whereRelation("profile", "slack_id", $id) | ||||
|             ->first(); | ||||
|  | ||||
|         return $user; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @throws UserNotFoundException | ||||
|      */ | ||||
|     protected function findUserBySlackIdOrFail(string $slackId): ?User | ||||
|     { | ||||
|         $user = $this->findUserBySlackId($slackId); | ||||
|  | ||||
|         if (!$user) { | ||||
|             throw new UserNotFoundException("Użytkownik {$slackId} nie istnieje w tobym"); | ||||
|         } | ||||
|  | ||||
|         return $user; | ||||
|     } | ||||
|  | ||||
|     protected function prepareSlackIdFromString(string $slackId): string | ||||
|     { | ||||
|         return Str::between($slackId, "<@", "|"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										46
									
								
								app/Infrastructure/Slack/Traits/ListsHandlers.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/Infrastructure/Slack/Traits/ListsHandlers.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Toby\Infrastructure\Slack\Traits; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Str; | ||||
| use Spatie\SlashCommand\AttachmentField; | ||||
| use Spatie\SlashCommand\Handlers\BaseHandler; | ||||
| use Spatie\SlashCommand\Handlers\SignatureHandler; | ||||
| use Spatie\SlashCommand\Handlers\SignatureParts; | ||||
| use Spatie\SlashCommand\HandlesSlashCommand; | ||||
|  | ||||
| trait ListsHandlers | ||||
| { | ||||
|     protected function findAvailableHandlers(): Collection | ||||
|     { | ||||
|         return collect(config("laravel-slack-slash-command.handlers")) | ||||
|             ->map(fn(string $handlerClassName): BaseHandler => new $handlerClassName($this->request)) | ||||
|             ->filter(fn(HandlesSlashCommand $handler): bool => $handler instanceof SignatureHandler) | ||||
|             ->filter(function (SignatureHandler $handler) { | ||||
|                 $signatureParts = new SignatureParts($handler->getSignature()); | ||||
|  | ||||
|                 return Str::is($signatureParts->getSlashCommandName(), $this->request->command); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     protected function mapHandlersToAttachments(Collection $handlers): array | ||||
|     { | ||||
|         return $handlers | ||||
|             ->sort( | ||||
|                 fn(SignatureHandler $handlerA, SignatureHandler $handlerB): int => strcmp( | ||||
|                     $handlerA->getFullCommand(), | ||||
|                     $handlerB->getFullCommand(), | ||||
|                 ), | ||||
|             ) | ||||
|             ->map( | ||||
|                 fn(SignatureHandler $handler): AttachmentField => AttachmentField::create( | ||||
|                     $handler->getDescription(), | ||||
|                     "`/{$handler->getSignature()}`", | ||||
|                 ), | ||||
|             ) | ||||
|             ->all(); | ||||
|     } | ||||
| } | ||||
| @@ -10,7 +10,7 @@ | ||||
|         "ext-redis": "*", | ||||
|         "azuyalabs/yasumi": "^2.4", | ||||
|         "barryvdh/laravel-dompdf": "^1.0", | ||||
|         "fruitcake/laravel-cors": "^2.0", | ||||
|         "fruitcake/laravel-cors": "^3.0", | ||||
|         "guzzlehttp/guzzle": "^7.0.1", | ||||
|         "inertiajs/inertia-laravel": "^0.5.1", | ||||
|         "laravel/framework": "^9.7", | ||||
| @@ -22,10 +22,11 @@ | ||||
|         "phpoffice/phpword": "^0.18.3", | ||||
|         "rackbeat/laravel-ui-avatars": "^1.0", | ||||
|         "spatie/laravel-google-calendar": "^3.5", | ||||
|         "spatie/laravel-model-states": "^2.1" | ||||
|         "spatie/laravel-model-states": "^2.1", | ||||
|         "spatie/laravel-slack-slash-command": "^1.11" | ||||
|     }, | ||||
|     "require-dev": { | ||||
|         "blumilksoftware/codestyle": "^1.0.0", | ||||
|         "blumilksoftware/codestyle": "^1.2.0", | ||||
|         "fakerphp/faker": "^1.19", | ||||
|         "laravel/dusk": "^6.21", | ||||
|         "mockery/mockery": "^1.4.4", | ||||
| @@ -62,7 +63,8 @@ | ||||
|     "extra": { | ||||
|         "laravel": { | ||||
|             "dont-discover": [ | ||||
|                 "laravel/telescope" | ||||
|                 "laravel/telescope", | ||||
|                 "spatie/laravel-slack-slash-command" | ||||
|             ] | ||||
|         } | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										696
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										696
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -37,12 +37,12 @@ return [ | ||||
|         Illuminate\Translation\TranslationServiceProvider::class, | ||||
|         Illuminate\Validation\ValidationServiceProvider::class, | ||||
|         Illuminate\View\ViewServiceProvider::class, | ||||
|         Barryvdh\DomPDF\ServiceProvider::class, | ||||
|         Toby\Architecture\Providers\AppServiceProvider::class, | ||||
|         Toby\Architecture\Providers\AuthServiceProvider::class, | ||||
|         Toby\Architecture\Providers\EventServiceProvider::class, | ||||
|         Toby\Architecture\Providers\RouteServiceProvider::class, | ||||
|         Toby\Architecture\Providers\TelescopeServiceProvider::class, | ||||
|         Toby\Architecture\Providers\ObserverServiceProvider::class, | ||||
|         Barryvdh\DomPDF\ServiceProvider::class, | ||||
|     ], | ||||
| ]; | ||||
|   | ||||
							
								
								
									
										24
									
								
								config/laravel-slack-slash-command.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								config/laravel-slack-slash-command.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| use Toby\Infrastructure\Slack\Handlers\CatchAll; | ||||
| use Toby\Infrastructure\Slack\Handlers\DailySummary; | ||||
| use Toby\Infrastructure\Slack\Handlers\GiveKeysTo; | ||||
| use Toby\Infrastructure\Slack\Handlers\Help; | ||||
| use Toby\Infrastructure\Slack\Handlers\HomeOffice; | ||||
| use Toby\Infrastructure\Slack\Handlers\KeyList; | ||||
| use Toby\Infrastructure\Slack\Handlers\TakeKeysFrom; | ||||
|  | ||||
| return [ | ||||
|     "signing_secret" => env("SLACK_SIGNING_SECRET"), | ||||
|     "handlers" => [ | ||||
|         TakeKeysFrom::class, | ||||
|         GiveKeysTo::class, | ||||
|         KeyList::class, | ||||
|         HomeOffice::class, | ||||
|         DailySummary::class, | ||||
|         Help::class, | ||||
|         CatchAll::class, | ||||
|     ], | ||||
| ]; | ||||
| @@ -8,4 +8,9 @@ return [ | ||||
|         "client_secret" => env("GOOGLE_CLIENT_SECRET"), | ||||
|         "redirect" => env("GOOGLE_REDIRECT"), | ||||
|     ], | ||||
|     "slack" => [ | ||||
|         "url" => "https://slack.com/api", | ||||
|         "client_token" => env("SLACK_CLIENT_TOKEN"), | ||||
|         "default_channel" => env("SLACK_DEFAULT_CHANNEL"), | ||||
|     ], | ||||
| ]; | ||||
|   | ||||
| @@ -23,6 +23,7 @@ class ProfileFactory extends Factory | ||||
|             "employment_form" => $this->faker->randomElement(EmploymentForm::cases()), | ||||
|             "position" => $this->faker->jobTitle(), | ||||
|             "employment_date" => Carbon::createFromInterface($this->faker->dateTimeBetween("2020-10-27"))->toDateString(), | ||||
|             "birthday" => Carbon::createFromInterface($this->faker->dateTimeBetween("1970-01-01", "1998-01-01"))->toDateString(), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,25 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| use Illuminate\Database\Migrations\Migration; | ||||
| use Illuminate\Database\Schema\Blueprint; | ||||
| use Illuminate\Support\Facades\Schema; | ||||
|  | ||||
| return new class() extends Migration { | ||||
|     public function up(): void | ||||
|     { | ||||
|         Schema::table("profiles", function (Blueprint $table): void { | ||||
|             $table->string("slack_id")->nullable(); | ||||
|             $table->date("birthday")->nullable(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public function down(): void | ||||
|     { | ||||
|         Schema::table("profiles", function (Blueprint $table): void { | ||||
|             $table->dropColumn("slack_id"); | ||||
|             $table->dropColumn("birthday"); | ||||
|         }); | ||||
|     } | ||||
| }; | ||||
| @@ -7,7 +7,7 @@ namespace Database\Seeders; | ||||
| use Illuminate\Database\Seeder; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Toby\Domain\PolishHolidaysRetriever; | ||||
| use Toby\Domain\VacationDaysCalculator; | ||||
| use Toby\Domain\WorkDaysCalculator; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationLimit; | ||||
| @@ -70,8 +70,7 @@ class DatabaseSeeder extends Seeder | ||||
|                     "year_period_id" => $yearPeriods->random()->id, | ||||
|                 ]) | ||||
|                 ->afterCreating(function (VacationRequest $vacationRequest): void { | ||||
|                     $days = app(VacationDaysCalculator::class)->calculateDays( | ||||
|                         $vacationRequest->yearPeriod, | ||||
|                     $days = app(WorkDaysCalculator::class)->calculateDays( | ||||
|                         $vacationRequest->from, | ||||
|                         $vacationRequest->to, | ||||
|                     ); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ use Toby\Domain\States\VacationRequest\Created; | ||||
| use Toby\Domain\States\VacationRequest\Rejected; | ||||
| use Toby\Domain\States\VacationRequest\WaitingForAdministrative; | ||||
| use Toby\Domain\States\VacationRequest\WaitingForTechnical; | ||||
| use Toby\Domain\VacationDaysCalculator; | ||||
| use Toby\Domain\WorkDaysCalculator; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| use Toby\Eloquent\Models\Resume; | ||||
| use Toby\Eloquent\Models\Technology; | ||||
| @@ -166,8 +166,7 @@ class DemoSeeder extends Seeder | ||||
|             ->for($user, "creator") | ||||
|             ->for($currentYearPeriod) | ||||
|             ->afterCreating(function (VacationRequest $vacationRequest): void { | ||||
|                 $days = app(VacationDaysCalculator::class)->calculateDays( | ||||
|                     $vacationRequest->yearPeriod, | ||||
|                 $days = app(WorkDaysCalculator::class)->calculateDays( | ||||
|                     $vacationRequest->from, | ||||
|                     $vacationRequest->to, | ||||
|                 ); | ||||
| @@ -236,8 +235,7 @@ class DemoSeeder extends Seeder | ||||
|             ->for($user, "creator") | ||||
|             ->for($currentYearPeriod) | ||||
|             ->afterCreating(function (VacationRequest $vacationRequest): void { | ||||
|                 $days = app(VacationDaysCalculator::class)->calculateDays( | ||||
|                     $vacationRequest->yearPeriod, | ||||
|                 $days = app(WorkDaysCalculator::class)->calculateDays( | ||||
|                     $vacationRequest->from, | ||||
|                     $vacationRequest->to, | ||||
|                 ); | ||||
| @@ -293,8 +291,7 @@ class DemoSeeder extends Seeder | ||||
|             ->for($user, "creator") | ||||
|             ->for($currentYearPeriod) | ||||
|             ->afterCreating(function (VacationRequest $vacationRequest): void { | ||||
|                 $days = app(VacationDaysCalculator::class)->calculateDays( | ||||
|                     $vacationRequest->yearPeriod, | ||||
|                 $days = app(WorkDaysCalculator::class)->calculateDays( | ||||
|                     $vacationRequest->from, | ||||
|                     $vacationRequest->to, | ||||
|                 ); | ||||
|   | ||||
| @@ -35,7 +35,7 @@ services: | ||||
|     restart: unless-stopped | ||||
|  | ||||
|   database: | ||||
|     image: postgres:13 | ||||
|     image: postgres:14 | ||||
|     container_name: toby-db-dev | ||||
|     environment: | ||||
|       - PGPASSWORD=${DOCKER_DEV_DB_ROOT_PASSWORD} | ||||
| @@ -51,7 +51,7 @@ services: | ||||
|     restart: unless-stopped | ||||
|  | ||||
|   database-test: | ||||
|     image: postgres:13 | ||||
|     image: postgres:14 | ||||
|     container_name: toby-db-test | ||||
|     environment: | ||||
|       - PGPASSWORD=${DOCKER_TEST_DB_ROOT_PASSWORD} | ||||
| @@ -65,7 +65,7 @@ services: | ||||
|     restart: unless-stopped | ||||
|  | ||||
|   redis: | ||||
|     image: redis:6 | ||||
|     image: redis:7 | ||||
|     container_name: toby-redis | ||||
|     ports: | ||||
|       - ${FORWARD_REDIS_PORT:-6379}:6379 | ||||
| @@ -76,7 +76,7 @@ services: | ||||
|     restart: unless-stopped | ||||
|  | ||||
|   node: | ||||
|     image: node:17.2.0-alpine3.14 | ||||
|     image: node:18.1.0-alpine3.14 | ||||
|     container_name: toby-node | ||||
|     working_dir: /application | ||||
|     volumes: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| FROM ghcr.io/blumilksoftware/php:8.1 | ||||
|  | ||||
| ARG XDEBUG_VERSION=3.1.2 | ||||
| ARG XDEBUG_VERSION=3.1.4 | ||||
| ARG INSTALL_XDEBUG=false | ||||
|  | ||||
| RUN if [ ${INSTALL_XDEBUG} = true ]; then \ | ||||
|   | ||||
							
								
								
									
										2094
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2094
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										29
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								package.json
									
									
									
									
									
								
							| @@ -13,28 +13,28 @@ | ||||
|         "postinstall": "npm run prod" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@headlessui/vue": "^1.6.0", | ||||
|         "@headlessui/vue": "^1.6.1", | ||||
|         "@heroicons/vue": "^1.0.6", | ||||
|         "@inertiajs/inertia": "^0.11.0", | ||||
|         "@inertiajs/inertia-vue3": "^0.6.0", | ||||
|         "@inertiajs/progress": "^0.2.7", | ||||
|         "@tailwindcss/forms": "^0.5.0", | ||||
|         "@tailwindcss/line-clamp": "^0.3.1", | ||||
|         "@tailwindcss/forms": "^0.5.1", | ||||
|         "@tailwindcss/line-clamp": "^0.4.0", | ||||
|         "@tailwindcss/typography": "^0.5.2", | ||||
|         "@vue/compiler-sfc": "^3.2.31", | ||||
|         "autoprefixer": "^10.4.4", | ||||
|         "axios": "^0.26.1", | ||||
|         "@vue/compiler-sfc": "^3.2.33", | ||||
|         "autoprefixer": "^10.4.7", | ||||
|         "axios": "^0.27.2", | ||||
|         "echarts": "^5.3.2", | ||||
|         "eslit": "^6.0.0", | ||||
|         "flatpickr": "^4.6.11", | ||||
|         "flatpickr": "^4.6.13", | ||||
|         "laravel-mix": "^6.0.43", | ||||
|         "lodash": "^4.17.21", | ||||
|         "luxon": "^2.3.1", | ||||
|         "postcss": "^8.4.12", | ||||
|         "tailwindcss": "^3.0.23", | ||||
|         "vue": "^3.2.31", | ||||
|         "luxon": "^2.3.2", | ||||
|         "postcss": "^8.4.13", | ||||
|         "tailwindcss": "^3.0.24", | ||||
|         "vue": "^3.2.33", | ||||
|         "vue-echarts": "^6.0.2", | ||||
|         "vue-flatpickr-component": "^9.0.5", | ||||
|         "vue-flatpickr-component": "^9.0.6", | ||||
|         "vue-loader": "^17.0.0", | ||||
|         "vue-material-design-icons": "^5.0.0", | ||||
|         "vue-toastification": "^2.0.0-rc.5", | ||||
| @@ -42,8 +42,7 @@ | ||||
|         "vuedraggable": "^4.1.0" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "eslint": "^8.12.0", | ||||
|         "eslint-plugin-tailwindcss": "^3.5.0", | ||||
|         "eslint-plugin-vue": "^8.5.0" | ||||
|         "eslint": "^8.14.0", | ||||
|         "eslint-plugin-vue": "^8.7.1" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import HandHeartOutlineIcon from 'vue-material-design-icons/HandHeartOutline.vue | ||||
| import CalendarCheckIcon from 'vue-material-design-icons/CalendarCheck.vue' | ||||
| import MedicalBagIcon from 'vue-material-design-icons/MedicalBag.vue' | ||||
| import CalendarRemoveIcon from 'vue-material-design-icons/CalendarRemove.vue' | ||||
| import LaptopIcon from 'vue-material-design-icons/Laptop.vue' | ||||
| import HomeCityIcon from 'vue-material-design-icons/HomeCity.vue' | ||||
|  | ||||
| const types = [ | ||||
|   { | ||||
| @@ -43,8 +43,8 @@ const types = [ | ||||
|     text: 'Urlop szkoleniowy', | ||||
|     value: 'training_vacation', | ||||
|     icon: HumanMaleBoardIcon, | ||||
|     color: 'text-blumilk-500', | ||||
|     border: 'border-blumilk-500', | ||||
|     color: 'text-indigo-500', | ||||
|     border: 'border-indigo-500', | ||||
|   }, | ||||
|   { | ||||
|     text: 'Urlop bezpłatny', | ||||
| @@ -84,9 +84,9 @@ const types = [ | ||||
|   { | ||||
|     text: 'Praca zdalna', | ||||
|     value: 'home_office', | ||||
|     icon: LaptopIcon, | ||||
|     color: 'text-fuchsia-500', | ||||
|     border: 'border-fuchsia-500', | ||||
|     icon: HomeCityIcon, | ||||
|     color: 'text-lime-500', | ||||
|     border: 'border-lime-500', | ||||
|   }, | ||||
| ] | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|         :remote-days="remoteDays.data" | ||||
|       /> | ||||
|       <UpcomingHolidays | ||||
|         v-if="years.current.year === years.selected.year" | ||||
|         v-if="years.current.year === years.selected.year && holidays.data.length" | ||||
|         :holidays="holidays.data" | ||||
|       /> | ||||
|     </div> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <InertiaHead title="Dodaj dzień wolny" /> | ||||
|   <div class="bg-white shadow-md"> | ||||
|   <div class="mx-auto w-full max-w-7xl bg-white shadow-md"> | ||||
|     <div class="p-4 sm:px-6"> | ||||
|       <h2 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|         Dodaj dzień wolny | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <InertiaHead title="Dodawanie użytkownika" /> | ||||
|   <div class="bg-white shadow-md"> | ||||
|   <div class="mx-auto w-full max-w-7xl bg-white shadow-md"> | ||||
|     <div class="p-4 sm:px-6"> | ||||
|       <h2 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|         Dodaj użytkownika | ||||
| @@ -234,6 +234,52 @@ | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="items-center py-4 sm:grid sm:grid-cols-3"> | ||||
|         <label | ||||
|           for="slackId" | ||||
|           class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|         > | ||||
|           Slack ID | ||||
|         </label> | ||||
|         <div class="mt-1 sm:col-span-2 sm:mt-0"> | ||||
|           <input | ||||
|             id="position" | ||||
|             v-model="form.slackId" | ||||
|             type="text" | ||||
|             class="block w-full max-w-lg rounded-md shadow-sm sm:text-sm" | ||||
|             :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.slackId, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.slackId }" | ||||
|           > | ||||
|           <p | ||||
|             v-if="form.errors.slackId" | ||||
|             class="mt-2 text-sm text-red-600" | ||||
|           > | ||||
|             {{ form.errors.slackId }} | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="items-center py-4 sm:grid sm:grid-cols-3"> | ||||
|         <label | ||||
|           for="birthday" | ||||
|           class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|         > | ||||
|           Data urodzenia | ||||
|         </label> | ||||
|         <div class="mt-1 sm:col-span-2 sm:mt-0"> | ||||
|           <FlatPickr | ||||
|             id="birthday" | ||||
|             v-model="form.birthday" | ||||
|             placeholder="Wybierz datę" | ||||
|             class="block w-full max-w-lg rounded-md shadow-sm sm:text-sm" | ||||
|             :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.birthday, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.birthday }" | ||||
|           /> | ||||
|           <p | ||||
|             v-if="form.errors.birthday" | ||||
|             class="mt-2 text-sm text-red-600" | ||||
|           > | ||||
|             {{ form.errors.birthday }} | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="flex justify-end py-3"> | ||||
|         <div class="space-x-3"> | ||||
|           <InertiaLink | ||||
| @@ -274,6 +320,8 @@ const form = useForm({ | ||||
|   role: props.roles[0], | ||||
|   position: null, | ||||
|   employmentDate: null, | ||||
|   birthday: null, | ||||
|   slackId: null, | ||||
| }) | ||||
|  | ||||
| function createUser() { | ||||
|   | ||||
| @@ -241,6 +241,52 @@ | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="items-center py-4 sm:grid sm:grid-cols-3"> | ||||
|         <label | ||||
|           for="birthday" | ||||
|           class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|         > | ||||
|           Data urodzenia | ||||
|         </label> | ||||
|         <div class="mt-1 sm:col-span-2 sm:mt-0"> | ||||
|           <FlatPickr | ||||
|             id="birthday" | ||||
|             v-model="form.birthday" | ||||
|             placeholder="Wybierz datę" | ||||
|             class="block w-full max-w-lg rounded-md shadow-sm sm:text-sm" | ||||
|             :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.birthday, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.birthday }" | ||||
|           /> | ||||
|           <p | ||||
|             v-if="form.errors.birthday" | ||||
|             class="mt-2 text-sm text-red-600" | ||||
|           > | ||||
|             {{ form.errors.birthday }} | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="items-center py-4 sm:grid sm:grid-cols-3"> | ||||
|         <label | ||||
|           for="slackId" | ||||
|           class="block text-sm font-medium text-gray-700 sm:mt-px" | ||||
|         > | ||||
|           Slack ID | ||||
|         </label> | ||||
|         <div class="mt-1 sm:col-span-2 sm:mt-0"> | ||||
|           <input | ||||
|             id="position" | ||||
|             v-model="form.slackId" | ||||
|             type="text" | ||||
|             class="block w-full max-w-lg rounded-md shadow-sm sm:text-sm" | ||||
|             :class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.slackId, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.slackId }" | ||||
|           > | ||||
|           <p | ||||
|             v-if="form.errors.slackId" | ||||
|             class="mt-2 text-sm text-red-600" | ||||
|           > | ||||
|             {{ form.errors.slackId }} | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="flex justify-end py-3"> | ||||
|         <div class="space-x-3"> | ||||
|           <InertiaLink | ||||
| @@ -282,6 +328,8 @@ const form = useForm({ | ||||
|   position: props.user.position, | ||||
|   employmentForm: props.employmentForms.find(form => form.value === props.user.employmentForm), | ||||
|   employmentDate: props.user.employmentDate, | ||||
|   birthday: props.user.birthday, | ||||
|   slackId: props.user.slackId, | ||||
| }) | ||||
|  | ||||
| function editUser() { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <InertiaHead title="Złóż wniosek" /> | ||||
|   <div class="grid grid-cols-1 gap-4 items-start xl:grid-cols-3 xl:gap-8"> | ||||
|   <div :class="[stats.limit > 0 ? ' grid grid-cols-1 gap-4 items-start xl:grid-cols-3 xl:gap-8' : 'mx-auto w-full max-w-7xl']"> | ||||
|     <div class="flex flex-col h-full bg-white shadow-md xl:col-span-2"> | ||||
|       <div class="p-4 sm:px-6"> | ||||
|         <h2 class="text-lg font-medium leading-6 text-gray-900"> | ||||
| @@ -306,7 +306,10 @@ | ||||
|         </div> | ||||
|       </form> | ||||
|     </div> | ||||
|     <div class="h-full bg-white shadow-md"> | ||||
|     <div | ||||
|       v-if="stats.limit > 0 " | ||||
|       class="h-full bg-white shadow-md" | ||||
|     > | ||||
|       <div class="p-4 sm:px-6"> | ||||
|         <h2 class="text-lg font-medium leading-6 text-gray-900"> | ||||
|           <span v-if="auth.user.id !== form.user.id"> | ||||
|   | ||||
| @@ -132,9 +132,11 @@ | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody class="bg-white divide-y divide-gray-100"> | ||||
|           <tr | ||||
|           <InertiaLink | ||||
|             v-for="request in requests.data" | ||||
|             :key="request.id" | ||||
|             :href="`/vacation/requests/${request.id}`" | ||||
|             as="tr" | ||||
|             class="relative hover:bg-blumilk-25" | ||||
|           > | ||||
|             <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
| @@ -167,12 +169,8 @@ | ||||
|               > | ||||
|                 <ChevronRightIcon class="block w-6 h-6 fill-blumilk-500" /> | ||||
|               </InertiaLink> | ||||
|               <InertiaLink | ||||
|                 :href="`/vacation/requests/${request.id}`" | ||||
|                 class="absolute inset-0 focus:outline-blumilk-500" | ||||
|               /> | ||||
|             </td> | ||||
|           </tr> | ||||
|           </InertiaLink> | ||||
|           <tr v-if="! requests.data.length"> | ||||
|             <td | ||||
|               colspan="100%" | ||||
|   | ||||
| @@ -211,10 +211,12 @@ | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody class="bg-white divide-y divide-gray-100"> | ||||
|           <tr | ||||
|           <InertiaLink | ||||
|             v-for="request in requests.data" | ||||
|             :key="request.id" | ||||
|             class="relative hover:bg-blumilk-25" | ||||
|             as="tr" | ||||
|             :href="`/vacation/requests/${request.id}`" | ||||
|             class="relative hover:bg-blumilk-25 hover:cursor-pointer" | ||||
|           > | ||||
|             <td class="p-4 text-sm text-gray-500 whitespace-nowrap"> | ||||
|               <InertiaLink | ||||
| @@ -261,12 +263,8 @@ | ||||
|               > | ||||
|                 <ChevronRightIcon class="block w-6 h-6 fill-blumilk-500" /> | ||||
|               </InertiaLink> | ||||
|               <InertiaLink | ||||
|                 :href="`/vacation/requests/${request.id}`" | ||||
|                 class="absolute inset-0 focus:outline-blumilk-500" | ||||
|               /> | ||||
|             </td> | ||||
|           </tr> | ||||
|           </InertiaLink> | ||||
|           <tr v-if="! requests.data.length"> | ||||
|             <td | ||||
|               colspan="100%" | ||||
|   | ||||
| @@ -77,7 +77,7 @@ | ||||
|               </dt> | ||||
|               <dd | ||||
|                 v-if="request.comment != null" | ||||
|                 class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0" | ||||
|                 class="mt-1 text-sm text-gray-900 break-all sm:col-span-2 sm:mt-0" | ||||
|               > | ||||
|                 {{ request.comment }} | ||||
|               </dd> | ||||
| @@ -89,7 +89,7 @@ | ||||
|               </dd> | ||||
|             </div> | ||||
|             <div | ||||
|               v-if="VacationType.isVacation" | ||||
|               v-if="request.isVacation" | ||||
|               class="py-5 px-4 bg-white sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6" | ||||
|             > | ||||
|               <dt class="flex items-center text-sm font-medium text-gray-500"> | ||||
| @@ -100,7 +100,7 @@ | ||||
|                   <li class="flex justify-between items-center py-3 pr-4 pl-3 text-sm"> | ||||
|                     <div class="flex flex-1 items-center w-0"> | ||||
|                       <PaperClipIcon class="shrink-0 w-5 h-5 text-gray-400" /> | ||||
|                       <span class="flex-1 ml-2 w-0 truncate"> wniosek_urlopowy.pdf </span> | ||||
|                       <span class="flex-1 ml-2 w-0 truncate">wniosek.pdf</span> | ||||
|                     </div> | ||||
|                     <div class="shrink-0 ml-4"> | ||||
|                       <a | ||||
|   | ||||
| @@ -14,18 +14,24 @@ | ||||
|             class="py-5" | ||||
|           > | ||||
|             <div class="relative focus-within:ring-2 focus-within:ring-blumilk-500"> | ||||
|               <h3 class="text-sm font-semibold text-blumilk-600 hover:text-blumilk-500"> | ||||
|                 <InertiaLink | ||||
|                   :href="`/vacation/requests/${request.id}`" | ||||
|                   class="hover:underline focus:outline-none" | ||||
|                 > | ||||
|                   <span class="absolute inset-0" /> | ||||
|                   Wniosek o {{ findType(request.type).text.toLowerCase() }} | ||||
|                   [{{ request.name }}] | ||||
|                 </InertiaLink> | ||||
|               </h3> | ||||
|               <p class="mt-1 text-sm text-gray-600"> | ||||
|                 {{ request.from }} - {{ request.to }} | ||||
|               <div class="flex flex-row"> | ||||
|                 <h3 class="text-sm font-semibold text-blumilk-600 hover:text-blumilk-500"> | ||||
|                   <InertiaLink | ||||
|                     :href="`/vacation/requests/${request.id}`" | ||||
|                     class="hover:underline focus:outline-none" | ||||
|                   > | ||||
|                     <span class="absolute inset-0" /> | ||||
|                     Wniosek [{{ request.name }}] | ||||
|                   </InertiaLink> | ||||
|                 </h3> | ||||
|                 <div> | ||||
|                   <div class="ml-2 text-sm text-gray-600"> | ||||
|                     {{ request.from }} - {{ request.to }} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <p class="mt-2 text-sm text-gray-600"> | ||||
|                 <VacationType :type="request.type" /> | ||||
|               </p> | ||||
|               <div class="mt-3 text-sm text-gray-600"> | ||||
|                 <div class="flex"> | ||||
| @@ -66,11 +72,10 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import useVacationTypeInfo from '@/Composables/vacationTypeInfo' | ||||
| import VacationType from '@/Shared/VacationType' | ||||
|  | ||||
| defineProps({ | ||||
|   requests: Object, | ||||
| }) | ||||
|  | ||||
| const { findType } = useVacationTypeInfo() | ||||
| </script> | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
|               {{ holiday.name }} | ||||
|             </p> | ||||
|             <p class="text-sm text-gray-500"> | ||||
|               {{ holiday.displayDate }} | ||||
|               {{ holiday.displayDate }} ({{ holiday.dayOfWeek }}) | ||||
|             </p> | ||||
|           </div> | ||||
|         </li> | ||||
|   | ||||
| @@ -14,18 +14,22 @@ | ||||
|             class="py-5" | ||||
|           > | ||||
|             <div class="relative focus-within:ring-2 focus-within:ring-blumilk-500"> | ||||
|               <h3 class="text-sm font-semibold text-blumilk-600 hover:text-blumilk-500"> | ||||
|                 <InertiaLink | ||||
|                   :href="`/vacation/requests/${request.id}`" | ||||
|                   class="hover:underline focus:outline-none" | ||||
|                 > | ||||
|                   <span class="absolute inset-0" /> | ||||
|                   Wniosek o {{ findType(request.type).text.toLowerCase() }} | ||||
|                   [{{ request.name }}] | ||||
|                 </InertiaLink> | ||||
|               </h3> | ||||
|               <p class="mt-1 text-sm text-gray-600"> | ||||
|                 {{ request.from }} - {{ request.to }} | ||||
|               <div class="flex flex-row"> | ||||
|                 <h3 class="text-sm font-semibold text-blumilk-600 hover:text-blumilk-500"> | ||||
|                   <InertiaLink | ||||
|                     :href="`/vacation/requests/${request.id}`" | ||||
|                     class="hover:underline focus:outline-none" | ||||
|                   > | ||||
|                     <span class="absolute inset-0" /> | ||||
|                     Wniosek [{{ request.name }}] | ||||
|                   </InertiaLink> | ||||
|                 </h3> | ||||
|                 <div class="ml-2 text-sm text-gray-600"> | ||||
|                   {{ request.from }} - {{ request.to }} | ||||
|                 </div> | ||||
|               </div> | ||||
|               <p class="mt-2 text-sm text-gray-600"> | ||||
|                 <VacationType :type="request.type" /> | ||||
|               </p> | ||||
|               <p class="mt-2 text-sm text-gray-600"> | ||||
|                 <Status :status="request.state" /> | ||||
| @@ -52,12 +56,11 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import useVacationTypeInfo from '@/Composables/vacationTypeInfo' | ||||
| import Status from '@/Shared/Status' | ||||
| import VacationType from '@/Shared/VacationType' | ||||
|  | ||||
| defineProps({ | ||||
|   requests: Object, | ||||
| }) | ||||
|  | ||||
| const { findType } = useVacationTypeInfo() | ||||
| </script> | ||||
|   | ||||
| @@ -61,7 +61,7 @@ | ||||
|           </dt> | ||||
|         </div> | ||||
|         <div class="py-5 px-4 bg-white shadow-md sm:p-6"> | ||||
|           <dt class="mt-1 text-4xl font-semibold text-fuchsia-700"> | ||||
|           <dt class="mt-1 text-4xl font-semibold text-lime-500"> | ||||
|             {{ stats.homeOffice }} | ||||
|           </dt> | ||||
|           <dd class="font-medium text-gray-700 truncate text-md"> | ||||
|   | ||||
| @@ -23,11 +23,11 @@ | ||||
|   "cancelled": "anulowany", | ||||
|   "rejected": "odrzucony", | ||||
|   "approved": "zatwierdzony", | ||||
|   "You have pending vacation request in this range.": "Masz oczekujący wniosek urlopowy w tym zakresie dat.", | ||||
|   "You have approved vacation request in this range.": "Masz zaakceptowany wniosek urlopowy w tym zakresie dat.", | ||||
|   "You have pending vacation request in this range.": "Masz oczekujący wniosek w tym zakresie dat.", | ||||
|   "You have approved vacation request in this range.": "Masz zaakceptowany wniosek w tym zakresie dat.", | ||||
|   "Vacation limit has been exceeded.": "Limit urlopu został przekroczony.", | ||||
|   "Vacation needs minimum one day.": "Urlop musi być co najmniej na jeden dzień.", | ||||
|   "The vacation request cannot be created at the turn of the year.": "Wniosek urlopowy nie może zostać złożony na przełomie roku.", | ||||
|   "The vacation request cannot be created at the turn of the year.": "Wniosek nie może zostać złożony na przełomie roku.", | ||||
|   "User has been created.": "Użytkownik został utworzony.", | ||||
|   "User has been updated.": "Użytkownik został zaktualizowany.", | ||||
|   "User has been deleted.": "Użytkownik został usunięty.", | ||||
| @@ -37,11 +37,11 @@ | ||||
|   "Holiday has been deleted.": "Dzień wolny został usunięty.", | ||||
|   "Selected year period has been changed.": "Wybrany rok został zmieniony.", | ||||
|   "Vacation limits have been updated.": "Limity urlopów zostały zaktualizowane.", | ||||
|   "Vacation request has been created.": "Wniosek urlopowy został utworzony.", | ||||
|   "Vacation request has been accepted.": "Wniosek urlopowy został zaakceptowany.", | ||||
|   "Vacation request has been approved.": "Wniosek urlopowy został zatwierdzony.", | ||||
|   "Vacation request has been rejected.": "Wniosek urlopowy został odrzucony.", | ||||
|   "Vacation request has been cancelled.": "Wniosek urlopowy został anulowany.", | ||||
|   "Vacation request has been created.": "Wniosek został utworzony.", | ||||
|   "Vacation request has been accepted.": "Wniosek został zaakceptowany.", | ||||
|   "Vacation request has been approved.": "Wniosek został zatwierdzony.", | ||||
|   "Vacation request has been rejected.": "Wniosek został odrzucony.", | ||||
|   "Vacation request has been cancelled.": "Wniosek został anulowany.", | ||||
|   "Sum:": "Suma:", | ||||
|   "Date": "Data", | ||||
|   "Day of week": "Dzień tygodnia", | ||||
| @@ -56,7 +56,7 @@ | ||||
|   "All rights reserved.": "Wszelkie prawa zastrzeżone", | ||||
|   "Show vacation request": "Pokaż wniosek", | ||||
|   "Vacation request :title has been created" : "Wniosek :title został utworzony", | ||||
|   "The vacation request :title has been created correctly in the :appName.": "W systemie :appName został poprawnie utworzony wniosek urlopowy :title.", | ||||
|   "The vacation request :title from user :requester has been created successfully.": "Wniosek :title użytkownika :requester został utworzony pomyślnie.", | ||||
|   "Vacation type: :type": "Rodzaj wniosku: :type", | ||||
|   "From :from to :to (number of days: :days)": "Od :from do :to (liczba dni: :days)", | ||||
|   "Click here for details": "Kliknij, aby zobaczyć szczegóły", | ||||
| @@ -67,9 +67,11 @@ | ||||
|   "Vacation request :title has been :status": "Wniosek :title został :status", | ||||
|   "The vacation request :title from user :requester has been :status.": "Wniosek urlopowy :title użytkownika :requester został :status.", | ||||
|   "Vacation request :title has been created on your behalf": "Wniosek urlopowy :title został utworzony w Twoim imieniu", | ||||
|   "The vacation request :title has been created correctly by user :creator on your behalf in the :appName.": "W systemie :appName został poprawnie utworzony wniosek urlopowy :title w Twoim imieniu przez użytkownika :creator.", | ||||
|   "The vacation request :title has been created successfully by user :creator on your behalf.": "Wniosek urlopowy :title został pomyślnie utworzony w Twoim imieniu przez użytkownika :creator.", | ||||
|   "Key no :number has been created.": "Klucz nr :number został utworzony.", | ||||
|   "Key no :number has been deleted.": "Klucz nr :number został usunięty.", | ||||
|   "Key no :number has been taken from :user.": "Klucz nr :number został zabrany użytkownikowi :user.", | ||||
|   "Key no :number has been given to :user.": "Klucz nr :number został przekazany użytkownikowi :user." | ||||
|   "Key no :number has been given to :user.": "Klucz nr :number został przekazany użytkownikowi :user.", | ||||
|   ":sender gives key no :key to :recipient": ":sender przekazuje klucz nr :key :recipient", | ||||
|   ":recipient takes key no :key from :sender": ":recipient zabiera klucz nr :key :sender" | ||||
| } | ||||
|   | ||||
| @@ -71,7 +71,7 @@ | ||||
|     </div> | ||||
|  | ||||
|     <div class="main"> | ||||
|         <h2>Wniosek o urlop</h2> | ||||
|         <h2>Wniosek</h2> | ||||
|         <p class="content"> | ||||
|             Proszę o {{ mb_strtolower($vacationRequest->type->label()) }} w okresie od dnia {{ $vacationRequest->from->format("d.m.Y") }} | ||||
|             do dnia {{ $vacationRequest->to->format("d.m.Y") }} włącznie tj. {{ $vacationRequest->vacations()->count() }} dni roboczych za rok {{ $vacationRequest->yearPeriod->year }}. | ||||
|   | ||||
| @@ -7,6 +7,9 @@ use Toby\Infrastructure\Http\Controllers\Api\CalculateUserUnavailableDaysControl | ||||
| use Toby\Infrastructure\Http\Controllers\Api\CalculateUserVacationStatsController; | ||||
| use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController; | ||||
| use Toby\Infrastructure\Http\Controllers\Api\GetAvailableVacationTypesController; | ||||
| use Toby\Infrastructure\Slack\Controller as SlackCommandController; | ||||
|  | ||||
| Route::post("slack", [SlackCommandController::class, "getResponse"]); | ||||
|  | ||||
| Route::middleware("auth:sanctum")->group(function (): void { | ||||
|     Route::post("vacation/calculate-days", CalculateVacationDaysController::class); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ declare(strict_types=1); | ||||
| namespace Tests\Feature; | ||||
|  | ||||
| use Illuminate\Foundation\Testing\DatabaseMigrations; | ||||
| use Illuminate\Support\Facades\Notification; | ||||
| use Inertia\Testing\AssertableInertia as Assert; | ||||
| use Tests\FeatureTestCase; | ||||
| use Toby\Eloquent\Models\Key; | ||||
| @@ -14,6 +15,13 @@ class KeyTest extends FeatureTestCase | ||||
| { | ||||
|     use DatabaseMigrations; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         parent::setUp(); | ||||
|  | ||||
|         Notification::fake(); | ||||
|     } | ||||
|  | ||||
|     public function testUserCanSeeKeyList(): void | ||||
|     { | ||||
|         Key::factory()->count(10)->create(); | ||||
|   | ||||
							
								
								
									
										82
									
								
								tests/Unit/SendDailySummaryToSlackTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								tests/Unit/SendDailySummaryToSlackTest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Tests\Unit; | ||||
|  | ||||
| use Illuminate\Foundation\Testing\RefreshDatabase; | ||||
| use Illuminate\Support\Carbon; | ||||
| use Illuminate\Support\Facades\Http; | ||||
| use Tests\TestCase; | ||||
| use Tests\Traits\InteractsWithYearPeriods; | ||||
| use Toby\Eloquent\Models\Holiday; | ||||
| use Toby\Infrastructure\Console\Commands\SendDailySummaryToSlack; | ||||
|  | ||||
| class SendDailySummaryToSlackTest extends TestCase | ||||
| { | ||||
|     use RefreshDatabase; | ||||
|     use InteractsWithYearPeriods; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         parent::setUp(); | ||||
|  | ||||
|         Http::fake(); | ||||
|         $this->createCurrentYearPeriod(); | ||||
|     } | ||||
|  | ||||
|     public function testCommandSendsMessageToSlackIfWeekday(): void | ||||
|     { | ||||
|         $weekDay = Carbon::create(2022, 4, 22); | ||||
|         $this->assertTrue($weekDay->isWeekday()); | ||||
|  | ||||
|         $this->travelTo($weekDay); | ||||
|  | ||||
|         $this->artisan(SendDailySummaryToSlack::class) | ||||
|             ->execute(); | ||||
|  | ||||
|         Http::assertSentCount(1); | ||||
|     } | ||||
|  | ||||
|     public function testCommandDoesntSendMessageIfWeekend(): void | ||||
|     { | ||||
|         $weekend = Carbon::create(2022, 4, 23); | ||||
|         $this->assertTrue($weekend->isWeekend()); | ||||
|  | ||||
|         $this->travelTo($weekend); | ||||
|  | ||||
|         $this->artisan(SendDailySummaryToSlack::class) | ||||
|             ->execute(); | ||||
|  | ||||
|         Http::assertNothingSent(); | ||||
|     } | ||||
|  | ||||
|     public function testCommandDoesntSendMessageIfHoliday(): void | ||||
|     { | ||||
|         $holiday = Holiday::factory(["date" => Carbon::create(2022, 4, 22)])->create(); | ||||
|  | ||||
|         $this->assertDatabaseHas("holidays", [ | ||||
|             "date" => $holiday->date->toDateString(), | ||||
|         ]); | ||||
|  | ||||
|         $this->travelTo(Carbon::create(2022, 4, 22)); | ||||
|  | ||||
|         $this->artisan(SendDailySummaryToSlack::class) | ||||
|             ->execute(); | ||||
|  | ||||
|         Http::assertNothingSent(); | ||||
|     } | ||||
|  | ||||
|     public function testCommandForceSendsMessageEvenIsWeekendOrHoliday(): void | ||||
|     { | ||||
|         $weekend = Carbon::create(2022, 4, 23); | ||||
|         $this->assertTrue($weekend->isWeekend()); | ||||
|  | ||||
|         $this->travelTo($weekend); | ||||
|  | ||||
|         $this->artisan(SendDailySummaryToSlack::class, ["--force" => true]) | ||||
|             ->execute(); | ||||
|  | ||||
|         Http::assertSentCount(1); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										130
									
								
								tests/Unit/SendVacationRequestSummariesTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								tests/Unit/SendVacationRequestSummariesTest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace Tests\Unit; | ||||
|  | ||||
| use Illuminate\Foundation\Testing\RefreshDatabase; | ||||
| use Illuminate\Support\Facades\Notification; | ||||
| use Tests\TestCase; | ||||
| use Tests\Traits\InteractsWithYearPeriods; | ||||
| use Toby\Domain\Enums\Role; | ||||
| use Toby\Domain\Notifications\VacationRequestsSummaryNotification; | ||||
| use Toby\Domain\States\VacationRequest\Approved; | ||||
| use Toby\Domain\States\VacationRequest\Cancelled; | ||||
| use Toby\Domain\States\VacationRequest\Created; | ||||
| use Toby\Domain\States\VacationRequest\Rejected; | ||||
| use Toby\Domain\States\VacationRequest\WaitingForTechnical; | ||||
| use Toby\Eloquent\Models\User; | ||||
| use Toby\Eloquent\Models\VacationRequest; | ||||
| use Toby\Eloquent\Models\YearPeriod; | ||||
| use Toby\Infrastructure\Console\Commands\SendVacationRequestSummariesToApprovers; | ||||
|  | ||||
| class SendVacationRequestSummariesTest extends TestCase | ||||
| { | ||||
|     use RefreshDatabase; | ||||
|     use InteractsWithYearPeriods; | ||||
|  | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         parent::setUp(); | ||||
|  | ||||
|         Notification::fake(); | ||||
|         $this->createCurrentYearPeriod(); | ||||
|     } | ||||
|  | ||||
|     public function testSummariesAreSentOnlyToProperApprovers(): void | ||||
|     { | ||||
|         $currentYearPeriod = YearPeriod::current(); | ||||
|  | ||||
|         $user = User::factory([ | ||||
|             "role" => Role::Employee, | ||||
|         ])->create(); | ||||
|         $technicalApprover = User::factory([ | ||||
|             "role" => Role::TechnicalApprover, | ||||
|         ])->create(); | ||||
|         $administrativeApprover = User::factory([ | ||||
|             "role" => Role::AdministrativeApprover, | ||||
|         ])->create(); | ||||
|         $admin = User::factory([ | ||||
|             "role" => Role::Administrator, | ||||
|         ])->create(); | ||||
|  | ||||
|         VacationRequest::factory() | ||||
|             ->for($user) | ||||
|             ->for($currentYearPeriod) | ||||
|             ->create(["state" => WaitingForTechnical::class]); | ||||
|  | ||||
|         $this->artisan(SendVacationRequestSummariesToApprovers::class) | ||||
|             ->execute(); | ||||
|  | ||||
|         Notification::assertSentTo([$technicalApprover, $admin], VacationRequestsSummaryNotification::class); | ||||
|         Notification::assertNotSentTo([$user, $administrativeApprover], VacationRequestsSummaryNotification::class); | ||||
|     } | ||||
|  | ||||
|     public function testSummariesAreSentOnlyIfVacationRequestWaitingForActionExists(): void | ||||
|     { | ||||
|         $currentYearPeriod = YearPeriod::current(); | ||||
|  | ||||
|         $user = User::factory([ | ||||
|             "role" => Role::Employee, | ||||
|         ])->create(); | ||||
|         $technicalApprover = User::factory([ | ||||
|             "role" => Role::TechnicalApprover, | ||||
|         ])->create(); | ||||
|         $admin = User::factory([ | ||||
|             "role" => Role::Administrator, | ||||
|         ])->create(); | ||||
|  | ||||
|         VacationRequest::factory() | ||||
|             ->for($user) | ||||
|             ->for($currentYearPeriod) | ||||
|             ->create(["state" => WaitingForTechnical::class]); | ||||
|  | ||||
|         $this->artisan(SendVacationRequestSummariesToApprovers::class) | ||||
|             ->execute(); | ||||
|  | ||||
|         Notification::assertSentTo([$technicalApprover, $admin], VacationRequestsSummaryNotification::class); | ||||
|         Notification::assertNotSentTo([$user], VacationRequestsSummaryNotification::class); | ||||
|     } | ||||
|  | ||||
|     public function testSummariesAreNotSentIfThereAreNoWaitingForActionVacationRequests(): void | ||||
|     { | ||||
|         $currentYearPeriod = YearPeriod::current(); | ||||
|  | ||||
|         $user = User::factory([ | ||||
|             "role" => Role::Employee, | ||||
|         ])->create(); | ||||
|         $technicalApprover = User::factory([ | ||||
|             "role" => Role::TechnicalApprover, | ||||
|         ])->create(); | ||||
|         $admin = User::factory([ | ||||
|             "role" => Role::Administrator, | ||||
|         ])->create(); | ||||
|  | ||||
|         VacationRequest::factory() | ||||
|             ->for($user) | ||||
|             ->for($currentYearPeriod) | ||||
|             ->create(["state" => Approved::class]); | ||||
|  | ||||
|         VacationRequest::factory() | ||||
|             ->for($user) | ||||
|             ->for($currentYearPeriod) | ||||
|             ->create(["state" => Cancelled::class]); | ||||
|  | ||||
|         VacationRequest::factory() | ||||
|             ->for($user) | ||||
|             ->for($currentYearPeriod) | ||||
|             ->create(["state" => Rejected::class]); | ||||
|  | ||||
|         VacationRequest::factory() | ||||
|             ->for($user) | ||||
|             ->for($currentYearPeriod) | ||||
|             ->create(["state" => Created::class]); | ||||
|  | ||||
|         $this->artisan(SendVacationRequestSummariesToApprovers::class) | ||||
|             ->execute(); | ||||
|  | ||||
|         Notification::assertNotSentTo([$user, $technicalApprover, $admin], VacationRequestsSummaryNotification::class); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user