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/CalendarGenerator.php b/app/Domain/CalendarGenerator.php index a4c9d3f..559269c 100644 --- a/app/Domain/CalendarGenerator.php +++ b/app/Domain/CalendarGenerator.php @@ -4,10 +4,9 @@ declare(strict_types=1); namespace Toby\Domain; -use Carbon\CarbonImmutable; -use Carbon\CarbonInterface; use Carbon\CarbonPeriod; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\Vacation; @@ -20,33 +19,16 @@ class CalendarGenerator ) { } - public function generate(YearPeriod $yearPeriod, string $month): array + public function generate(Carbon $month): array { - $date = CarbonImmutable::create($yearPeriod->year, $this->monthNameToNumber($month)); - $period = CarbonPeriod::create($date->startOfMonth(), $date->endOfMonth()); + $period = CarbonPeriod::create($month->copy()->startOfMonth(), $month->copy()->endOfMonth()); + $yearPeriod = YearPeriod::findByYear($month->year); + $holidays = $yearPeriod->holidays()->pluck("date"); return $this->generateCalendar($period, $holidays); } - protected function monthNameToNumber($name): int - { - return match ($name) { - default => CarbonInterface::JANUARY, - "february" => CarbonInterface::FEBRUARY, - "march" => CarbonInterface::MARCH, - "april" => CarbonInterface::APRIL, - "may" => CarbonInterface::MAY, - "june" => CarbonInterface::JUNE, - "july" => CarbonInterface::JULY, - "august" => CarbonInterface::AUGUST, - "september" => CarbonInterface::SEPTEMBER, - "october" => CarbonInterface::OCTOBER, - "november" => CarbonInterface::NOVEMBER, - "december" => CarbonInterface::DECEMBER, - }; - } - protected function generateCalendar(CarbonPeriod $period, Collection $holidays): array { $calendar = []; diff --git a/app/Domain/Enums/Month.php b/app/Domain/Enums/Month.php new file mode 100644 index 0000000..a6d2921 --- /dev/null +++ b/app/Domain/Enums/Month.php @@ -0,0 +1,53 @@ + CarbonInterface::JANUARY, + self::February => CarbonInterface::FEBRUARY, + self::March => CarbonInterface::MARCH, + self::April => CarbonInterface::APRIL, + self::May => CarbonInterface::MAY, + self::June => CarbonInterface::JUNE, + self::July => CarbonInterface::JULY, + self::August => CarbonInterface::AUGUST, + self::September => CarbonInterface::SEPTEMBER, + self::October => CarbonInterface::OCTOBER, + self::November => CarbonInterface::NOVEMBER, + self::December => CarbonInterface::DECEMBER, + }; + } + + public static function current(): Month + { + return Month::from(Str::lower(Carbon::now()->englishMonth)); + } + + public static function fromNameOrCurrent(string $name): Month + { + return Month::tryFrom($name) ?? Month::current(); + } +} 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 @@ +vacationRequest; + if ($vacationRequest->hasFlowSkipped()) { + $this->stateManager->approve($vacationRequest); + + return; + } + if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) { $this->stateManager->waitForTechnical($vacationRequest); diff --git a/app/Domain/Listeners/SendApprovedVacationRequestNotification.php b/app/Domain/Listeners/SendApprovedVacationRequestNotification.php new file mode 100644 index 0000000..ed4abe3 --- /dev/null +++ b/app/Domain/Listeners/SendApprovedVacationRequestNotification.php @@ -0,0 +1,34 @@ +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..56bd726 --- /dev/null +++ b/app/Domain/Listeners/SendCreatedVacationRequestNotification.php @@ -0,0 +1,27 @@ +vacationRequest; + + if ($vacationRequest->creator->is($vacationRequest->user)) { + $vacationRequest->user->notify(new VacationRequestCreatedNotification($vacationRequest)); + } else { + $vacationRequest->user->notify(new VacationRequestCreatedOnEmployeeBehalf($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/VacationRequestCreatedOnEmployeeBehalf.php b/app/Domain/Notifications/VacationRequestCreatedOnEmployeeBehalf.php new file mode 100644 index 0000000..cf54b6b --- /dev/null +++ b/app/Domain/Notifications/VacationRequestCreatedOnEmployeeBehalf.php @@ -0,0 +1,74 @@ + $this->vacationRequest, + ], + ); + return $this->buildMailMessage($url); + } + + protected function buildMailMessage(string $url): MailMessage + { + $creator = $this->vacationRequest->creator->fullName; + $user = $this->vacationRequest->user->first_name; + $title = $this->vacationRequest->name; + $type = $this->vacationRequest->type->label(); + $from = $this->vacationRequest->from->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 on your behalf", [ + "title" => $title, + ])) + ->line(__("The vacation request :title has been created correctly by user :creator on your behalf in the :appName.", [ + "title" => $title, + "appName" => $appName, + "creator" => $creator, + ])) + ->line(__("Vacation type: :type", [ + "type" => $type, + ])) + ->line(__("From :from to :to (number of days: :days)", [ + "from" => $from, + "to" => $to, + "days" => $days, + ])) + ->action(__("Click here for details"), $url); + } +} 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/TimesheetExport.php b/app/Domain/TimesheetExport.php new file mode 100644 index 0000000..f0ed480 --- /dev/null +++ b/app/Domain/TimesheetExport.php @@ -0,0 +1,37 @@ +users + ->map(fn(User $user) => new TimesheetPerUserSheet($user, $this->month)) + ->toArray(); + } + + public function forUsers(Collection $users): static + { + $this->users = $users; + + return $this; + } + + public function forMonth(Carbon $month): static + { + $this->month = $month; + + return $this; + } +} diff --git a/app/Domain/TimesheetPerUserSheet.php b/app/Domain/TimesheetPerUserSheet.php new file mode 100644 index 0000000..7563473 --- /dev/null +++ b/app/Domain/TimesheetPerUserSheet.php @@ -0,0 +1,222 @@ +user->fullName; + } + + public function headings(): array + { + $types = VacationType::cases(); + + $headings = [ + __("Date"), + __("Day of week"), + __("Start date"), + __("End date"), + __("Worked hours"), + ]; + + foreach ($types as $type) { + $headings[] = $type->label(); + } + + return $headings; + } + + public function generator(): Generator + { + $period = CarbonPeriod::create($this->month->copy()->startOfMonth(), $this->month->copy()->endOfMonth()); + $vacations = $this->getVacationsForPeriod($this->user, $period); + $holidays = $this->getHolidaysForPeriod($period); + + foreach ($period as $day) { + $vacationsForDay = $vacations->get($day->toDateString(), new Collection()); + $workedThisDay = $this->checkIfWorkedThisDay($day, $holidays, $vacationsForDay); + + $row = [ + Date::dateTimeToExcel($day), + $day->translatedFormat("l"), + $workedThisDay ? $this->toExcelTime(Carbon::createFromTime(static::START_HOUR)) : null, + $workedThisDay ? $this->toExcelTime(Carbon::createFromTime(static::END_HOUR)) : null, + $workedThisDay ? static::HOURS_PER_DAY : null, + ]; + + foreach (VacationType::cases() as $type) { + $row[] = $vacationsForDay->has($type->value) ? static::HOURS_PER_DAY : null; + } + + yield $row; + } + } + + public function styles(Worksheet $sheet): void + { + $lastRow = $sheet->getHighestRow(); + $lastColumn = $sheet->getHighestColumn(); + + $sheet->getStyle("A1:{$lastColumn}1") + ->getFont()->setBold(true); + + $sheet->getStyle("A1:{$lastColumn}1") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); + + $sheet->getStyle("A1:{$lastColumn}1") + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setRGB("D9D9D9"); + + $sheet->getStyle("C1:{$lastColumn}{$lastRow}") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER); + + $sheet->getStyle("A2:A{$lastRow}") + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_DATE_DDMMYYYY); + + $sheet->getStyle("C1:D{$lastRow}") + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_DATE_TIME3); + + $sheet->getStyle("A2:A{$lastRow}") + ->getFont() + ->setBold(true); + + for ($i = 2; $i < $lastRow; $i++) { + $date = Date::excelToDateTimeObject($sheet->getCell("A{$i}")->getValue()); + + if (Carbon::createFromInterface($date)->isWeekend()) { + $sheet->getStyle("A{$i}:{$lastColumn}{$i}") + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setRGB("FEE2E2"); + } + } + + $sheet->getStyle("A1:{$lastColumn}{$lastRow}") + ->getBorders() + ->getAllBorders() + ->setBorderStyle(Border::BORDER_THIN) + ->getColor() + ->setRGB("B7B7B7"); + } + + public static function afterSheet(AfterSheet $event): void + { + $sheet = $event->getSheet(); + $lastRow = $sheet->getDelegate()->getHighestRow(); + + $sheet->append([ + __("Sum:"), + null, + null, + null, + "=SUM(E2:E{$lastRow})", + ]); + + $lastRow++; + + $sheet->getDelegate()->getStyle("A{$lastRow}") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_RIGHT); + + $sheet->getDelegate()->getStyle("A{$lastRow}") + ->getFont() + ->setBold(true); + + $sheet->getDelegate()->getStyle("E{$lastRow}") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER); + + $sheet->getDelegate()->mergeCells("A{$lastRow}:D{$lastRow}"); + + $sheet->getDelegate()->getStyle("A{$lastRow}:E{$lastRow}") + ->getBorders() + ->getAllBorders() + ->setBorderStyle(Border::BORDER_THIN) + ->getColor() + ->setRGB("B7B7B7"); + } + + protected function getVacationsForPeriod(User $user, CarbonPeriod $period): Collection + { + return $user->vacations() + ->with("vacationRequest") + ->whereBetween("date", [$period->start, $period->end]) + ->whereRelation("vacationRequest", "state", VacationRequestState::Approved->value) + ->get() + ->groupBy( + [ + fn(Vacation $vacation) => $vacation->date->toDateString(), + fn(Vacation $vacation) => $vacation->vacationRequest->type->value, + ], + ); + } + + protected function getHolidaysForPeriod(CarbonPeriod $period): Collection + { + return Holiday::query() + ->whereBetween("date", [$period->start, $period->end]) + ->pluck("date"); + } + + protected function toExcelTime(Carbon $time): float + { + $excelTimestamp = Date::dateTimeToExcel($time); + $excelDate = floor($excelTimestamp); + + return $excelTimestamp - $excelDate; + } + + protected function checkIfWorkedThisDay(CarbonInterface $day, Collection $holidays, Collection $vacations): bool + { + return $day->isWeekday() && $holidays->doesntContain($day) && $vacations->isEmpty(); + } +} diff --git a/app/Domain/VacationRequestStateManager.php b/app/Domain/VacationRequestStateManager.php index 50e3929..624cee1 100644 --- a/app/Domain/VacationRequestStateManager.php +++ b/app/Domain/VacationRequestStateManager.php @@ -11,7 +11,10 @@ use Toby\Domain\Events\VacationRequestAcceptedByTechnical; use Toby\Domain\Events\VacationRequestApproved; use Toby\Domain\Events\VacationRequestCancelled; use Toby\Domain\Events\VacationRequestCreated; +use Toby\Domain\Events\VacationRequestRejected; use Toby\Domain\Events\VacationRequestStateChanged; +use Toby\Domain\Events\VacationRequestWaitsForAdminApproval; +use Toby\Domain\Events\VacationRequestWaitsForTechApproval; use Toby\Domain\States\VacationRequest\AcceptedByAdministrative; use Toby\Domain\States\VacationRequest\AcceptedByTechnical; use Toby\Domain\States\VacationRequest\Approved; @@ -48,6 +51,7 @@ class VacationRequestStateManager public function reject(VacationRequest $vacationRequest, ?User $user = null): void { $this->changeState($vacationRequest, Rejected::class, $user); + $this->dispatcher->dispatch(new VacationRequestRejected($vacationRequest)); } public function cancel(VacationRequest $vacationRequest, ?User $user = null): void @@ -74,11 +78,15 @@ class VacationRequestStateManager public function waitForTechnical(VacationRequest $vacationRequest, ?User $user = null): void { $this->changeState($vacationRequest, WaitingForTechnical::class, $user); + + $this->dispatcher->dispatch(new VacationRequestWaitsForTechApproval($vacationRequest)); } public function waitForAdministrative(VacationRequest $vacationRequest, ?User $user = null): void { $this->changeState($vacationRequest, WaitingForAdministrative::class, $user); + + $this->dispatcher->dispatch(new VacationRequestWaitsForAdminApproval($vacationRequest)); } protected function changeState(VacationRequest $vacationRequest, string $state, ?User $user = null): void diff --git a/app/Eloquent/Models/User.php b/app/Eloquent/Models/User.php index 1f8c6e3..eafa9aa 100644 --- a/app/Eloquent/Models/User.php +++ b/app/Eloquent/Models/User.php @@ -58,6 +58,11 @@ class User extends Authenticatable return $this->hasMany(VacationRequest::class); } + public function createdVacationRequests(): HasMany + { + return $this->hasMany(VacationRequest::class, "creator_id"); + } + public function vacations(): HasMany { return $this->hasMany(Vacation::class); diff --git a/app/Eloquent/Models/VacationRequest.php b/app/Eloquent/Models/VacationRequest.php index ea4353f..0b36764 100644 --- a/app/Eloquent/Models/VacationRequest.php +++ b/app/Eloquent/Models/VacationRequest.php @@ -23,7 +23,9 @@ use Toby\Domain\States\VacationRequest\VacationRequestState; * @property Carbon $from * @property Carbon $to * @property string $comment + * @property bool $flow_skipped * @property User $user + * @property User $creator * @property YearPeriod $yearPeriod * @property Collection $activities * @property Collection $vacations @@ -49,6 +51,11 @@ class VacationRequest extends Model return $this->belongsTo(User::class); } + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, "creator_id"); + } + public function yearPeriod(): BelongsTo { return $this->belongsTo(YearPeriod::class); @@ -80,6 +87,11 @@ class VacationRequest extends Model ->where("to", ">=", $vacationRequest->from); } + public function hasFlowSkipped(): bool + { + return $this->flow_skipped; + } + protected static function newFactory(): VacationRequestFactory { return VacationRequestFactory::new(); diff --git a/app/Eloquent/Observers/VacationRequestObserver.php b/app/Eloquent/Observers/VacationRequestObserver.php index 35b29da..4095ce2 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\Eloquent\Models\VacationRequest; class VacationRequestObserver diff --git a/app/Infrastructure/Http/Controllers/TimesheetController.php b/app/Infrastructure/Http/Controllers/TimesheetController.php new file mode 100644 index 0000000..9f7fcd7 --- /dev/null +++ b/app/Infrastructure/Http/Controllers/TimesheetController.php @@ -0,0 +1,35 @@ +selected(); + $carbonMonth = Carbon::create($yearPeriod->year, $month->toCarbonNumber()); + + $users = User::query() + ->orderBy("last_name") + ->orderBy("first_name") + ->get(); + + $filename = "{$carbonMonth->translatedFormat("F Y")}.xlsx"; + + $timesheet = (new TimesheetExport()) + ->forMonth($carbonMonth) + ->forUsers($users); + + return Excel::download($timesheet, $filename); + } +} diff --git a/app/Infrastructure/Http/Controllers/VacationCalendarController.php b/app/Infrastructure/Http/Controllers/VacationCalendarController.php index d37cce6..ec7a33a 100644 --- a/app/Infrastructure/Http/Controllers/VacationCalendarController.php +++ b/app/Infrastructure/Http/Controllers/VacationCalendarController.php @@ -4,11 +4,10 @@ declare(strict_types=1); namespace Toby\Infrastructure\Http\Controllers; -use Illuminate\Http\Request; use Illuminate\Support\Carbon; -use Illuminate\Support\Str; use Inertia\Response; use Toby\Domain\CalendarGenerator; +use Toby\Domain\Enums\Month; use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\User; use Toby\Infrastructure\Http\Resources\UserResource; @@ -16,22 +15,25 @@ use Toby\Infrastructure\Http\Resources\UserResource; class VacationCalendarController extends Controller { public function index( - Request $request, YearPeriodRetriever $yearPeriodRetriever, CalendarGenerator $calendarGenerator, + ?string $month = null, ): Response { - $month = Str::lower($request->query("month", Carbon::now()->englishMonth)); + $month = Month::fromNameOrCurrent((string)$month); + $yearPeriod = $yearPeriodRetriever->selected(); + $carbonMonth = Carbon::create($yearPeriod->year, $month->toCarbonNumber()); + $users = User::query() ->orderBy("last_name") ->orderBy("first_name") ->get(); - $calendar = $calendarGenerator->generate($yearPeriod, $month); + $calendar = $calendarGenerator->generate($carbonMonth); return inertia("Calendar", [ "calendar" => $calendar, - "currentMonth" => $month, + "currentMonth" => $month->value, "users" => UserResource::collection($users), ]); } diff --git a/app/Infrastructure/Http/Controllers/VacationRequestController.php b/app/Infrastructure/Http/Controllers/VacationRequestController.php index d432be7..08aa8f6 100644 --- a/app/Infrastructure/Http/Controllers/VacationRequestController.php +++ b/app/Infrastructure/Http/Controllers/VacationRequestController.php @@ -8,7 +8,6 @@ use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response as LaravelResponse; -use Illuminate\Support\Arr; use Inertia\Response; use Toby\Domain\Enums\Role; use Toby\Domain\Enums\VacationType; @@ -21,8 +20,10 @@ use Toby\Domain\VacationRequestStateManager; use Toby\Domain\VacationRequestStatesRetriever; use Toby\Domain\Validation\VacationRequestValidator; use Toby\Eloquent\Helpers\YearPeriodRetriever; +use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\VacationRequest; use Toby\Infrastructure\Http\Requests\VacationRequestRequest; +use Toby\Infrastructure\Http\Resources\UserResource; use Toby\Infrastructure\Http\Resources\VacationRequestActivityResource; use Toby\Infrastructure\Http\Resources\VacationRequestResource; @@ -61,10 +62,10 @@ class VacationRequestController extends Controller "acceptAsAdministrative" => $vacationRequest->state->canTransitionTo(AcceptedByAdministrative::class) && $user === Role::AdministrativeApprover, "reject" => $vacationRequest->state->canTransitionTo(Rejected::class) - && in_array($user->role, [Role::TechnicalApprover, Role::AdministrativeApprover]), + && in_array($user->role, [Role::TechnicalApprover, Role::AdministrativeApprover], true), "cancel" => $vacationRequest->state->canTransitionTo(Cancelled::class) && $user === Role::AdministrativeApprover, - ] + ], ]); } @@ -79,8 +80,14 @@ class VacationRequestController extends Controller public function create(): Response { + $users = User::query() + ->orderBy("last_name") + ->orderBy("first_name") + ->get(); + return inertia("VacationRequest/Create", [ "vacationTypes" => VacationType::casesToSelect(), + "users" => UserResource::collection($users), ]); } @@ -91,7 +98,7 @@ class VacationRequestController extends Controller VacationDaysCalculator $vacationDaysCalculator, ): RedirectResponse { /** @var VacationRequest $vacationRequest */ - $vacationRequest = $request->user()->vacationRequests()->make($request->data()); + $vacationRequest = $request->user()->createdVacationRequests()->make($request->data()); $vacationRequestValidator->validate($vacationRequest); $vacationRequest->save(); diff --git a/app/Infrastructure/Http/Requests/VacationRequestRequest.php b/app/Infrastructure/Http/Requests/VacationRequestRequest.php index 3fd9e9f..f69ef10 100644 --- a/app/Infrastructure/Http/Requests/VacationRequestRequest.php +++ b/app/Infrastructure/Http/Requests/VacationRequestRequest.php @@ -16,9 +16,11 @@ class VacationRequestRequest extends FormRequest public function rules(): array { return [ + "user" => ["required", "exists:users,id"], "type" => ["required", new Enum(VacationType::class)], "from" => ["required", "date_format:Y-m-d", new YearPeriodExists()], "to" => ["required", "date_format:Y-m-d", new YearPeriodExists()], + "flowSkipped" => ["nullable", "boolean"], "comment" => ["nullable"], ]; } @@ -28,11 +30,13 @@ class VacationRequestRequest extends FormRequest $from = $this->get("from"); return [ + "user_id" => $this->get("user"), "type" => $this->get("type"), "from" => $from, "to" => $this->get("to"), "year_period_id" => YearPeriod::findByYear(Carbon::create($from)->year)->id, "comment" => $this->get("comment"), + "flow_skipped" => $this->boolean("flowSkipped"), ]; } } diff --git a/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php b/app/Infrastructure/Http/Resources/VacationRequestActivityResource.php index fb9ff70..976050e 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/composer.json b/composer.json index 1d5857d..2abe7c9 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "laravel/tinker": "^2.5", "lasserafn/php-initial-avatar-generator": "^4.2", "spatie/laravel-google-calendar": "^3.5", - "spatie/laravel-model-states": "^2.1" + "spatie/laravel-model-states": "^2.1", + "maatwebsite/excel": "^3.1" }, "require-dev": { "blumilksoftware/codestyle": "^0.9.0", diff --git a/composer.lock b/composer.lock index b266227..71aae96 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3fdff95b32e84ce3b115bc3bc7289cf4", + "content-hash": "c090431972dc8bfbe198ce9fc9f816f3", "packages": [ { "name": "asm89/stack-cors", @@ -709,6 +709,57 @@ ], "time": "2021-10-11T09:18:27+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.14.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/12ab42bd6e742c70c0a52f7b82477fcd44e64b75", + "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + }, + "time": "2021-12-25T01:21:49+00:00" + }, { "name": "facade/ignition-contracts", "version": "1.0.2", @@ -821,25 +872,23 @@ }, { "name": "fruitcake/laravel-cors", - "version": "v2.0.5", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/fruitcake/laravel-cors.git", - "reference": "3a066e5cac32e2d1cdaacd6b961692778f37b5fc" + "reference": "783a74f5e3431d7b9805be8afb60fd0a8f743534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/laravel-cors/zipball/3a066e5cac32e2d1cdaacd6b961692778f37b5fc", - "reference": "3a066e5cac32e2d1cdaacd6b961692778f37b5fc", + "url": "https://api.github.com/repos/fruitcake/laravel-cors/zipball/783a74f5e3431d7b9805be8afb60fd0a8f743534", + "reference": "783a74f5e3431d7b9805be8afb60fd0a8f743534", "shasum": "" }, "require": { "asm89/stack-cors": "^2.0.1", "illuminate/contracts": "^6|^7|^8|^9", "illuminate/support": "^6|^7|^8|^9", - "php": ">=7.2", - "symfony/http-foundation": "^4|^5|^6", - "symfony/http-kernel": "^4.3.4|^5|^6" + "php": ">=7.2" }, "require-dev": { "laravel/framework": "^6|^7.24|^8", @@ -850,7 +899,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" }, "laravel": { "providers": [ @@ -886,7 +935,7 @@ ], "support": { "issues": "https://github.com/fruitcake/laravel-cors/issues", - "source": "https://github.com/fruitcake/laravel-cors/tree/v2.0.5" + "source": "https://github.com/fruitcake/laravel-cors/tree/v2.2.0" }, "funding": [ { @@ -898,7 +947,78 @@ "type": "github" } ], - "time": "2022-01-03T14:53:04+00:00" + "time": "2022-02-23T14:25:13+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "58571acbaa5f9f462c9c77e911700ac66f446d4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/58571acbaa5f9f462c9c77e911700ac66f446d4e", + "reference": "58571acbaa5f9f462c9c77e911700ac66f446d4e", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2022-02-20T15:07:15+00:00" }, { "name": "google/apiclient", @@ -973,7 +1093,7 @@ }, { "name": "google/apiclient-services", - "version": "v0.235.0", + "version": "v0.236.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client-services.git", @@ -1011,7 +1131,7 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client-services/issues", - "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.235.0" + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.236.0" }, "time": "2022-02-07T14:04:26+00:00" }, @@ -1284,12 +1404,12 @@ } }, "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1490,12 +1610,12 @@ } }, "autoload": { - "psr-4": { - "Inertia\\": "src" - }, "files": [ "./helpers.php" - ] + ], + "psr-4": { + "Inertia\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1611,16 +1731,16 @@ }, { "name": "laravel/framework", - "version": "v9.1.0", + "version": "v9.2.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "ca7ddd4782f120ae50569d04eb9e40e52f67a9d9" + "reference": "13372872bed31ae75df8709b9de5cde01d50646e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/ca7ddd4782f120ae50569d04eb9e40e52f67a9d9", - "reference": "ca7ddd4782f120ae50569d04eb9e40e52f67a9d9", + "url": "https://api.github.com/repos/laravel/framework/zipball/13372872bed31ae75df8709b9de5cde01d50646e", + "reference": "13372872bed31ae75df8709b9de5cde01d50646e", "shasum": "" }, "require": { @@ -1629,6 +1749,7 @@ "egulias/email-validator": "^3.1", "ext-mbstring": "*", "ext-openssl": "*", + "fruitcake/php-cors": "^1.2", "laravel/serializable-closure": "^1.0", "league/commonmark": "^2.2", "league/flysystem": "^3.0", @@ -1703,7 +1824,7 @@ "league/flysystem-ftp": "^3.0", "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.4.4", - "orchestra/testbench-core": "^7.0", + "orchestra/testbench-core": "^7.1", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.0", "phpunit/phpunit": "^9.5.8", @@ -1785,20 +1906,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-02-15T15:06:44+00:00" + "time": "2022-02-22T15:30:23+00:00" }, { "name": "laravel/sanctum", - "version": "v2.14.1", + "version": "v2.14.2", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "89937617fa144ddb759a740861a47c4f2fd2245b" + "reference": "dc5d749ba9bfcfd68d8f5c272238f88bea223e66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/89937617fa144ddb759a740861a47c4f2fd2245b", - "reference": "89937617fa144ddb759a740861a47c4f2fd2245b", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/dc5d749ba9bfcfd68d8f5c272238f88bea223e66", + "reference": "dc5d749ba9bfcfd68d8f5c272238f88bea223e66", "shasum": "" }, "require": { @@ -1849,7 +1970,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2022-02-15T08:08:57+00:00" + "time": "2022-02-16T14:40:23+00:00" }, { "name": "laravel/serializable-closure", @@ -2466,16 +2587,16 @@ }, { "name": "league/flysystem", - "version": "3.0.8", + "version": "3.0.9", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "30f2c7069b2625da5b126ac66cbea7618a3db8b6" + "reference": "fb0801a60b7f9ea4188f01c25cb48aed26db7fb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/30f2c7069b2625da5b126ac66cbea7618a3db8b6", - "reference": "30f2c7069b2625da5b126ac66cbea7618a3db8b6", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/fb0801a60b7f9ea4188f01c25cb48aed26db7fb6", + "reference": "fb0801a60b7f9ea4188f01c25cb48aed26db7fb6", "shasum": "" }, "require": { @@ -2535,7 +2656,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.0.8" + "source": "https://github.com/thephpleague/flysystem/tree/3.0.9" }, "funding": [ { @@ -2551,7 +2672,7 @@ "type": "tidelift" } ], - "time": "2022-02-16T18:51:54+00:00" + "time": "2022-02-22T07:37:40+00:00" }, { "name": "league/mime-type-detection", @@ -2685,6 +2806,262 @@ }, "time": "2021-08-15T23:05:49+00:00" }, + { + "name": "maatwebsite/excel", + "version": "3.1.36", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "eb31f30d72c51c3fb11644b636945accbe50404f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/eb31f30d72c51c3fb11644b636945accbe50404f", + "reference": "eb31f30d72c51c3fb11644b636945accbe50404f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/support": "5.8.*|^6.0|^7.0|^8.0|^9.0", + "php": "^7.0|^8.0", + "phpoffice/phpspreadsheet": "^1.18" + }, + "require-dev": { + "orchestra/testbench": "^6.0|^7.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ], + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + } + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.36" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2022-01-27T18:34:20+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58", + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58", + "shasum": "" + }, + "require": { + "myclabs/php-enum": "^1.5", + "php": ">= 7.1", + "psr/http-message": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "ext-zip": "*", + "guzzlehttp/guzzle": ">= 6.3", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": ">= 7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/master" + }, + "funding": [ + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" + } + ], + "time": "2020-05-30T13:11:16+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "ab8bc271e404909db09ff2d5ffa1e538085c0f22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/ab8bc271e404909db09ff2d5ffa1e538085c0f22", + "reference": "ab8bc271e404909db09ff2d5ffa1e538085c0f22", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.1" + }, + "time": "2021-06-29T15:32:53+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "c66aefcafb4f6c269510e9ac46b82619a904c576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/c66aefcafb4f6c269510e9ac46b82619a904c576", + "reference": "c66aefcafb4f6c269510e9ac46b82619a904c576", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.0" + }, + "time": "2021-07-01T19:01:15+00:00" + }, { "name": "meyfa/php-svg", "version": "v0.9.1", @@ -2834,6 +3211,66 @@ ], "time": "2021-10-01T21:08:31+00:00" }, + { + "name": "myclabs/php-enum", + "version": "1.8.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "b942d263c641ddb5190929ff840c68f78713e937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937", + "reference": "b942d263c641ddb5190929ff840c68f78713e937", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.3" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2021-07-05T08:18:36+00:00" + }, { "name": "nesbot/carbon", "version": "2.57.0", @@ -3413,6 +3850,110 @@ }, "time": "2021-12-17T14:08:35+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.22.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "3a9e29b4f386a08a151a33578e80ef1747037a48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3a9e29b4f386a08a151a33578e80ef1747037a48", + "reference": "3a9e29b4f386a08a151a33578e80ef1747037a48", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.13", + "maennchen/zipstream-php": "^2.1", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^7.3 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "dompdf/dompdf": "^1.0", + "friendsofphp/php-cs-fixer": "^3.2", + "jpgraph/jpgraph": "^4.0", + "mpdf/mpdf": "8.0.17", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.6", + "tecnickcom/tcpdf": "^6.4" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", + "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.22.0" + }, + "time": "2022-02-18T12:57:07+00:00" + }, { "name": "phpoption/phpoption", "version": "1.8.1", @@ -3959,25 +4500,25 @@ }, { "name": "psr/simple-cache", - "version": "3.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/simple-cache.git", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -3992,7 +4533,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interfaces for simple caching", @@ -4004,9 +4545,9 @@ "simple-cache" ], "support": { - "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + "source": "https://github.com/php-fig/simple-cache/tree/master" }, - "time": "2021-10-29T13:26:27+00:00" + "time": "2017-10-23T01:57:42+00:00" }, { "name": "psy/psysh", @@ -4508,16 +5049,16 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.11.1", + "version": "1.11.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "e933f14dae0b31b1f6a45aa769dbd97ce781031a" + "reference": "16a8de828e7f1f32d580c667e1de5bf2943abd6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/e933f14dae0b31b1f6a45aa769dbd97ce781031a", - "reference": "e933f14dae0b31b1f6a45aa769dbd97ce781031a", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/16a8de828e7f1f32d580c667e1de5bf2943abd6b", + "reference": "16a8de828e7f1f32d580c667e1de5bf2943abd6b", "shasum": "" }, "require": { @@ -4555,7 +5096,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.11.1" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.11.2" }, "funding": [ { @@ -4563,7 +5104,7 @@ "type": "github" } ], - "time": "2022-02-16T11:14:09+00:00" + "time": "2022-02-22T08:55:13+00:00" }, { "name": "symfony/console", @@ -7388,9 +7929,6 @@ "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" - }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", @@ -7578,16 +8116,16 @@ }, { "name": "phar-io/version", - "version": "3.1.1", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "15a90844ad40f127afd244c0cad228de2a80052a" + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/15a90844ad40f127afd244c0cad228de2a80052a", - "reference": "15a90844ad40f127afd244c0cad228de2a80052a", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { @@ -7623,9 +8161,9 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.1" + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2022-02-07T21:56:48+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { "name": "php-webdriver/webdriver", @@ -7921,16 +8459,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.10", + "version": "9.2.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687" + "reference": "deac8540cb7bd40b2b8cfa679b76202834fd04e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d5850aaf931743067f4bfc1ae4cbd06468400687", - "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/deac8540cb7bd40b2b8cfa679b76202834fd04e8", + "reference": "deac8540cb7bd40b2b8cfa679b76202834fd04e8", "shasum": "" }, "require": { @@ -7986,7 +8524,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.10" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.13" }, "funding": [ { @@ -7994,7 +8532,7 @@ "type": "github" } ], - "time": "2021-12-05T09:12:13+00:00" + "time": "2022-02-23T17:02:38+00:00" }, { "name": "phpunit/php-file-iterator", @@ -8239,16 +8777,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.13", + "version": "9.5.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "597cb647654ede35e43b137926dfdfef0fb11743" + "reference": "5ff8c545a50226c569310a35f4fa89d79f1ddfdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/597cb647654ede35e43b137926dfdfef0fb11743", - "reference": "597cb647654ede35e43b137926dfdfef0fb11743", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5ff8c545a50226c569310a35f4fa89d79f1ddfdc", + "reference": "5ff8c545a50226c569310a35f4fa89d79f1ddfdc", "shasum": "" }, "require": { @@ -8264,7 +8802,7 @@ "phar-io/version": "^3.0.2", "php": ">=7.3", "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.7", + "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -8326,7 +8864,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.16" }, "funding": [ { @@ -8338,7 +8876,7 @@ "type": "github" } ], - "time": "2022-01-24T07:33:35+00:00" + "time": "2022-02-23T17:10:58+00:00" }, { "name": "sebastian/cli-parser", @@ -9432,16 +9970,16 @@ }, { "name": "spatie/ignition", - "version": "1.0.4", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/spatie/ignition.git", - "reference": "617c41d1bf675d95a7bd9adc826ba93d43affe7f" + "reference": "6b7bb804f4834b080f5ac941f6ac6800a485011e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/ignition/zipball/617c41d1bf675d95a7bd9adc826ba93d43affe7f", - "reference": "617c41d1bf675d95a7bd9adc826ba93d43affe7f", + "url": "https://api.github.com/repos/spatie/ignition/zipball/6b7bb804f4834b080f5ac941f6ac6800a485011e", + "reference": "6b7bb804f4834b080f5ac941f6ac6800a485011e", "shasum": "" }, "require": { @@ -9499,7 +10037,7 @@ "type": "github" } ], - "time": "2022-02-16T09:02:42+00:00" + "time": "2022-02-17T21:40:47+00:00" }, { "name": "spatie/laravel-ignition", diff --git a/config/excel.php b/config/excel.php new file mode 100644 index 0000000..216664a --- /dev/null +++ b/config/excel.php @@ -0,0 +1,104 @@ + [ + "chunk_size" => 1000, + "pre_calculate_formulas" => false, + "strict_null_comparison" => false, + "csv" => [ + "delimiter" => ",", + "enclosure" => '"', + "line_ending" => PHP_EOL, + "use_bom" => false, + "include_separator_line" => false, + "excel_compatibility" => false, + "output_encoding" => "", + ], + "properties" => [ + "creator" => "", + "lastModifiedBy" => "", + "title" => "", + "description" => "", + "subject" => "", + "keywords" => "", + "category" => "", + "manager" => "", + "company" => "", + ], + ], + + "imports" => [ + "read_only" => true, + "ignore_empty" => false, + "heading_row" => [ + "formatter" => "slug", + ], + "csv" => [ + "delimiter" => ",", + "enclosure" => '"', + "escape_character" => "\\", + "contiguous" => false, + "input_encoding" => "UTF-8", + ], + "properties" => [ + "creator" => "", + "lastModifiedBy" => "", + "title" => "", + "description" => "", + "subject" => "", + "keywords" => "", + "category" => "", + "manager" => "", + "company" => "", + ], + ], + "extension_detector" => [ + "xlsx" => Excel::XLSX, + "xlsm" => Excel::XLSX, + "xltx" => Excel::XLSX, + "xltm" => Excel::XLSX, + "xls" => Excel::XLS, + "xlt" => Excel::XLS, + "ods" => Excel::ODS, + "ots" => Excel::ODS, + "slk" => Excel::SLK, + "xml" => Excel::XML, + "gnumeric" => Excel::GNUMERIC, + "htm" => Excel::HTML, + "html" => Excel::HTML, + "csv" => Excel::CSV, + "tsv" => Excel::TSV, + "pdf" => Excel::DOMPDF, + ], + "value_binder" => [ + "default" => DefaultValueBinder::class, + ], + + "cache" => [ + "driver" => "memory", + "batch" => [ + "memory_limit" => 60000, + ], + "illuminate" => [ + "store" => null, + ], + ], + "transactions" => [ + "handler" => "db", + "db" => [ + "connection" => null, + ], + ], + + "temporary_files" => [ + "local_path" => storage_path("framework/cache/laravel-excel"), + "remote_disk" => null, + "remote_prefix" => null, + "force_resync_remote" => null, + ], +]; 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/database/factories/VacationRequestFactory.php b/database/factories/VacationRequestFactory.php index fbf130b..f9689b7 100644 --- a/database/factories/VacationRequestFactory.php +++ b/database/factories/VacationRequestFactory.php @@ -24,8 +24,9 @@ class VacationRequestFactory extends Factory return [ "user_id" => User::factory(), + "creator_id" => fn(array $attributes): int => $attributes["user_id"], "year_period_id" => YearPeriod::factory(), - "name" => fn(array $attributes) => $this->generateName($attributes), + "name" => fn(array $attributes): string => $this->generateName($attributes), "type" => $this->faker->randomElement(VacationType::cases()), "state" => $this->faker->randomElement(VacationRequestStatesRetriever::all()), "from" => $from, diff --git a/database/migrations/2022_01_26_100039_create_vacation_requests_table.php b/database/migrations/2022_01_26_100039_create_vacation_requests_table.php index f5270da..d9efcc7 100644 --- a/database/migrations/2022_01_26_100039_create_vacation_requests_table.php +++ b/database/migrations/2022_01_26_100039_create_vacation_requests_table.php @@ -14,6 +14,7 @@ return new class() extends Migration { Schema::create("vacation_requests", function (Blueprint $table): void { $table->id(); $table->string("name"); + $table->foreignIdFor(User::class, "creator_id")->constrained("users")->cascadeOnDelete(); $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); $table->foreignIdFor(YearPeriod::class)->constrained()->cascadeOnDelete(); $table->string("type"); @@ -21,6 +22,7 @@ return new class() extends Migration { $table->date("from"); $table->date("to"); $table->text("comment")->nullable(); + $table->boolean("flow_skipped")->default(false); $table->timestamps(); }); } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 614d712..12390fa 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -77,6 +77,7 @@ class DatabaseSeeder extends Seeder VacationRequest::factory() ->count(10) ->for($user) + ->for($user, "creator") ->sequence(fn() => [ "year_period_id" => $yearPeriods->random()->id, ]) 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/Composables/yearPeriodInfo.js b/resources/js/Composables/yearPeriodInfo.js new file mode 100644 index 0000000..8425e0a --- /dev/null +++ b/resources/js/Composables/yearPeriodInfo.js @@ -0,0 +1,12 @@ +import {computed} from 'vue' +import {usePage} from '@inertiajs/inertia-vue3' + +export default function useCurrentYearPeriodInfo() { + const minDate = computed(() => new Date(usePage().props.value.years.current, 0, 1)) + const maxDate = computed(() => new Date(usePage().props.value.years.current, 11, 31)) + + return { + minDate, + maxDate, + } +} diff --git a/resources/js/Pages/Calendar.vue b/resources/js/Pages/Calendar.vue index 1415b0a..1a8e095 100644 --- a/resources/js/Pages/Calendar.vue +++ b/resources/js/Pages/Calendar.vue @@ -1,10 +1,20 @@ \ No newline at end of file + diff --git a/resources/lang/pl.json b/resources/lang/pl.json index d512566..98c46cf 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -42,7 +42,35 @@ "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.", + "Sum:": "Suma:", + "Date": "Data", + "Day of week": "Dzień tygodnia", + "Start date": "Data rozpoczęcia", + "End date": "Data zakończenia", + "Worked hours": "Liczba godzin", + "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.", + "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." } 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/routes/web.php b/routes/web.php index 59f3682..a94d8aa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use Toby\Infrastructure\Http\Controllers\GoogleController; use Toby\Infrastructure\Http\Controllers\HolidayController; use Toby\Infrastructure\Http\Controllers\LogoutController; use Toby\Infrastructure\Http\Controllers\SelectYearPeriodController; +use Toby\Infrastructure\Http\Controllers\TimesheetController; use Toby\Infrastructure\Http\Controllers\UserController; use Toby\Infrastructure\Http\Controllers\VacationCalendarController; use Toby\Infrastructure\Http\Controllers\VacationLimitController; @@ -24,8 +25,10 @@ Route::middleware("auth")->group(function (): void { Route::get("/vacation-limits", [VacationLimitController::class, "edit"]) ->name("vacation.limits"); - Route::get("/vacation-calendar", [VacationCalendarController::class, "index"]) + Route::get("/vacation-calendar/{month?}", [VacationCalendarController::class, "index"]) ->name("vacation.calendar"); + Route::get("/timesheet/{month}", TimesheetController::class) + ->name("timesheet"); Route::get("/vacation-limits", [VacationLimitController::class, "edit"])->name("vacation.limits"); Route::put("/vacation-limits", [VacationLimitController::class, "update"]); diff --git a/tests/Feature/VacationRequestTest.php b/tests/Feature/VacationRequestTest.php index 0118b80..9c7523e 100644 --- a/tests/Feature/VacationRequestTest.php +++ b/tests/Feature/VacationRequestTest.php @@ -6,10 +6,14 @@ 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\VacationType; +use Toby\Domain\Events\VacationRequestAcceptedByAdministrative; +use Toby\Domain\Events\VacationRequestAcceptedByTechnical; +use Toby\Domain\Events\VacationRequestApproved; +use Toby\Domain\Events\VacationRequestRejected; use Toby\Domain\PolishHolidaysRetriever; use Toby\Domain\States\VacationRequest\Approved; use Toby\Domain\States\VacationRequest\Rejected; @@ -19,7 +23,6 @@ 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 { @@ -31,8 +34,6 @@ class VacationRequestTest extends FeatureTestCase { parent::setUp(); - Bus::fake(); - $this->polishHolidaysRetriever = $this->app->make(PolishHolidaysRetriever::class); } @@ -72,6 +73,7 @@ class VacationRequestTest extends FeatureTestCase $this->actingAs($user) ->post("/vacation-requests", [ + "user" => $user->id, "type" => VacationType::Vacation->value, "from" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(), "to" => Carbon::create($currentYearPeriod->year, 2, 11)->toDateString(), @@ -91,8 +93,87 @@ class VacationRequestTest extends FeatureTestCase ]); } + public function testUserCanCreateVacationRequestOnEmployeeBehalf(): void + { + $creator = User::factory()->createQuietly(); + $user = User::factory()->createQuietly(); + + $currentYearPeriod = YearPeriod::current(); + + VacationLimit::factory([ + "days" => 20, + ]) + ->for($user) + ->for($currentYearPeriod) + ->create(); + + $this->actingAs($creator) + ->post("/vacation-requests", [ + "user" => $user->id, + "type" => VacationType::Vacation->value, + "from" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(), + "to" => Carbon::create($currentYearPeriod->year, 2, 11)->toDateString(), + "comment" => "Comment for the vacation request.", + ]) + ->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("vacation_requests", [ + "user_id" => $user->id, + "creator_id" => $creator->id, + "year_period_id" => $currentYearPeriod->id, + "name" => "1/" . $currentYearPeriod->year, + "type" => VacationType::Vacation->value, + "state" => WaitingForTechnical::$name, + "from" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(), + "to" => Carbon::create($currentYearPeriod->year, 2, 11)->toDateString(), + "comment" => "Comment for the vacation request.", + ]); + } + + public function testUserCanCreateVacationRequestOnEmployeeBehalfAndSkipAcceptanceFlow(): void + { + Event::fake(VacationRequestApproved::class); + + $creator = User::factory()->createQuietly(); + $user = User::factory()->createQuietly(); + + $currentYearPeriod = YearPeriod::current(); + + VacationLimit::factory([ + "days" => 20, + ]) + ->for($user) + ->for($currentYearPeriod) + ->create(); + + $this->actingAs($creator) + ->post("/vacation-requests", [ + "user" => $user->id, + "type" => VacationType::Vacation->value, + "from" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(), + "to" => Carbon::create($currentYearPeriod->year, 2, 11)->toDateString(), + "comment" => "Comment for the vacation request.", + "flowSkipped" => true, + ]) + ->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("vacation_requests", [ + "user_id" => $user->id, + "creator_id" => $creator->id, + "year_period_id" => $currentYearPeriod->id, + "name" => "1/" . $currentYearPeriod->year, + "type" => VacationType::Vacation->value, + "state" => Approved::$name, + "from" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(), + "to" => Carbon::create($currentYearPeriod->year, 2, 11)->toDateString(), + "comment" => "Comment for the vacation request.", + ]); + } + public function testTechnicalApproverCanApproveVacationRequest(): void { + Event::fake(VacationRequestAcceptedByTechnical::class); + $user = User::factory()->createQuietly(); $technicalApprover = User::factory()->createQuietly(); $currentYearPeriod = YearPeriod::current(); @@ -109,13 +190,13 @@ class VacationRequestTest extends FeatureTestCase ->post("/vacation-requests/{$vacationRequest->id}/accept-as-technical") ->assertSessionHasNoErrors(); - $this->assertDatabaseHas("vacation_requests", [ - "state" => WaitingForAdministrative::$name, - ]); + Event::assertDispatched(VacationRequestAcceptedByTechnical::class); } public function testAdministrativeApproverCanApproveVacationRequest(): void { + Event::fake(VacationRequestAcceptedByAdministrative::class); + $user = User::factory()->createQuietly(); $administrativeApprover = User::factory()->createQuietly(); @@ -132,15 +213,13 @@ class VacationRequestTest extends FeatureTestCase ->post("/vacation-requests/{$vacationRequest->id}/accept-as-administrative") ->assertSessionHasNoErrors(); - $this->assertDatabaseHas("vacation_requests", [ - "state" => Approved::$name, - ]); - - 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(); @@ -164,6 +243,7 @@ class VacationRequestTest extends FeatureTestCase ->post("/vacation-requests/{$vacationRequest->id}/reject") ->assertSessionHasNoErrors(); + Event::assertDispatched(VacationRequestRejected::class); $this->assertDatabaseHas("vacation_requests", [ "state" => Rejected::$name, ]); @@ -183,6 +263,7 @@ class VacationRequestTest extends FeatureTestCase $this->actingAs($user) ->post("/vacation-requests", [ + "user" => $user->id, "type" => VacationType::Vacation->value, "from" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(), "to" => Carbon::create($currentYearPeriod->year, 2, 11)->toDateString(), @@ -207,6 +288,7 @@ class VacationRequestTest extends FeatureTestCase $this->actingAs($user) ->post("/vacation-requests", [ + "user" => $user->id, "type" => VacationType::Vacation->value, "from" => Carbon::create($currentYearPeriod->year, 2, 5)->toDateString(), "to" => Carbon::create($currentYearPeriod->year, 2, 6)->toDateString(), @@ -238,6 +320,7 @@ class VacationRequestTest extends FeatureTestCase $this->actingAs($user) ->post("/vacation-requests", [ + "user" => $user->id, "type" => VacationType::Vacation->value, "from" => Carbon::create($currentYearPeriod->year, 4, 18)->toDateString(), "to" => Carbon::create($currentYearPeriod->year, 4, 18)->toDateString(), @@ -273,6 +356,7 @@ class VacationRequestTest extends FeatureTestCase $this->actingAs($user) ->post("/vacation-requests", [ + "user" => $user->id, "type" => VacationType::Vacation->value, "from" => Carbon::create($currentYearPeriod->year, 2, 1)->toDateString(), "to" => Carbon::create($currentYearPeriod->year, 2, 4)->toDateString(), @@ -280,8 +364,7 @@ class VacationRequestTest extends FeatureTestCase ]) ->assertSessionHasErrors([ "vacationRequest" => __("You have pending vacation request in this range."), - ]) - ; + ]); } public function testUserCannotCreateVacationRequestIfHeHasApprovedVacationRequestInThisRange(): void @@ -309,6 +392,7 @@ class VacationRequestTest extends FeatureTestCase $this->actingAs($user) ->post("/vacation-requests", [ + "user" => $user->id, "type" => VacationType::Vacation->value, "from" => Carbon::create($currentYearPeriod->year, 2, 1)->toDateString(), "to" => Carbon::create($currentYearPeriod->year, 2, 4)->toDateString(), @@ -325,6 +409,7 @@ class VacationRequestTest extends FeatureTestCase $currentYearPeriod = YearPeriod::current(); $this->actingAs($user) ->post("/vacation-requests", [ + "user" => $user->id, "type" => VacationType::Vacation->value, "from" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(), "to" => Carbon::create($currentYearPeriod->year, 2, 6)->toDateString(), @@ -342,6 +427,7 @@ class VacationRequestTest extends FeatureTestCase $nextYearPeriod = $this->createYearPeriod(Carbon::now()->year + 1); $this->actingAs($user) ->post("/vacation-requests", [ + "user" => $user->id, "type" => VacationType::Vacation->value, "from" => Carbon::create($currentYearPeriod->year, 12, 27)->toDateString(), "to" => Carbon::create($nextYearPeriod->year, 1, 2)->toDateString(), diff --git a/tests/Unit/VacationRequestNotificationTest.php b/tests/Unit/VacationRequestNotificationTest.php new file mode 100644 index 0000000..0a59f51 --- /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" => Created::class, + "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); + } +} diff --git a/tests/Unit/VacationRequestStatesTest.php b/tests/Unit/VacationRequestStatesTest.php index bdb10f4..8713f84 100644 --- a/tests/Unit/VacationRequestStatesTest.php +++ b/tests/Unit/VacationRequestStatesTest.php @@ -6,6 +6,7 @@ namespace Tests\Unit; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Notification; use Tests\TestCase; use Tests\Traits\InteractsWithYearPeriods; use Toby\Domain\Enums\VacationType; @@ -28,6 +29,8 @@ class VacationRequestStatesTest extends TestCase { parent::setUp(); + Notification::fake(); + $this->stateManager = $this->app->make(VacationRequestStateManager::class); $this->createCurrentYearPeriod();