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