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:
Adrian Hopek
2022-05-11 09:03:20 +02:00
99 changed files with 3268 additions and 1529 deletions

View File

@@ -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"));
}
}

View File

@@ -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);
}
}

View File

@@ -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()));
}
}

View File

@@ -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([

View File

@@ -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());
}
}

View 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();
}
}

View File

@@ -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,
],

View File

@@ -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,
],

View File

@@ -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,
],

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
class Channels
{
public const MAIL = "mail";
public const SLACK = "slack";
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Notifications;
interface Notifiable
{
public function notify($instance);
}

View File

@@ -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,
]);
}

View File

@@ -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(),
]);
}
}

View File

@@ -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>");
}
/**

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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,
],
);
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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),

View File

@@ -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();

View File

@@ -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

View File

@@ -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();

View File

@@ -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)

View File

@@ -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");
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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());
}
}

View File

@@ -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,
])

View File

@@ -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()

View File

@@ -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.", [

View File

@@ -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";

View File

@@ -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(),

View File

@@ -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"),

View File

@@ -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"),
];
}
}

View File

@@ -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,
];

View File

@@ -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,
];
}
}

View 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");
}
}

View 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());
}
}

View 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:");
}
}

View 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
{
}

View 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:");
}
}

View 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");
}
}

View 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,
]);
}
}

View 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:");
}
}

View 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(),
];
}
}

View File

@@ -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})";
});
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Slack\Exceptions;
use Spatie\SlashCommand\Exceptions\SlackSlashCommandException;
class UserNotFoundException extends SlackSlashCommandException
{
}

View 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),
);
}
}

View 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());
}
}

View 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",
];
}
}

View 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),
);
}
}

View 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);
}
}

View 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));
}
}

View 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 [];
}
}

View 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",
];
}
}

View 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";
}
}

View 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, "<@", "|");
}
}

View 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();
}
}