diff --git a/.env.example b/.env.example index a3ccf90..661c459 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_NAME="Toby HR applicaiton" +APP_NAME="Toby HR application" APP_ENV=local APP_KEY= APP_DEBUG=true diff --git a/app/Architecture/Providers/AppServiceProvider.php b/app/Architecture/Providers/AppServiceProvider.php index d7e58c9..b1dc896 100644 --- a/app/Architecture/Providers/AppServiceProvider.php +++ b/app/Architecture/Providers/AppServiceProvider.php @@ -15,6 +15,7 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y")); + Carbon::macro("toDisplayDate", fn() => $this->translatedFormat("d.m.Y")); $selectedYearPeriodScope = $this->app->make(SelectedYearPeriodScope::class); diff --git a/app/Architecture/Providers/EventServiceProvider.php b/app/Architecture/Providers/EventServiceProvider.php index 14bd1e2..3107b26 100644 --- a/app/Architecture/Providers/EventServiceProvider.php +++ b/app/Architecture/Providers/EventServiceProvider.php @@ -10,22 +10,34 @@ use Toby\Domain\Events\VacationRequestAcceptedByTechnical; use Toby\Domain\Events\VacationRequestApproved; use Toby\Domain\Events\VacationRequestCancelled; use Toby\Domain\Events\VacationRequestCreated; +use Toby\Domain\Events\VacationRequestRejected; use Toby\Domain\Events\VacationRequestStateChanged; +use Toby\Domain\Events\VacationRequestWaitsForAdminApproval; +use Toby\Domain\Events\VacationRequestWaitsForTechApproval; use Toby\Domain\Listeners\CreateVacationRequestActivity; use Toby\Domain\Listeners\HandleAcceptedByAdministrativeVacationRequest; use Toby\Domain\Listeners\HandleAcceptedByTechnicalVacationRequest; use Toby\Domain\Listeners\HandleApprovedVacationRequest; use Toby\Domain\Listeners\HandleCancelledVacationRequest; use Toby\Domain\Listeners\HandleCreatedVacationRequest; +use Toby\Domain\Listeners\SendApprovedVacationRequestNotification; +use Toby\Domain\Listeners\SendCancelledVacationRequestNotification; +use Toby\Domain\Listeners\SendCreatedVacationRequestNotification; +use Toby\Domain\Listeners\SendRejectedVacationRequestNotification; +use Toby\Domain\Listeners\SendWaitedForAdministrativeVacationRequestNotification; +use Toby\Domain\Listeners\SendWaitedForTechnicalVacationRequestNotification; class EventServiceProvider extends ServiceProvider { protected $listen = [ VacationRequestStateChanged::class => [CreateVacationRequestActivity::class], - VacationRequestCreated::class => [HandleCreatedVacationRequest::class], + VacationRequestCreated::class => [HandleCreatedVacationRequest::class, SendCreatedVacationRequestNotification::class], VacationRequestAcceptedByTechnical::class => [HandleAcceptedByTechnicalVacationRequest::class], VacationRequestAcceptedByAdministrative::class => [HandleAcceptedByAdministrativeVacationRequest::class], - VacationRequestApproved::class => [HandleApprovedVacationRequest::class], - VacationRequestCancelled::class => [HandleCancelledVacationRequest::class], + VacationRequestApproved::class => [HandleApprovedVacationRequest::class, SendApprovedVacationRequestNotification::class], + VacationRequestRejected::class => [SendRejectedVacationRequestNotification::class], + VacationRequestCancelled::class => [HandleCancelledVacationRequest::class, SendCancelledVacationRequestNotification::class], + VacationRequestWaitsForTechApproval::class => [SendWaitedForTechnicalVacationRequestNotification::class], + VacationRequestWaitsForAdminApproval::class => [SendWaitedForAdministrativeVacationRequestNotification::class], ]; } diff --git a/app/Domain/Events/VacationRequestRejected.php b/app/Domain/Events/VacationRequestRejected.php new file mode 100644 index 0000000..5378736 --- /dev/null +++ b/app/Domain/Events/VacationRequestRejected.php @@ -0,0 +1,20 @@ +getUsersForNotifications() as $user) { + $user->notify(new VacationRequestApprovedNotification($event->vacationRequest, $user)); + } + + $event->vacationRequest->user->notify(new VacationRequestApprovedNotification($event->vacationRequest, $event->vacationRequest->user)); + } + + protected function getUsersForNotifications(): Collection + { + return User::query() + ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover]) + ->get(); + } +} diff --git a/app/Domain/Listeners/SendCancelledVacationRequestNotification.php b/app/Domain/Listeners/SendCancelledVacationRequestNotification.php new file mode 100644 index 0000000..ec64eaf --- /dev/null +++ b/app/Domain/Listeners/SendCancelledVacationRequestNotification.php @@ -0,0 +1,34 @@ +getUsersForNotifications() as $user) { + $user->notify(new VacationRequestCancelledNotification($event->vacationRequest, $user)); + } + + $event->vacationRequest->user->notify(new VacationRequestCancelledNotification($event->vacationRequest, $event->vacationRequest->user)); + } + + protected function getUsersForNotifications(): Collection + { + return User::query() + ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover]) + ->get(); + } +} diff --git a/app/Domain/Listeners/SendCreatedVacationRequestNotification.php b/app/Domain/Listeners/SendCreatedVacationRequestNotification.php new file mode 100644 index 0000000..5aece51 --- /dev/null +++ b/app/Domain/Listeners/SendCreatedVacationRequestNotification.php @@ -0,0 +1,20 @@ +vacationRequest->user->notify(new VacationRequestCreatedNotification($event->vacationRequest)); + } +} diff --git a/app/Domain/Listeners/SendRejectedVacationRequestNotification.php b/app/Domain/Listeners/SendRejectedVacationRequestNotification.php new file mode 100644 index 0000000..3835d34 --- /dev/null +++ b/app/Domain/Listeners/SendRejectedVacationRequestNotification.php @@ -0,0 +1,34 @@ +getUsersForNotifications() as $user) { + $user->notify(new VacationRequestRejectedNotification($event->vacationRequest, $user)); + } + + $event->vacationRequest->user->notify(new VacationRequestRejectedNotification($event->vacationRequest, $event->vacationRequest->user)); + } + + protected function getUsersForNotifications(): Collection + { + return User::query() + ->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover]) + ->get(); + } +} diff --git a/app/Domain/Listeners/SendWaitedForAdministrativeVacationRequestNotification.php b/app/Domain/Listeners/SendWaitedForAdministrativeVacationRequestNotification.php new file mode 100644 index 0000000..0637521 --- /dev/null +++ b/app/Domain/Listeners/SendWaitedForAdministrativeVacationRequestNotification.php @@ -0,0 +1,32 @@ +getUsersForNotifications() as $user) { + $user->notify(new VacationRequestWaitsForAdminApprovalNotification($event->vacationRequest, $user)); + } + } + + protected function getUsersForNotifications(): Collection + { + return User::query() + ->where("role", [Role::AdministrativeApprover]) + ->get(); + } +} diff --git a/app/Domain/Listeners/SendWaitedForTechnicalVacationRequestNotification.php b/app/Domain/Listeners/SendWaitedForTechnicalVacationRequestNotification.php new file mode 100644 index 0000000..3a0ef75 --- /dev/null +++ b/app/Domain/Listeners/SendWaitedForTechnicalVacationRequestNotification.php @@ -0,0 +1,32 @@ +getUsersForNotifications() as $user) { + $user->notify(new VacationRequestWaitsForTechApprovalNotification($event->vacationRequest, $user)); + } + } + + protected function getUsersForNotifications(): Collection + { + return User::query() + ->where("role", [Role::TechnicalApprover]) + ->get(); + } +} diff --git a/app/Domain/Notifications/VacationRequestApprovedNotification.php b/app/Domain/Notifications/VacationRequestApprovedNotification.php new file mode 100644 index 0000000..0466062 --- /dev/null +++ b/app/Domain/Notifications/VacationRequestApprovedNotification.php @@ -0,0 +1,75 @@ + $this->vacationRequest, + ], + ); + + return $this->buildMailMessage($url); + } + + protected function buildMailMessage(string $url): MailMessage + { + $user = $this->user->first_name; + $title = $this->vacationRequest->name; + $type = $this->vacationRequest->type->label(); + $from = $this->vacationRequest->from->toDisplayDate(); + $to = $this->vacationRequest->to->toDisplayDate(); + $days = $this->vacationRequest->vacations()->count(); + $requester = $this->vacationRequest->user->fullName; + + return (new MailMessage()) + ->greeting(__("Hi :user!", [ + "user" => $user, + ])) + ->subject(__("Vacation request :title has been approved", [ + "title" => $title, + ])) + ->line(__("The vacation request :title for user :requester has been approved.", [ + "title" => $title, + "requester" => $requester, + ])) + ->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); + } +} diff --git a/app/Domain/Notifications/VacationRequestCancelledNotification.php b/app/Domain/Notifications/VacationRequestCancelledNotification.php new file mode 100644 index 0000000..2b19940 --- /dev/null +++ b/app/Domain/Notifications/VacationRequestCancelledNotification.php @@ -0,0 +1,75 @@ + $this->vacationRequest, + ], + ); + + return $this->buildMailMessage($url); + } + + protected function buildMailMessage(string $url): MailMessage + { + $user = $this->user->first_name; + $title = $this->vacationRequest->name; + $type = $this->vacationRequest->type->label(); + $from = $this->vacationRequest->from->toDisplayDate(); + $to = $this->vacationRequest->to->toDisplayDate(); + $days = $this->vacationRequest->vacations()->count(); + $requester = $this->vacationRequest->user->fullName; + + return (new MailMessage()) + ->greeting(__("Hi :user!", [ + "user" => $user, + ])) + ->subject(__("Vacation request :title has been cancelled", [ + "title" => $title, + ])) + ->line(__("The vacation request :title for user :requester has been cancelled.", [ + "title" => $title, + "requester" => $requester, + ])) + ->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); + } +} diff --git a/app/Domain/Notifications/VacationRequestCreatedNotification.php b/app/Domain/Notifications/VacationRequestCreatedNotification.php new file mode 100644 index 0000000..75c9549 --- /dev/null +++ b/app/Domain/Notifications/VacationRequestCreatedNotification.php @@ -0,0 +1,72 @@ + $this->vacationRequest, + ], + ); + return $this->buildMailMessage($url); + } + + protected function buildMailMessage(string $url): MailMessage + { + $user = $this->vacationRequest->user->first_name; + $title = $this->vacationRequest->name; + $type = $this->vacationRequest->type->label(); + $from = $this->vacationRequest->from->toDisplayDate(); + $to = $this->vacationRequest->to->toDisplayDate(); + $days = $this->vacationRequest->vacations()->count(); + $appName = config("app.name"); + + return (new MailMessage()) + ->greeting(__("Hi :user!", [ + "user" => $user, + ])) + ->subject(__("Vacation request :title has been created", [ + "title" => $title, + ])) + ->line(__("The vacation request :title has been created correctly in the :appName.", [ + "title" => $title, + "appName" => $appName, + ])) + ->line(__("Vacation type: :type", [ + "type" => $type, + ])) + ->line(__("From :from to :to (number of days: :days)", [ + "from" => $from, + "to" => $to, + "days" => $days, + ])) + ->action(__("Click here for details"), $url); + } +} diff --git a/app/Domain/Notifications/VacationRequestRejectedNotification.php b/app/Domain/Notifications/VacationRequestRejectedNotification.php new file mode 100644 index 0000000..98775cd --- /dev/null +++ b/app/Domain/Notifications/VacationRequestRejectedNotification.php @@ -0,0 +1,75 @@ + $this->vacationRequest, + ], + ); + + return $this->buildMailMessage($url); + } + + protected function buildMailMessage(string $url): MailMessage + { + $user = $this->user->first_name; + $title = $this->vacationRequest->name; + $type = $this->vacationRequest->type->label(); + $from = $this->vacationRequest->from->toDisplayDate(); + $to = $this->vacationRequest->to->toDisplayDate(); + $days = $this->vacationRequest->vacations()->count(); + $requester = $this->vacationRequest->user->fullName; + + return (new MailMessage()) + ->greeting(__("Hi :user!", [ + "user" => $user, + ])) + ->subject(__("Vacation request :title has been rejected", [ + "title" => $title, + ])) + ->line(__("The vacation request :title for user :requester has been rejected.", [ + "title" => $title, + "requester" => $requester, + ])) + ->line(__("Vacation type: :type", [ + "type" => $type, + ])) + ->line(__("From :from to :to (number of days: :days)", [ + "from" => $from, + "to" => $to, + "days" => $days, + ])) + ->action(__("Click here for details"), $url); + } +} diff --git a/app/Domain/Notifications/VacationRequestWaitsForAdminApprovalNotification.php b/app/Domain/Notifications/VacationRequestWaitsForAdminApprovalNotification.php new file mode 100644 index 0000000..2dc718c --- /dev/null +++ b/app/Domain/Notifications/VacationRequestWaitsForAdminApprovalNotification.php @@ -0,0 +1,75 @@ + $this->vacationRequest, + ], + ); + + return $this->buildMailMessage($url); + } + + protected function buildMailMessage(string $url): MailMessage + { + $user = $this->user->first_name; + $requester = $this->vacationRequest->user->fullName; + $title = $this->vacationRequest->name; + $type = $this->vacationRequest->type->label(); + $from = $this->vacationRequest->from->toDisplayDate(); + $to = $this->vacationRequest->to->toDisplayDate(); + $days = $this->vacationRequest->vacations()->count(); + + return (new MailMessage()) + ->greeting(__("Hi :user!", [ + "user" => $user, + ])) + ->subject(__("Vacation request :title is waiting for your approval", [ + "title" => $title, + ])) + ->line(__("The vacation request :title from user: :requester is waiting for your approval.", [ + "title" => $title, + "requester" => $requester, + ])) + ->line(__("Vacation type: :type", [ + "type" => $type, + ])) + ->line(__("From :from to :to (number of days: :days)", [ + "from" => $from, + "to" => $to, + "days" => $days, + ])) + ->action(__("Click here for details"), $url); + } +} diff --git a/app/Domain/Notifications/VacationRequestWaitsForTechApprovalNotification.php b/app/Domain/Notifications/VacationRequestWaitsForTechApprovalNotification.php new file mode 100644 index 0000000..703cbee --- /dev/null +++ b/app/Domain/Notifications/VacationRequestWaitsForTechApprovalNotification.php @@ -0,0 +1,75 @@ + $this->vacationRequest, + ], + ); + + return $this->buildMailMessage($url); + } + + protected function buildMailMessage(string $url): MailMessage + { + $user = $this->user->first_name; + $requester = $this->vacationRequest->user->fullName; + $title = $this->vacationRequest->name; + $type = $this->vacationRequest->type->label(); + $from = $this->vacationRequest->from->toDisplayDate(); + $to = $this->vacationRequest->to->toDisplayDate(); + $days = $this->vacationRequest->vacations()->count(); + + return (new MailMessage()) + ->greeting(__("Hi :user!", [ + "user" => $user, + ])) + ->subject(__("Vacation request :title is waiting for your approval", [ + "title" => $title, + ])) + ->line(__("The vacation request :title from user: :requester is waiting for your approval.", [ + "title" => $title, + "requester" => $requester, + ])) + ->line(__("Vacation type: :type", [ + "type" => $type, + ])) + ->line(__("From :from to :to (number of days: :days)", [ + "from" => $from, + "to" => $to, + "days" => $days, + ])) + ->action(__("Click here for details"), $url); + } +} diff --git a/app/Domain/VacationRequestStateManager.php b/app/Domain/VacationRequestStateManager.php index c3e3e2d..4256112 100644 --- a/app/Domain/VacationRequestStateManager.php +++ b/app/Domain/VacationRequestStateManager.php @@ -12,6 +12,9 @@ use Toby\Domain\Events\VacationRequestAcceptedByTechnical; use Toby\Domain\Events\VacationRequestApproved; use Toby\Domain\Events\VacationRequestCancelled; use Toby\Domain\Events\VacationRequestCreated; +use Toby\Domain\Events\VacationRequestRejected; +use Toby\Domain\Events\VacationRequestWaitsForAdminApproval; +use Toby\Domain\Events\VacationRequestWaitsForTechApproval; use Toby\Eloquent\Models\VacationRequest; class VacationRequestStateManager @@ -39,6 +42,8 @@ class VacationRequestStateManager public function reject(VacationRequest $vacationRequest): void { $this->changeState($vacationRequest, VacationRequestState::Rejected); + + $this->dispatcher->dispatch(new VacationRequestRejected($vacationRequest)); } public function cancel(VacationRequest $vacationRequest): void @@ -65,11 +70,15 @@ class VacationRequestStateManager public function waitForTechnical(VacationRequest $vacationRequest): void { $this->changeState($vacationRequest, VacationRequestState::WaitingForTechnical); + + $this->dispatcher->dispatch(new VacationRequestWaitsForTechApproval($vacationRequest)); } public function waitForAdministrative(VacationRequest $vacationRequest): void { $this->changeState($vacationRequest, VacationRequestState::WaitingForAdministrative); + + $this->dispatcher->dispatch(new VacationRequestWaitsForAdminApproval($vacationRequest)); } protected function changeState(VacationRequest $vacationRequest, VacationRequestState $state): void diff --git a/app/Eloquent/Observers/VacationRequestObserver.php b/app/Eloquent/Observers/VacationRequestObserver.php index eec46bc..ecbc5b1 100644 --- a/app/Eloquent/Observers/VacationRequestObserver.php +++ b/app/Eloquent/Observers/VacationRequestObserver.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Toby\Eloquent\Observers; use Illuminate\Contracts\Auth\Factory as Auth; -use Illuminate\Events\Dispatcher; +use Illuminate\Contracts\Events\Dispatcher; use Toby\Domain\Enums\VacationRequestState; use Toby\Domain\Events\VacationRequestStateChanged; use Toby\Eloquent\Models\User; diff --git a/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php b/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php index c513785..e751f89 100644 --- a/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php +++ b/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php @@ -13,7 +13,7 @@ class VacationRequestActivityResource extends JsonResource public function toArray($request): array { return [ - "date" => $this->created_at->format("d.m.Y"), + "date" => $this->created_at->toDisplayDate(), "time" => $this->created_at->format("H:i"), "user" => $this->user ? $this->user->fullName : __("System"), "state" => $this->to, diff --git a/config/mail.php b/config/mail.php index 6461e38..0bbffd9 100644 --- a/config/mail.php +++ b/config/mail.php @@ -35,7 +35,7 @@ return [ "name" => env("MAIL_FROM_NAME", "Example"), ], "markdown" => [ - "theme" => "default", + "theme" => "mail", "paths" => [ resource_path("views/vendor/mail"), ], diff --git a/package-lock.json b/package-lock.json index 958999c..1d8d8f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "toby", + "name": "application", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/resources/js/Shared/MainMenu.vue b/resources/js/Shared/MainMenu.vue index 5318860..cb75cdd 100644 --- a/resources/js/Shared/MainMenu.vue +++ b/resources/js/Shared/MainMenu.vue @@ -390,4 +390,4 @@ export default { } }, } - \ No newline at end of file + diff --git a/resources/lang/pl.json b/resources/lang/pl.json index d512566..2366fe2 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -42,7 +42,27 @@ "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 cancelled.": "Wniosek urlopowy został anulowany.", + "Hi :user!": "Cześć :user!", + "The vacation request :title has changed state to :state.": "Wniosek urlopowy :title zmienił status na :state.", + "Vacation request :title": "Wniosek urlopowy :title", + "Regards": "Z poważaniem", + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Jeżeli masz problemy z kliknięciem przycisku \":actionText\", skopiuj i wklej poniższy adres w pasek przeglądarki:", + "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.", + "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", + "Vacation request :title is waiting for your approval": "Wniosek urlopowy :title czeka na zaakceptowanie", + "The vacation request :title from user: :requester is waiting for your approval.": "Wniosek urlopowy :title od użytkownika :requester czeka na Twoją akceptację.", + "Vacation request :title has been approved": "Wniosek urlopowy :title został zatwierdzony", + "The vacation request :title for user :requester has been approved.": "Wniosek urlopowy :title od użytkownika :requester został zatwierdzony.", + "Vacation request :title has been cancelled": "Wniosek urlopowy :title został anulowany", + "The vacation request :title for user :requester has been cancelled.": "Wniosek urlopowy :title od użytkownika :requester został anulowany.", + "Vacation request :title has been rejected": "Wniosek urlopowy :title został odrzucony", + "The vacation request :title for user :requester has been rejected.": "Wniosek urlopowy :title od użytkownika :requester został odrzucony." } diff --git a/resources/views/vendor/mail/html/themes/mail.css b/resources/views/vendor/mail/html/themes/mail.css new file mode 100644 index 0000000..d2f67d3 --- /dev/null +++ b/resources/views/vendor/mail/html/themes/mail.css @@ -0,0 +1,286 @@ +/* Base */ + +body, body *:not(html):not(style):not(br):not(tr):not(code) { + font-family: Avenir, Helvetica, sans-serif; + box-sizing: border-box; +} + +body { + background-color: #f5f8fa; + color: #74787e; + height: 100%; + hyphens: auto; + line-height: 1.4; + margin: 0; + -moz-hyphens: auto; + -ms-word-break: break-all; + width: 100% !important; + -webkit-hyphens: auto; + -webkit-text-size-adjust: none; + word-break: break-all; + word-break: break-word; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: left; +} + +a { + color: #3c5f97; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #2F3133; + font-size: 19px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h2 { + color: #2F3133; + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h3 { + color: #2F3133; + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + color: #74787e; + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + background-color: #f5f8fa; + margin: 0; + padding: 0; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.content { + margin: 0; + padding: 0; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #3c5f97; + font-size: 19px; + font-weight: bold; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +/* Body */ + +.body { + background-color: #ffffff; + border-bottom: 1px solid #edeff2; + border-top: 1px solid #edeff2; + margin: 0; + padding: 0; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.inner-body { + background-color: #ffffff; + margin: 0 auto; + padding: 0; + width: 570px; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #edeff2; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 12px; +} + +/* Footer */ + +.footer { + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; +} + +.footer p { + color: #aeaeae; + font-size: 12px; + text-align: center; +} + +/* Tables */ + +.table table { + margin: 30px auto; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.table th { + color: #74787e; + border-bottom: 1px solid #edeff2; + padding-bottom: 8px; +} + +.table td { + color: #74787e; + font-size: 15px; + line-height: 18px; + padding: 10px 0; +} + +.content-cell { + padding: 35px; +} + +/* Buttons */ + +.action { + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.button { + border-radius: 3px; + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); + color: #ffffff; + display: inline-block; + text-decoration: none; + -webkit-text-size-adjust: none; +} + +.button-blue, .button-primary { + background-color: #3c5f97; + border-top: 10px solid #3c5f97; + border-right: 18px solid #3c5f97; + border-bottom: 10px solid #3c5f97; + border-left: 18px solid #3c5f97; +} + +.button-green, .button-success { + background-color: #22c55e; + border-top: 10px solid #22c55e; + border-right: 18px solid #22c55e; + border-bottom: 10px solid #22c55e; + border-left: 18px solid #22c55e; +} + +.button-red, .button-error { + background-color: #ef4444; + border-top: 10px solid #ef4444; + border-right: 18px solid #ef4444; + border-bottom: 10px solid #ef4444; + border-left: 18px solid #ef4444; +} + +/* Panels */ + +.panel { + margin: 0 0 21px; +} + +.panel-content { + background-color: #edeff2; + padding: 16px; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Promotions */ + +.promotion { + background-color: #FFFFFF; + border: 2px dashed #9BA2AB; + margin: 0; + margin-bottom: 25px; + margin-top: 25px; + padding: 24px; + width: 100%; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; +} + +.promotion h1 { + text-align: center; +} + +.promotion p { + font-size: 15px; + text-align: center; +} diff --git a/tests/Feature/VacationRequestTest.php b/tests/Feature/VacationRequestTest.php index 0841d1e..95c9c79 100644 --- a/tests/Feature/VacationRequestTest.php +++ b/tests/Feature/VacationRequestTest.php @@ -6,17 +6,19 @@ namespace Tests\Feature; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Event; use Inertia\Testing\AssertableInertia as Assert; use Tests\FeatureTestCase; use Toby\Domain\Enums\VacationRequestState; use Toby\Domain\Enums\VacationType; +use Toby\Domain\Events\VacationRequestAcceptedByAdministrative; +use Toby\Domain\Events\VacationRequestAcceptedByTechnical; +use Toby\Domain\Events\VacationRequestRejected; use Toby\Domain\PolishHolidaysRetriever; use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationLimit; use Toby\Eloquent\Models\VacationRequest; use Toby\Eloquent\Models\YearPeriod; -use Toby\Infrastructure\Jobs\SendVacationRequestDaysToGoogleCalendar; class VacationRequestTest extends FeatureTestCase { @@ -28,8 +30,6 @@ class VacationRequestTest extends FeatureTestCase { parent::setUp(); - Bus::fake(); - $this->polishHolidaysRetriever = $this->app->make(PolishHolidaysRetriever::class); } @@ -90,6 +90,8 @@ class VacationRequestTest extends FeatureTestCase public function testTechnicalApproverCanApproveVacationRequest(): void { + Event::fake(VacationRequestAcceptedByTechnical::class); + $user = User::factory()->createQuietly(); $technicalApprover = User::factory()->createQuietly(); $currentYearPeriod = YearPeriod::current(); @@ -106,13 +108,13 @@ class VacationRequestTest extends FeatureTestCase ->post("/vacation-requests/{$vacationRequest->id}/accept-as-technical") ->assertSessionHasNoErrors(); - $this->assertDatabaseHas("vacation_requests", [ - "state" => VacationRequestState::WaitingForAdministrative, - ]); + Event::assertDispatched(VacationRequestAcceptedByTechnical::class); } public function testAdministrativeApproverCanApproveVacationRequest(): void { + Event::fake(VacationRequestAcceptedByAdministrative::class); + $user = User::factory()->createQuietly(); $administrativeApprover = User::factory()->createQuietly(); @@ -129,20 +131,18 @@ class VacationRequestTest extends FeatureTestCase ->post("/vacation-requests/{$vacationRequest->id}/accept-as-administrative") ->assertSessionHasNoErrors(); - $this->assertDatabaseHas("vacation_requests", [ - "state" => VacationRequestState::Approved, - ]); - - Bus::assertDispatched(SendVacationRequestDaysToGoogleCalendar::class); + Event::assertDispatched(VacationRequestAcceptedByAdministrative::class); } public function testTechnicalApproverCanRejectVacationRequest(): void { + Event::fake(VacationRequestRejected::class); + $user = User::factory()->createQuietly(); $technicalApprover = User::factory()->createQuietly(); $currentYearPeriod = YearPeriod::current(); - $vacationLimit = VacationLimit::factory([ + VacationLimit::factory([ "days" => 20, ]) ->for($user) @@ -161,9 +161,7 @@ class VacationRequestTest extends FeatureTestCase ->post("/vacation-requests/{$vacationRequest->id}/reject") ->assertSessionHasNoErrors(); - $this->assertDatabaseHas("vacation_requests", [ - "state" => VacationRequestState::Rejected, - ]); + Event::assertDispatched(VacationRequestRejected::class); } public function testUserCannotCreateVacationRequestIfHeExceedsHisVacationLimit(): void diff --git a/tests/Unit/VacationRequestNotificationTest.php b/tests/Unit/VacationRequestNotificationTest.php new file mode 100644 index 0000000..b6e693f --- /dev/null +++ b/tests/Unit/VacationRequestNotificationTest.php @@ -0,0 +1,70 @@ +stateManager = $this->app->make(VacationRequestStateManager::class); + + $this->createCurrentYearPeriod(); + } + + public function testAfterChangingVacationRequestStateNotificationAreSentToUsers(): void + { + Notification::fake(); + + $user = User::factory([ + "role" => Role::Employee, + ])->createQuietly(); + $technicalApprover = User::factory([ + "role" => Role::TechnicalApprover, + ])->createQuietly(); + $administrativeApprover = User::factory([ + "role" => Role::AdministrativeApprover, + ])->createQuietly(); + + $currentYearPeriod = YearPeriod::current(); + + /** @var VacationRequest $vacationRequest */ + $vacationRequest = VacationRequest::factory([ + "type" => VacationType::Vacation->value, + "state" => VacationRequestState::Created, + "from" => Carbon::create($currentYearPeriod->year, 2, 1)->toDateString(), + "to" => Carbon::create($currentYearPeriod->year, 2, 4)->toDateString(), + "comment" => "Comment for the vacation request.", + ]) + ->for($user) + ->for($currentYearPeriod) + ->create(); + + $this->stateManager->waitForTechnical($vacationRequest); + + Notification::assertSentTo($technicalApprover, VacationRequestWaitsForTechApprovalNotification::class); + Notification::assertNotSentTo([$user, $administrativeApprover], VacationRequestWaitsForTechApprovalNotification::class); + } +}