Compare commits
57 Commits
#17-dusk-t
...
main
Author | SHA1 | Date | |
---|---|---|---|
643f546142 | |||
995c0b6696 | |||
9147b859d3 | |||
![]() |
2f9ef0ff12 | ||
![]() |
68e32ad930 | ||
![]() |
31a6d287c8 | ||
77d3b4df0d | |||
2fb2415203 | |||
fe639f264d | |||
![]() |
4b06e6c02b | ||
4309e8104b | |||
![]() |
3af92b2085 | ||
![]() |
0bebe2ecf1 | ||
![]() |
a3b8b18384 | ||
![]() |
431262dfb7 | ||
![]() |
7154caa340 | ||
![]() |
74f33d6c0b | ||
![]() |
88b1543071 | ||
![]() |
f4d928c6ae | ||
![]() |
497f47068c | ||
![]() |
e1f449fb52 | ||
![]() |
3404bf1da8 | ||
![]() |
94da727fe4 | ||
![]() |
06863b854a | ||
![]() |
6b2556c1da | ||
![]() |
c69866bb52 | ||
![]() |
d60dc75f99 | ||
![]() |
c95d08c861 | ||
![]() |
cc981b02b4 | ||
![]() |
459b62500e | ||
![]() |
4af0482a93 | ||
![]() |
ff8d6aade6 | ||
![]() |
84403a762a | ||
![]() |
6af4380fe6 | ||
![]() |
fa244b96cd | ||
![]() |
3ab02f1df4 | ||
![]() |
1ae23bd7cb | ||
![]() |
b52d206554 | ||
![]() |
172eab162d | ||
![]() |
794e8df3ea | ||
![]() |
b49fcadbba | ||
![]() |
08421b8a69 | ||
![]() |
ab16af1ca9 | ||
![]() |
fdbc374d7e | ||
![]() |
720d2c4e7b | ||
![]() |
957b07b3eb | ||
![]() |
dcda8c6255 | ||
![]() |
95f5ed44d6 | ||
![]() |
d8ac2bd61f | ||
![]() |
a0e60a3160 | ||
![]() |
8c1819aa01 | ||
![]() |
afb1a5e9ff | ||
![]() |
43870fa060 | ||
![]() |
0076c04e88 | ||
![]() |
287c6c19ab | ||
![]() |
8a54403318 | ||
![]() |
6d62c8b776 |
@ -57,6 +57,13 @@ DOCKER_INSTALL_XDEBUG=false
|
||||
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_CALENDAR_ENABLED=true
|
||||
GOOGLE_REDIRECT=http://localhost/login/google/end
|
||||
GOOGLE_CALENDAR_ID=
|
||||
LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE=
|
||||
|
||||
SLACK_URL=https://slack.com/api
|
||||
SLACK_ENABLED=true
|
||||
SLACK_CLIENT_TOKEN=
|
||||
SLACK_SIGNING_SECRET=
|
||||
SLACK_DEFAULT_CHANNEL="#general"
|
||||
|
@ -13,5 +13,8 @@ module.exports = {
|
||||
indent: ['error', 2],
|
||||
'vue/html-indent': ['error', 2],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'vue/require-default-prop': 0,
|
||||
'vue/multi-word-component-names': 0,
|
||||
},
|
||||
}
|
||||
|
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
||||
* @blumilksoftware/toby
|
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}}
|
2
.github/workflows/test-and-lint-php.yml
vendored
2
.github/workflows/test-and-lint-php.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
run: composer install --prefer-dist --no-interaction --no-suggest
|
||||
|
||||
- name: Run PHP linter
|
||||
run: composer ecs
|
||||
run: composer cs
|
||||
|
||||
- name: Execute tests
|
||||
run: php artisan test --env=ci
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,5 +15,6 @@ Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
google-credentials.json
|
||||
.idea/
|
||||
.composer
|
||||
|
3
Procfile
Normal file
3
Procfile
Normal file
@ -0,0 +1,3 @@
|
||||
web: vendor/bin/heroku-php-nginx -C environment/prod/nginx.conf public/
|
||||
release: php artisan config:cache && php artisan route:cache && php artisan migrate --force
|
||||
worker: php artisan queue:work
|
@ -5,6 +5,9 @@ declare(strict_types=1);
|
||||
namespace Toby\Architecture;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Throwable;
|
||||
|
||||
class ExceptionHandler extends Handler
|
||||
{
|
||||
@ -13,4 +16,36 @@ class ExceptionHandler extends Handler
|
||||
"password",
|
||||
"password_confirmation",
|
||||
];
|
||||
protected array $handleByInertia = [
|
||||
Response::HTTP_INTERNAL_SERVER_ERROR,
|
||||
Response::HTTP_SERVICE_UNAVAILABLE,
|
||||
Response::HTTP_TOO_MANY_REQUESTS,
|
||||
419, // CSRF
|
||||
Response::HTTP_NOT_FOUND,
|
||||
Response::HTTP_FORBIDDEN,
|
||||
Response::HTTP_UNAUTHORIZED,
|
||||
];
|
||||
|
||||
public function render($request, Throwable $e): Response
|
||||
{
|
||||
$response = parent::render($request, $e);
|
||||
|
||||
if (!app()->environment("production")) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($response->status() === Response::HTTP_METHOD_NOT_ALLOWED) {
|
||||
$response->setStatusCode(Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (in_array($response->status(), $this->handleByInertia, true)) {
|
||||
return Inertia::render("Error", [
|
||||
"status" => $response->status(),
|
||||
])
|
||||
->toResponse($request)
|
||||
->setStatusCode($response->status());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,9 @@ namespace Toby\Architecture\Providers;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Policies\KeyPolicy;
|
||||
use Toby\Domain\Policies\VacationRequestPolicy;
|
||||
use Toby\Eloquent\Models\Key;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
@ -15,6 +17,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $policies = [
|
||||
VacationRequest::class => VacationRequestPolicy::class,
|
||||
Key::class => KeyPolicy::class,
|
||||
];
|
||||
|
||||
public function boot(): void
|
||||
@ -27,9 +30,11 @@ 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("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);
|
||||
Gate::define("manageResumes", fn(User $user): bool => $user->role === Role::TechnicalApprover);
|
||||
}
|
||||
}
|
||||
|
@ -5,39 +5,8 @@ declare(strict_types=1);
|
||||
namespace Toby\Architecture\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
|
||||
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
|
||||
use Toby\Domain\Events\VacationRequestApproved;
|
||||
use Toby\Domain\Events\VacationRequestCancelled;
|
||||
use Toby\Domain\Events\VacationRequestCreated;
|
||||
use Toby\Domain\Events\VacationRequestRejected;
|
||||
use Toby\Domain\Events\VacationRequestStateChanged;
|
||||
use Toby\Domain\Events\VacationRequestWaitsForAdminApproval;
|
||||
use Toby\Domain\Events\VacationRequestWaitsForTechApproval;
|
||||
use Toby\Domain\Listeners\CreateVacationRequestActivity;
|
||||
use Toby\Domain\Listeners\HandleAcceptedByAdministrativeVacationRequest;
|
||||
use Toby\Domain\Listeners\HandleAcceptedByTechnicalVacationRequest;
|
||||
use Toby\Domain\Listeners\HandleApprovedVacationRequest;
|
||||
use Toby\Domain\Listeners\HandleCancelledVacationRequest;
|
||||
use Toby\Domain\Listeners\HandleCreatedVacationRequest;
|
||||
use Toby\Domain\Listeners\SendApprovedVacationRequestNotification;
|
||||
use Toby\Domain\Listeners\SendCancelledVacationRequestNotification;
|
||||
use Toby\Domain\Listeners\SendCreatedVacationRequestNotification;
|
||||
use Toby\Domain\Listeners\SendRejectedVacationRequestNotification;
|
||||
use Toby\Domain\Listeners\SendWaitedForAdministrativeVacationRequestNotification;
|
||||
use Toby\Domain\Listeners\SendWaitedForTechnicalVacationRequestNotification;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $listen = [
|
||||
VacationRequestStateChanged::class => [CreateVacationRequestActivity::class],
|
||||
VacationRequestCreated::class => [HandleCreatedVacationRequest::class, SendCreatedVacationRequestNotification::class],
|
||||
VacationRequestAcceptedByTechnical::class => [HandleAcceptedByTechnicalVacationRequest::class],
|
||||
VacationRequestAcceptedByAdministrative::class => [HandleAcceptedByAdministrativeVacationRequest::class],
|
||||
VacationRequestApproved::class => [HandleApprovedVacationRequest::class, SendApprovedVacationRequestNotification::class],
|
||||
VacationRequestRejected::class => [SendRejectedVacationRequestNotification::class],
|
||||
VacationRequestCancelled::class => [HandleCancelledVacationRequest::class, SendCancelledVacationRequestNotification::class],
|
||||
VacationRequestWaitsForTechApproval::class => [SendWaitedForTechnicalVacationRequestNotification::class],
|
||||
VacationRequestWaitsForAdminApproval::class => [SendWaitedForAdministrativeVacationRequestNotification::class],
|
||||
];
|
||||
protected $listen = [];
|
||||
}
|
||||
|
@ -7,17 +7,14 @@ namespace Toby\Architecture\Providers;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
use Toby\Eloquent\Models\YearPeriod;
|
||||
use Toby\Eloquent\Observers\UserObserver;
|
||||
use Toby\Eloquent\Observers\VacationRequestObserver;
|
||||
use Toby\Eloquent\Observers\YearPeriodObserver;
|
||||
|
||||
class ObserverServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
User::observe(UserObserver::class);
|
||||
YearPeriod::observe(YearPeriodObserver::class);
|
||||
VacationRequest::observe(VacationRequestObserver::class);
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
35
app/Domain/Actions/CreateUserAction.php
Normal file
35
app/Domain/Actions/CreateUserAction.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions;
|
||||
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\YearPeriod;
|
||||
|
||||
class CreateUserAction
|
||||
{
|
||||
public function execute(array $userData, array $profileData): User
|
||||
{
|
||||
$user = new User($userData);
|
||||
|
||||
$user->save();
|
||||
|
||||
$user->profile()->create($profileData);
|
||||
|
||||
$this->createVacationLimitsFor($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function createVacationLimitsFor(User $user): void
|
||||
{
|
||||
$yearPeriods = YearPeriod::all();
|
||||
|
||||
foreach ($yearPeriods as $yearPeriod) {
|
||||
$user->vacationLimits()->create([
|
||||
"year_period_id" => $yearPeriod->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,22 +2,30 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Observers;
|
||||
namespace Toby\Domain\Actions;
|
||||
|
||||
use Toby\Domain\PolishHolidaysRetriever;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\YearPeriod;
|
||||
|
||||
class YearPeriodObserver
|
||||
class CreateYearPeriodAction
|
||||
{
|
||||
public function __construct(
|
||||
protected PolishHolidaysRetriever $polishHolidaysRetriever,
|
||||
) {}
|
||||
|
||||
public function created(YearPeriod $yearPeriod): void
|
||||
public function execute(int $year): YearPeriod
|
||||
{
|
||||
$yearPeriod = new YearPeriod([
|
||||
"year" => $year,
|
||||
]);
|
||||
|
||||
$yearPeriod->save();
|
||||
|
||||
$this->createVacationLimitsFor($yearPeriod);
|
||||
$this->createHolidaysFor($yearPeriod);
|
||||
|
||||
return $yearPeriod;
|
||||
}
|
||||
|
||||
protected function createVacationLimitsFor(YearPeriod $yearPeriod): void
|
19
app/Domain/Actions/UpdateUserAction.php
Normal file
19
app/Domain/Actions/UpdateUserAction.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions;
|
||||
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class UpdateUserAction
|
||||
{
|
||||
public function execute(User $user, array $userData, array $profileData): User
|
||||
{
|
||||
$user->update($userData);
|
||||
|
||||
$user->profile->update($profileData);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions\VacationRequest;
|
||||
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class AcceptAsAdministrativeAction
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
protected ApproveAction $approveAction,
|
||||
) {}
|
||||
|
||||
public function execute(VacationRequest $vacationRequest, User $user): void
|
||||
{
|
||||
$this->stateManager->acceptAsAdministrative($vacationRequest, $user);
|
||||
|
||||
$this->approveAction->execute($vacationRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions\VacationRequest;
|
||||
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class AcceptAsTechnicalAction
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
protected WaitForAdminApprovalAction $waitForAdminApprovalAction,
|
||||
protected ApproveAction $approveAction,
|
||||
) {}
|
||||
|
||||
public function execute(VacationRequest $vacationRequest, User $user): void
|
||||
{
|
||||
$this->stateManager->acceptAsTechnical($vacationRequest, $user);
|
||||
|
||||
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
|
||||
$this->waitForAdminApprovalAction->execute($vacationRequest);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->approveAction->execute($vacationRequest);
|
||||
}
|
||||
}
|
46
app/Domain/Actions/VacationRequest/ApproveAction.php
Normal file
46
app/Domain/Actions/VacationRequest/ApproveAction.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions\VacationRequest;
|
||||
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Notifications\VacationRequestStatusChangedNotification;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
use Toby\Infrastructure\Jobs\SendVacationRequestDaysToGoogleCalendar;
|
||||
|
||||
class ApproveAction
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
) {}
|
||||
|
||||
public function execute(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
{
|
||||
$this->stateManager->approve($vacationRequest, $user);
|
||||
|
||||
if ($this->configRetriever->isVacation($vacationRequest->type)) {
|
||||
SendVacationRequestDaysToGoogleCalendar::dispatch($vacationRequest);
|
||||
|
||||
$this->notify($vacationRequest);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notify(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$users = User::query()
|
||||
->where("id", "!=", $vacationRequest->user->id)
|
||||
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator])
|
||||
->get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user));
|
||||
}
|
||||
|
||||
$vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user));
|
||||
}
|
||||
}
|
46
app/Domain/Actions/VacationRequest/CancelAction.php
Normal file
46
app/Domain/Actions/VacationRequest/CancelAction.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions\VacationRequest;
|
||||
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Notifications\VacationRequestStatusChangedNotification;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
use Toby\Infrastructure\Jobs\ClearVacationRequestDaysInGoogleCalendar;
|
||||
|
||||
class CancelAction
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
) {}
|
||||
|
||||
public function execute(VacationRequest $vacationRequest, User $user): void
|
||||
{
|
||||
$this->stateManager->cancel($vacationRequest, $user);
|
||||
|
||||
ClearVacationRequestDaysInGoogleCalendar::dispatch($vacationRequest);
|
||||
|
||||
if ($this->configRetriever->isVacation($vacationRequest->type)) {
|
||||
$this->notify($vacationRequest);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notify(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$users = User::query()
|
||||
->where("id", "!=", $vacationRequest->user->id)
|
||||
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator])
|
||||
->get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user));
|
||||
}
|
||||
|
||||
$vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user));
|
||||
}
|
||||
}
|
97
app/Domain/Actions/VacationRequest/CreateAction.php
Normal file
97
app/Domain/Actions/VacationRequest/CreateAction.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions\VacationRequest;
|
||||
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Toby\Domain\Notifications\VacationRequestCreatedNotification;
|
||||
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;
|
||||
|
||||
class CreateAction
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
protected VacationRequestValidator $vacationRequestValidator,
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
protected WorkDaysCalculator $workDaysCalculator,
|
||||
protected WaitForTechApprovalAction $waitForTechApprovalAction,
|
||||
protected WaitForAdminApprovalAction $waitForAdminApprovalAction,
|
||||
protected ApproveAction $approveAction,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function execute(array $data, User $creator): VacationRequest
|
||||
{
|
||||
$vacationRequest = $this->createVacationRequest($data, $creator);
|
||||
$this->handleCreatedVacationRequest($vacationRequest);
|
||||
|
||||
if ($this->configRetriever->isVacation($vacationRequest->type)) {
|
||||
$this->notify($vacationRequest);
|
||||
}
|
||||
|
||||
return $vacationRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function createVacationRequest(array $data, User $creator): VacationRequest
|
||||
{
|
||||
/** @var VacationRequest $vacationRequest */
|
||||
$vacationRequest = $creator->createdVacationRequests()->make($data);
|
||||
|
||||
$this->vacationRequestValidator->validate($vacationRequest);
|
||||
|
||||
$vacationRequest->save();
|
||||
|
||||
$days = $this->workDaysCalculator->calculateDays($vacationRequest->from, $vacationRequest->to);
|
||||
|
||||
foreach ($days as $day) {
|
||||
$vacationRequest->vacations()->create([
|
||||
"date" => $day,
|
||||
"user_id" => $vacationRequest->user->id,
|
||||
"year_period_id" => $vacationRequest->yearPeriod->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->stateManager->markAsCreated($vacationRequest);
|
||||
|
||||
return $vacationRequest;
|
||||
}
|
||||
|
||||
protected function handleCreatedVacationRequest(VacationRequest $vacationRequest): void
|
||||
{
|
||||
if ($vacationRequest->hasFlowSkipped()) {
|
||||
$this->approveAction->execute($vacationRequest);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) {
|
||||
$this->waitForTechApprovalAction->execute($vacationRequest);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
|
||||
$this->waitForAdminApprovalAction->execute($vacationRequest);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->stateManager->approve($vacationRequest);
|
||||
}
|
||||
|
||||
protected function notify(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$vacationRequest->user->notify(new VacationRequestCreatedNotification($vacationRequest));
|
||||
}
|
||||
}
|
39
app/Domain/Actions/VacationRequest/RejectAction.php
Normal file
39
app/Domain/Actions/VacationRequest/RejectAction.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions\VacationRequest;
|
||||
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Notifications\VacationRequestStatusChangedNotification;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class RejectAction
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
) {}
|
||||
|
||||
public function execute(VacationRequest $vacationRequest, User $user): void
|
||||
{
|
||||
$this->stateManager->reject($vacationRequest, $user);
|
||||
|
||||
$this->notify($vacationRequest);
|
||||
}
|
||||
|
||||
protected function notify(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$users = User::query()
|
||||
->where("id", "!=", $vacationRequest->user->id)
|
||||
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover, Role::Administrator])
|
||||
->get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $user));
|
||||
}
|
||||
|
||||
$vacationRequest->user->notify(new VacationRequestStatusChangedNotification($vacationRequest, $vacationRequest->user));
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions\VacationRequest;
|
||||
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Notifications\VacationRequestWaitsForApprovalNotification;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class WaitForAdminApprovalAction
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
protected ApproveAction $approveAction,
|
||||
) {}
|
||||
|
||||
public function execute(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$this->stateManager->waitForAdministrative($vacationRequest);
|
||||
|
||||
if ($this->configRetriever->isVacation($vacationRequest->type)) {
|
||||
$this->notifyAdminApprovers($vacationRequest);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyAdminApprovers(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$users = User::query()
|
||||
->whereIn("role", [Role::AdministrativeApprover, Role::Administrator])
|
||||
->get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new VacationRequestWaitsForApprovalNotification($vacationRequest, $user));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Actions\VacationRequest;
|
||||
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Notifications\VacationRequestWaitsForApprovalNotification;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class WaitForTechApprovalAction
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
protected ApproveAction $approveAction,
|
||||
) {}
|
||||
|
||||
public function execute(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$this->stateManager->waitForTechnical($vacationRequest);
|
||||
|
||||
if ($this->configRetriever->isVacation($vacationRequest->type)) {
|
||||
$this->notifyTechApprovers($vacationRequest);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyTechApprovers(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$users = User::query()
|
||||
->whereIn("role", [Role::TechnicalApprover, Role::Administrator])
|
||||
->get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->notify(new VacationRequestWaitsForApprovalNotification($vacationRequest, $user));
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Toby\Domain;
|
||||
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Eloquent\Helpers\YearPeriodRetriever;
|
||||
@ -44,6 +43,7 @@ class CalendarGenerator
|
||||
"isWeekend" => $day->isWeekend(),
|
||||
"isHoliday" => $holidays->contains($day),
|
||||
"vacations" => $vacationsForDay->pluck("user_id"),
|
||||
"vacationTypes" => $vacationsForDay->pluck("vacationRequest.type", "user_id"),
|
||||
];
|
||||
}
|
||||
|
||||
@ -54,8 +54,9 @@ class CalendarGenerator
|
||||
{
|
||||
return Vacation::query()
|
||||
->whereBetween("date", [$period->start, $period->end])
|
||||
->whereRelation("vacationRequest", fn(Builder $query) => $query->states(VacationRequestStatesRetriever::successStates()))
|
||||
->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,
|
||||
],
|
||||
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Enums;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
enum VacationType: string
|
||||
{
|
||||
case Vacation = "vacation";
|
||||
@ -15,6 +17,8 @@ enum VacationType: string
|
||||
case Volunteering = "volunteering_vacation";
|
||||
case TimeInLieu = "time_in_lieu";
|
||||
case Sick = "sick_vacation";
|
||||
case Absence = "absence";
|
||||
case HomeOffice = "home_office";
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
@ -23,13 +27,18 @@ enum VacationType: string
|
||||
|
||||
public static function casesToSelect(): array
|
||||
{
|
||||
$cases = collect(VacationType::cases());
|
||||
$cases = VacationType::all();
|
||||
|
||||
return $cases->map(
|
||||
fn(VacationType $enum) => [
|
||||
fn(VacationType $enum): array => [
|
||||
"label" => $enum->label(),
|
||||
"value" => $enum->value,
|
||||
],
|
||||
)->toArray();
|
||||
}
|
||||
|
||||
public static function all(): Collection
|
||||
{
|
||||
return new Collection(VacationType::cases());
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestAcceptedByAdministrative
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
) {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestAcceptedByTechnical
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
) {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestApproved
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
) {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestCancelled
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
) {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestCreated
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
) {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestRejected
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
) {}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Domain\States\VacationRequest\VacationRequestState;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestStateChanged
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
public ?VacationRequestState $from,
|
||||
public VacationRequestState $to,
|
||||
public ?User $user = null,
|
||||
) {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestWaitsForAdminApproval
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
) {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestWaitsForTechApproval
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public VacationRequest $vacationRequest,
|
||||
) {}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Toby\Domain\Events\VacationRequestStateChanged;
|
||||
|
||||
class CreateVacationRequestActivity
|
||||
{
|
||||
public function handle(VacationRequestStateChanged $event): void
|
||||
{
|
||||
$event->vacationRequest->activities()->create([
|
||||
"from" => $event->from,
|
||||
"to" => $event->to,
|
||||
"user_id" => $event->user?->id,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
|
||||
class HandleAcceptedByAdministrativeVacationRequest
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestAcceptedByAdministrative $event): void
|
||||
{
|
||||
$this->stateManager->approve($event->vacationRequest);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
|
||||
class HandleAcceptedByTechnicalVacationRequest
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestAcceptedByTechnical $event): void
|
||||
{
|
||||
$vacationRequest = $event->vacationRequest;
|
||||
|
||||
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
|
||||
$this->stateManager->waitForAdministrative($vacationRequest);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->stateManager->approve($vacationRequest);
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Toby\Domain\Events\VacationRequestApproved;
|
||||
use Toby\Infrastructure\Jobs\SendVacationRequestDaysToGoogleCalendar;
|
||||
|
||||
class HandleApprovedVacationRequest
|
||||
{
|
||||
public function handle(VacationRequestApproved $event): void
|
||||
{
|
||||
SendVacationRequestDaysToGoogleCalendar::dispatch($event->vacationRequest);
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Toby\Domain\Events\VacationRequestCancelled;
|
||||
use Toby\Infrastructure\Jobs\ClearVacationRequestDaysInGoogleCalendar;
|
||||
|
||||
class HandleCancelledVacationRequest
|
||||
{
|
||||
public function handle(VacationRequestCancelled $event): void
|
||||
{
|
||||
ClearVacationRequestDaysInGoogleCalendar::dispatch($event->vacationRequest);
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Toby\Domain\Events\VacationRequestCreated;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
|
||||
class HandleCreatedVacationRequest
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
protected VacationRequestStateManager $stateManager,
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestCreated $event): void
|
||||
{
|
||||
$vacationRequest = $event->vacationRequest;
|
||||
|
||||
if ($vacationRequest->hasFlowSkipped()) {
|
||||
$this->stateManager->approve($vacationRequest);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) {
|
||||
$this->stateManager->waitForTechnical($vacationRequest);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
|
||||
$this->stateManager->waitForAdministrative($vacationRequest);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->stateManager->approve($vacationRequest);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Events\VacationRequestApproved;
|
||||
use Toby\Domain\Notifications\VacationRequestApprovedNotification;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class SendApprovedVacationRequestNotification
|
||||
{
|
||||
public function __construct(
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestApproved $event): void
|
||||
{
|
||||
foreach ($this->getUsersForNotifications() as $user) {
|
||||
$user->notify(new VacationRequestApprovedNotification($event->vacationRequest, $user));
|
||||
}
|
||||
|
||||
$event->vacationRequest->user->notify(new VacationRequestApprovedNotification($event->vacationRequest, $event->vacationRequest->user));
|
||||
}
|
||||
|
||||
protected function getUsersForNotifications(): Collection
|
||||
{
|
||||
return User::query()
|
||||
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover])
|
||||
->get();
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Events\VacationRequestCancelled;
|
||||
use Toby\Domain\Notifications\VacationRequestCancelledNotification;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class SendCancelledVacationRequestNotification
|
||||
{
|
||||
public function __construct(
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestCancelled $event): void
|
||||
{
|
||||
foreach ($this->getUsersForNotifications() as $user) {
|
||||
$user->notify(new VacationRequestCancelledNotification($event->vacationRequest, $user));
|
||||
}
|
||||
|
||||
$event->vacationRequest->user->notify(new VacationRequestCancelledNotification($event->vacationRequest, $event->vacationRequest->user));
|
||||
}
|
||||
|
||||
protected function getUsersForNotifications(): Collection
|
||||
{
|
||||
return User::query()
|
||||
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover])
|
||||
->get();
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Toby\Domain\Events\VacationRequestCreated;
|
||||
use Toby\Domain\Notifications\VacationRequestCreatedNotification;
|
||||
use Toby\Domain\Notifications\VacationRequestCreatedOnEmployeeBehalf;
|
||||
|
||||
class SendCreatedVacationRequestNotification
|
||||
{
|
||||
public function __construct(
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestCreated $event): void
|
||||
{
|
||||
$vacationRequest = $event->vacationRequest;
|
||||
|
||||
if ($vacationRequest->creator->is($vacationRequest->user)) {
|
||||
$vacationRequest->user->notify(new VacationRequestCreatedNotification($vacationRequest));
|
||||
} else {
|
||||
$vacationRequest->user->notify(new VacationRequestCreatedOnEmployeeBehalf($vacationRequest));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Events\VacationRequestRejected;
|
||||
use Toby\Domain\Notifications\VacationRequestRejectedNotification;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class SendRejectedVacationRequestNotification
|
||||
{
|
||||
public function __construct(
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestRejected $event): void
|
||||
{
|
||||
foreach ($this->getUsersForNotifications() as $user) {
|
||||
$user->notify(new VacationRequestRejectedNotification($event->vacationRequest, $user));
|
||||
}
|
||||
|
||||
$event->vacationRequest->user->notify(new VacationRequestRejectedNotification($event->vacationRequest, $event->vacationRequest->user));
|
||||
}
|
||||
|
||||
protected function getUsersForNotifications(): Collection
|
||||
{
|
||||
return User::query()
|
||||
->whereIn("role", [Role::TechnicalApprover, Role::AdministrativeApprover])
|
||||
->get();
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Events\VacationRequestWaitsForAdminApproval;
|
||||
use Toby\Domain\Notifications\VacationRequestWaitsForAdminApprovalNotification;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class SendWaitedForAdministrativeVacationRequestNotification
|
||||
{
|
||||
public function __construct(
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestWaitsForAdminApproval $event): void
|
||||
{
|
||||
foreach ($this->getUsersForNotifications() as $user) {
|
||||
$user->notify(new VacationRequestWaitsForAdminApprovalNotification($event->vacationRequest, $user));
|
||||
}
|
||||
}
|
||||
|
||||
protected function getUsersForNotifications(): Collection
|
||||
{
|
||||
return User::query()
|
||||
->where("role", [Role::AdministrativeApprover])
|
||||
->get();
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Listeners;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Events\VacationRequestWaitsForTechApproval;
|
||||
use Toby\Domain\Notifications\VacationRequestWaitsForTechApprovalNotification;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class SendWaitedForTechnicalVacationRequestNotification
|
||||
{
|
||||
public function __construct(
|
||||
) {}
|
||||
|
||||
public function handle(VacationRequestWaitsForTechApproval $event): void
|
||||
{
|
||||
foreach ($this->getUsersForNotifications() as $user) {
|
||||
$user->notify(new VacationRequestWaitsForTechApprovalNotification($event->vacationRequest, $user));
|
||||
}
|
||||
}
|
||||
|
||||
protected function getUsersForNotifications(): Collection
|
||||
{
|
||||
return User::query()
|
||||
->where("role", [Role::TechnicalApprover])
|
||||
->get();
|
||||
}
|
||||
}
|
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,16 @@ 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]);
|
||||
$seeDetails = __("See details");
|
||||
|
||||
return (new SlackMessage())
|
||||
->text("{$this->buildDescription()}\n <${url}|${seeDetails}>");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,33 +49,64 @@ class VacationRequestCreatedNotification extends Notification
|
||||
|
||||
protected function buildMailMessage(string $url): MailMessage
|
||||
{
|
||||
$user = $this->vacationRequest->user->first_name;
|
||||
$title = $this->vacationRequest->name;
|
||||
$user = $this->vacationRequest->user->profile->first_name;
|
||||
$type = $this->vacationRequest->type->label();
|
||||
$from = $this->vacationRequest->from->toDisplayString();
|
||||
$to = $this->vacationRequest->to->toDisplayString();
|
||||
$days = $this->vacationRequest->vacations()->count();
|
||||
$appName = config("app.name");
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(__("Hi :user!", [
|
||||
->greeting(
|
||||
__("Hi :user!", [
|
||||
"user" => $user,
|
||||
]))
|
||||
->subject(__("Vacation request :title has been created", [
|
||||
"title" => $title,
|
||||
]))
|
||||
->line(__("The vacation request :title has been created correctly in the :appName.", [
|
||||
"title" => $title,
|
||||
"appName" => $appName,
|
||||
]))
|
||||
->line(__("Vacation type: :type", [
|
||||
]),
|
||||
)
|
||||
->subject($this->buildSubject())
|
||||
->line($this->buildDescription())
|
||||
->line(
|
||||
__("Vacation type: :type", [
|
||||
"type" => $type,
|
||||
]))
|
||||
->line(__("From :from to :to (number of days: :days)", [
|
||||
]),
|
||||
)
|
||||
->line(
|
||||
__("From :from to :to (number of days: :days)", [
|
||||
"from" => $from,
|
||||
"to" => $to,
|
||||
"days" => $days,
|
||||
]))
|
||||
]),
|
||||
)
|
||||
->action(__("Click here for details"), $url);
|
||||
}
|
||||
|
||||
protected function buildSubject(): string
|
||||
{
|
||||
$name = $this->vacationRequest->name;
|
||||
|
||||
if ($this->vacationRequest->creator()->is($this->vacationRequest->user)) {
|
||||
return __("Vacation request :title has been created", [
|
||||
"title" => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
return __("Vacation request :title has been created on your behalf", [
|
||||
"title" => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function buildDescription(): string
|
||||
{
|
||||
$name = $this->vacationRequest->name;
|
||||
|
||||
if ($this->vacationRequest->creator()->is($this->vacationRequest->user)) {
|
||||
return __("The vacation request :title has been created successfully.", [
|
||||
"requester" => $this->vacationRequest->user->profile->full_name,
|
||||
"title" => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
return __("The vacation request :title has been created successfully by user :creator on your behalf.", [
|
||||
"title" => $this->vacationRequest->name,
|
||||
"creator" => $this->vacationRequest->creator->profile->full_name,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use InvalidArgumentException;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestCreatedOnEmployeeBehalf extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected VacationRequest $vacationRequest,
|
||||
) {}
|
||||
|
||||
public function via(): array
|
||||
{
|
||||
return ["mail"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$url = route(
|
||||
"vacation.requests.show",
|
||||
[
|
||||
"vacationRequest" => $this->vacationRequest,
|
||||
],
|
||||
);
|
||||
return $this->buildMailMessage($url);
|
||||
}
|
||||
|
||||
protected function buildMailMessage(string $url): MailMessage
|
||||
{
|
||||
$creator = $this->vacationRequest->creator->fullName;
|
||||
$user = $this->vacationRequest->user->first_name;
|
||||
$title = $this->vacationRequest->name;
|
||||
$type = $this->vacationRequest->type->label();
|
||||
$from = $this->vacationRequest->from->toDisplayString();
|
||||
$to = $this->vacationRequest->to->toDisplayString();
|
||||
$days = $this->vacationRequest->vacations()->count();
|
||||
$appName = config("app.name");
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(__("Hi :user!", [
|
||||
"user" => $user,
|
||||
]))
|
||||
->subject(__("Vacation request :title has been created on your behalf", [
|
||||
"title" => $title,
|
||||
]))
|
||||
->line(__("The vacation request :title has been created correctly by user :creator on your behalf in the :appName.", [
|
||||
"title" => $title,
|
||||
"appName" => $appName,
|
||||
"creator" => $creator,
|
||||
]))
|
||||
->line(__("Vacation type: :type", [
|
||||
"type" => $type,
|
||||
]))
|
||||
->line(__("From :from to :to (number of days: :days)", [
|
||||
"from" => $from,
|
||||
"to" => $to,
|
||||
"days" => $days,
|
||||
]))
|
||||
->action(__("Click here for details"), $url);
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use InvalidArgumentException;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestRejectedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected VacationRequest $vacationRequest,
|
||||
protected User $user,
|
||||
) {}
|
||||
|
||||
public function via(): array
|
||||
{
|
||||
return ["mail"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$url = route(
|
||||
"vacation.requests.show",
|
||||
[
|
||||
"vacationRequest" => $this->vacationRequest,
|
||||
],
|
||||
);
|
||||
|
||||
return $this->buildMailMessage($url);
|
||||
}
|
||||
|
||||
protected function buildMailMessage(string $url): MailMessage
|
||||
{
|
||||
$user = $this->user->first_name;
|
||||
$title = $this->vacationRequest->name;
|
||||
$type = $this->vacationRequest->type->label();
|
||||
$from = $this->vacationRequest->from->toDisplayString();
|
||||
$to = $this->vacationRequest->to->toDisplayString();
|
||||
$days = $this->vacationRequest->vacations()->count();
|
||||
$requester = $this->vacationRequest->user->fullName;
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(__("Hi :user!", [
|
||||
"user" => $user,
|
||||
]))
|
||||
->subject(__("Vacation request :title has been rejected", [
|
||||
"title" => $title,
|
||||
]))
|
||||
->line(__("The vacation request :title for user :requester has been rejected.", [
|
||||
"title" => $title,
|
||||
"requester" => $requester,
|
||||
]))
|
||||
->line(__("Vacation type: :type", [
|
||||
"type" => $type,
|
||||
]))
|
||||
->line(__("From :from to :to (number of days: :days)", [
|
||||
"from" => $from,
|
||||
"to" => $to,
|
||||
"days" => $days,
|
||||
]))
|
||||
->action(__("Click here for details"), $url);
|
||||
}
|
||||
}
|
@ -10,8 +10,9 @@ use Illuminate\Notifications\Notification;
|
||||
use InvalidArgumentException;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
use Toby\Infrastructure\Slack\Elements\SlackMessage;
|
||||
|
||||
class VacationRequestApprovedNotification extends Notification
|
||||
class VacationRequestStatusChangedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
@ -22,7 +23,16 @@ class VacationRequestApprovedNotification 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]);
|
||||
$seeDetails = __("See details");
|
||||
|
||||
return (new SlackMessage())
|
||||
->text("{$this->buildDescription()}\n <${url}|${seeDetails}>");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,25 +52,18 @@ class VacationRequestApprovedNotification extends Notification
|
||||
|
||||
protected function buildMailMessage(string $url): MailMessage
|
||||
{
|
||||
$user = $this->user->first_name;
|
||||
$title = $this->vacationRequest->name;
|
||||
$user = $this->user->profile->first_name;
|
||||
$type = $this->vacationRequest->type->label();
|
||||
$from = $this->vacationRequest->from->toDisplayString();
|
||||
$to = $this->vacationRequest->to->toDisplayString();
|
||||
$days = $this->vacationRequest->vacations()->count();
|
||||
$requester = $this->vacationRequest->user->fullName;
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(__("Hi :user!", [
|
||||
"user" => $user,
|
||||
]))
|
||||
->subject(__("Vacation request :title has been approved", [
|
||||
"title" => $title,
|
||||
]))
|
||||
->line(__("The vacation request :title for user :requester has been approved.", [
|
||||
"title" => $title,
|
||||
"requester" => $requester,
|
||||
]))
|
||||
->subject($this->buildSubject())
|
||||
->line($this->buildDescription())
|
||||
->line(__("Vacation type: :type", [
|
||||
"type" => $type,
|
||||
]))
|
||||
@ -71,4 +74,21 @@ class VacationRequestApprovedNotification 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(),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use InvalidArgumentException;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestWaitsForAdminApprovalNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected VacationRequest $vacationRequest,
|
||||
protected User $user,
|
||||
) {}
|
||||
|
||||
public function via(): array
|
||||
{
|
||||
return ["mail"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$url = route(
|
||||
"vacation.requests.show",
|
||||
[
|
||||
"vacationRequest" => $this->vacationRequest,
|
||||
],
|
||||
);
|
||||
|
||||
return $this->buildMailMessage($url);
|
||||
}
|
||||
|
||||
protected function buildMailMessage(string $url): MailMessage
|
||||
{
|
||||
$user = $this->user->first_name;
|
||||
$requester = $this->vacationRequest->user->fullName;
|
||||
$title = $this->vacationRequest->name;
|
||||
$type = $this->vacationRequest->type->label();
|
||||
$from = $this->vacationRequest->from->toDisplayString();
|
||||
$to = $this->vacationRequest->to->toDisplayString();
|
||||
$days = $this->vacationRequest->vacations()->count();
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(__("Hi :user!", [
|
||||
"user" => $user,
|
||||
]))
|
||||
->subject(__("Vacation request :title is waiting for your approval", [
|
||||
"title" => $title,
|
||||
]))
|
||||
->line(__("The vacation request :title from user: :requester is waiting for your approval.", [
|
||||
"title" => $title,
|
||||
"requester" => $requester,
|
||||
]))
|
||||
->line(__("Vacation type: :type", [
|
||||
"type" => $type,
|
||||
]))
|
||||
->line(__("From :from to :to (number of days: :days)", [
|
||||
"from" => $from,
|
||||
"to" => $to,
|
||||
"days" => $days,
|
||||
]))
|
||||
->action(__("Click here for details"), $url);
|
||||
}
|
||||
}
|
@ -8,10 +8,12 @@ use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
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 VacationRequestCancelledNotification extends Notification
|
||||
class VacationRequestWaitsForApprovalNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
@ -22,7 +24,16 @@ class VacationRequestCancelledNotification 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]);
|
||||
$seeDetails = __("See details");
|
||||
|
||||
return (new SlackMessage())
|
||||
->text("{$this->buildDescription()}\n <${url}|${seeDetails}>");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,25 +53,18 @@ class VacationRequestCancelledNotification extends Notification
|
||||
|
||||
protected function buildMailMessage(string $url): MailMessage
|
||||
{
|
||||
$user = $this->user->first_name;
|
||||
$title = $this->vacationRequest->name;
|
||||
$user = $this->user->profile->first_name;
|
||||
$type = $this->vacationRequest->type->label();
|
||||
$from = $this->vacationRequest->from->toDisplayString();
|
||||
$to = $this->vacationRequest->to->toDisplayString();
|
||||
$days = $this->vacationRequest->vacations()->count();
|
||||
$requester = $this->vacationRequest->user->fullName;
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(__("Hi :user!", [
|
||||
"user" => $user,
|
||||
]))
|
||||
->subject(__("Vacation request :title has been cancelled", [
|
||||
"title" => $title,
|
||||
]))
|
||||
->line(__("The vacation request :title for user :requester has been cancelled.", [
|
||||
"title" => $title,
|
||||
"requester" => $requester,
|
||||
]))
|
||||
->subject($this->buildSubject())
|
||||
->line($this->buildDescription())
|
||||
->line(__("Vacation type: :type", [
|
||||
"type" => $type,
|
||||
]))
|
||||
@ -71,4 +75,37 @@ class VacationRequestCancelledNotification extends Notification
|
||||
]))
|
||||
->action(__("Click here for details"), $url);
|
||||
}
|
||||
|
||||
protected function buildSubject(): string
|
||||
{
|
||||
$title = $this->vacationRequest->name;
|
||||
|
||||
if ($this->vacationRequest->state->equals(WaitingForTechnical::class)) {
|
||||
return __("Vacation request :title is waiting for your technical approval", [
|
||||
"title" => $title,
|
||||
]);
|
||||
}
|
||||
|
||||
return __("Vacation request :title is waiting for your administrative approval", [
|
||||
"title" => $title,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function buildDescription(): string
|
||||
{
|
||||
$title = $this->vacationRequest->name;
|
||||
$requester = $this->vacationRequest->user->profile->full_name;
|
||||
|
||||
if ($this->vacationRequest->state->equals(WaitingForTechnical::class)) {
|
||||
return __("The vacation request :title from user :requester is waiting for your technical approval.", [
|
||||
"title" => $title,
|
||||
"requester" => $requester,
|
||||
]);
|
||||
}
|
||||
|
||||
return __("The vacation request :title from user :requester is waiting for your administrative approval.", [
|
||||
"title" => $title,
|
||||
"requester" => $requester,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use InvalidArgumentException;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestWaitsForTechApprovalNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected VacationRequest $vacationRequest,
|
||||
protected User $user,
|
||||
) {}
|
||||
|
||||
public function via(): array
|
||||
{
|
||||
return ["mail"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$url = route(
|
||||
"vacation.requests.show",
|
||||
[
|
||||
"vacationRequest" => $this->vacationRequest,
|
||||
],
|
||||
);
|
||||
|
||||
return $this->buildMailMessage($url);
|
||||
}
|
||||
|
||||
protected function buildMailMessage(string $url): MailMessage
|
||||
{
|
||||
$user = $this->user->first_name;
|
||||
$requester = $this->vacationRequest->user->fullName;
|
||||
$title = $this->vacationRequest->name;
|
||||
$type = $this->vacationRequest->type->label();
|
||||
$from = $this->vacationRequest->from->toDisplayString();
|
||||
$to = $this->vacationRequest->to->toDisplayString();
|
||||
$days = $this->vacationRequest->vacations()->count();
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(__("Hi :user!", [
|
||||
"user" => $user,
|
||||
]))
|
||||
->subject(__("Vacation request :title is waiting for your approval", [
|
||||
"title" => $title,
|
||||
]))
|
||||
->line(__("The vacation request :title from user: :requester is waiting for your approval.", [
|
||||
"title" => $title,
|
||||
"requester" => $requester,
|
||||
]))
|
||||
->line(__("Vacation type: :type", [
|
||||
"type" => $type,
|
||||
]))
|
||||
->line(__("From :from to :to (number of days: :days)", [
|
||||
"from" => $from,
|
||||
"to" => $to,
|
||||
"days" => $days,
|
||||
]))
|
||||
->action(__("Click here for details"), $url);
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
<?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(__("Requests wait for your approval - status for day :date:", ["date" => $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 (__("Requests list waits for your approval - status for day :date:", ["date" => $this->day->toDisplayString()]))
|
||||
->subject(__("Requests wait for your approval - status for day :date:", ["date" => $this->day->toDisplayString()]));
|
||||
|
||||
foreach ($this->vacationRequests as $request) {
|
||||
$url = route("vacation.requests.show", ["vacationRequest" => $request->id]);
|
||||
|
||||
$message->line(
|
||||
__("- [request no. :request](:url) of user :user (:startDate - :endDate)", ["request" => $request->name, "url" => $url, "user" => $request->user->profile->full_name, "startDate" => $request->from->toDisplayString(), "endDate" => $request->to->toDisplayString()]),
|
||||
);
|
||||
}
|
||||
|
||||
return $message
|
||||
->action(__("Go to requests"), $url);
|
||||
}
|
||||
}
|
26
app/Domain/Policies/KeyPolicy.php
Normal file
26
app/Domain/Policies/KeyPolicy.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Policies;
|
||||
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Eloquent\Models\Key;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class KeyPolicy
|
||||
{
|
||||
public function manage(User $user): bool
|
||||
{
|
||||
return $user->role === Role::AdministrativeApprover;
|
||||
}
|
||||
|
||||
public function give(User $user, Key $key): bool
|
||||
{
|
||||
if ($key->user()->is($user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->role === Role::AdministrativeApprover;
|
||||
}
|
||||
}
|
@ -5,6 +5,9 @@ declare(strict_types=1);
|
||||
namespace Toby\Domain\Policies;
|
||||
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\States\VacationRequest\Created;
|
||||
use Toby\Domain\States\VacationRequest\WaitingForAdministrative;
|
||||
use Toby\Domain\States\VacationRequest\WaitingForTechnical;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
@ -40,8 +43,16 @@ class VacationRequestPolicy
|
||||
return in_array($user->role, [Role::AdministrativeApprover, Role::TechnicalApprover], true);
|
||||
}
|
||||
|
||||
public function cancel(User $user): bool
|
||||
public function cancel(User $user, VacationRequest $vacationRequest): bool
|
||||
{
|
||||
if ($vacationRequest->user->is($user) && $vacationRequest->state->equals(
|
||||
Created::class,
|
||||
WaitingForAdministrative::class,
|
||||
WaitingForTechnical::class,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->role === Role::AdministrativeApprover;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
113
app/Domain/ResumeGenerator.php
Normal file
113
app/Domain/ResumeGenerator.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use PhpOffice\PhpWord\TemplateProcessor;
|
||||
use Toby\Eloquent\Models\Resume;
|
||||
|
||||
class ResumeGenerator
|
||||
{
|
||||
public function generate(Resume $resume): string
|
||||
{
|
||||
$processor = new TemplateProcessor($this->getTemplate());
|
||||
|
||||
$processor->setValue("id", $resume->id);
|
||||
$processor->setValue("name", $resume->user ? $resume->user->profile->full_name : $resume->name);
|
||||
|
||||
$this->fillTechnologies($processor, $resume);
|
||||
$this->fillLanguages($processor, $resume);
|
||||
$this->fillEducation($processor, $resume);
|
||||
$this->fillProjects($processor, $resume);
|
||||
|
||||
return $processor->save();
|
||||
}
|
||||
|
||||
public function getTemplate(): string
|
||||
{
|
||||
return resource_path("views/docx/resume_eng.docx");
|
||||
}
|
||||
|
||||
protected function fillTechnologies(TemplateProcessor $processor, Resume $resume): void
|
||||
{
|
||||
$processor->cloneBlock("technologies", 0, true, false, $this->getTechnologies($resume));
|
||||
}
|
||||
|
||||
protected function fillLanguages(TemplateProcessor $processor, Resume $resume): void
|
||||
{
|
||||
$processor->cloneBlock("languages", 0, true, false, $this->getLanguages($resume));
|
||||
}
|
||||
|
||||
protected function fillEducation(TemplateProcessor $processor, Resume $resume): void
|
||||
{
|
||||
$processor->cloneBlock("education", 0, true, false, $this->getEducation($resume));
|
||||
}
|
||||
|
||||
protected function fillProjects(TemplateProcessor $processor, Resume $resume): void
|
||||
{
|
||||
$processor->cloneBlock("projects", $resume->projects->count(), true, true);
|
||||
|
||||
foreach ($resume->projects as $index => $project) {
|
||||
++$index;
|
||||
$processor->setValues($this->getProject($project, $index));
|
||||
|
||||
$processor->cloneBlock("project_technologies#{$index}", 0, true, false, $this->getProjectTechnologies($project, $index));
|
||||
}
|
||||
}
|
||||
|
||||
protected function getProject(array $project, int $index): array
|
||||
{
|
||||
return [
|
||||
"index#{$index}" => $index,
|
||||
"start_date#{$index}" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"),
|
||||
"end_date#{$index}" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"),
|
||||
"description#{$index}" => $project["description"],
|
||||
"tasks#{$index}" => $this->withNewLines($project["tasks"]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function withNewLines(string $text): string
|
||||
{
|
||||
return Str::replace("\n", "</w:t><w:br/><w:t>", $text);
|
||||
}
|
||||
|
||||
protected function getProjectTechnologies(array $project, int $index): array
|
||||
{
|
||||
$technologies = new Collection($project["technologies"] ?? []);
|
||||
|
||||
return $technologies->map(fn(string $name) => [
|
||||
"technology#{$index}" => $name,
|
||||
])->all();
|
||||
}
|
||||
|
||||
protected function getTechnologies(Resume $resume): array
|
||||
{
|
||||
return $resume->technologies->map(fn(array $technology): array => [
|
||||
"technology_name" => $technology["name"],
|
||||
"technology_level" => __("resume.technology_levels.{$technology["level"]}"),
|
||||
])->all();
|
||||
}
|
||||
|
||||
protected function getLanguages(Resume $resume): array
|
||||
{
|
||||
return $resume->languages->map(fn(array $language): array => [
|
||||
"language_name" => $language["name"],
|
||||
"language_level" => __("resume.language_levels.{$language["level"]}"),
|
||||
])->all();
|
||||
}
|
||||
|
||||
protected function getEducation(Resume $resume): array
|
||||
{
|
||||
return $resume->education->map(fn(array $project, int $index): array => [
|
||||
"start_date" => Carbon::createFromFormat("m/Y", $project["startDate"])->format("n.Y"),
|
||||
"end_date" => $project["current"] ? "present" : Carbon::createFromFormat("m/Y", $project["endDate"])->format("n.Y"),
|
||||
"school" => $project["school"],
|
||||
"field_of_study" => $project["fieldOfStudy"],
|
||||
"degree" => $project["degree"],
|
||||
])->all();
|
||||
}
|
||||
}
|
@ -36,4 +36,9 @@ abstract class VacationRequestState extends State
|
||||
Approved::class,
|
||||
], Cancelled::class);
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return __(static::$name);
|
||||
}
|
||||
}
|
||||
|
@ -4,20 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class TimesheetExport implements WithMultipleSheets
|
||||
{
|
||||
protected Collection $users;
|
||||
protected Collection $types;
|
||||
protected Carbon $month;
|
||||
|
||||
public function sheets(): array
|
||||
{
|
||||
return $this->users
|
||||
->map(fn(User $user) => new TimesheetPerUserSheet($user, $this->month))
|
||||
->map(fn(User $user): TimesheetPerUserSheet => new TimesheetPerUserSheet($user, $this->month, $this->types))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
@ -34,4 +35,11 @@ class TimesheetExport implements WithMultipleSheets
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function forVacationTypes(Collection $types): static
|
||||
{
|
||||
$this->types = $types;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ namespace Toby\Domain;
|
||||
use Carbon\CarbonInterface;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Generator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\FromGenerator;
|
||||
@ -25,7 +26,6 @@ use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Toby\Domain\Enums\VacationType;
|
||||
use Toby\Domain\States\VacationRequest\Approved;
|
||||
use Toby\Eloquent\Models\Holiday;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\Vacation;
|
||||
@ -41,17 +41,16 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
|
||||
public function __construct(
|
||||
protected User $user,
|
||||
protected Carbon $month,
|
||||
protected Collection $types,
|
||||
) {}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return $this->user->fullName;
|
||||
return $this->user->profile->full_name;
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
$types = VacationType::cases();
|
||||
|
||||
$headings = [
|
||||
__("Date"),
|
||||
__("Day of week"),
|
||||
@ -60,7 +59,7 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
|
||||
__("Worked hours"),
|
||||
];
|
||||
|
||||
foreach ($types as $type) {
|
||||
foreach ($this->types as $type) {
|
||||
$headings[] = $type->label();
|
||||
}
|
||||
|
||||
@ -188,13 +187,14 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
|
||||
{
|
||||
return $user->vacations()
|
||||
->with("vacationRequest")
|
||||
->whereRelation("vacationRequest", fn(Builder $query): Builder => $query->whereIn("type", $this->types))
|
||||
->whereBetween("date", [$period->start, $period->end])
|
||||
->whereRelation("vacationRequest", "state", Approved::$name)
|
||||
->approved()
|
||||
->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,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -5,9 +5,10 @@ declare(strict_types=1);
|
||||
namespace Toby\Domain;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Domain\Enums\VacationType;
|
||||
use Toby\Eloquent\Models\User;
|
||||
use Toby\Eloquent\Models\Vacation;
|
||||
use Toby\Eloquent\Models\YearPeriod;
|
||||
|
||||
class UserVacationStatsRetriever
|
||||
@ -20,24 +21,39 @@ class UserVacationStatsRetriever
|
||||
{
|
||||
return $user
|
||||
->vacations()
|
||||
->where("year_period_id", $yearPeriod->id)
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->whereRelation(
|
||||
"vacationRequest",
|
||||
fn(Builder $query) => $query
|
||||
fn(Builder $query): Builder => $query
|
||||
->whereIn("type", $this->getLimitableVacationTypes())
|
||||
->states(VacationRequestStatesRetriever::successStates()),
|
||||
)
|
||||
->count();
|
||||
}
|
||||
|
||||
public function getUsedVacationDaysByMonth(User $user, YearPeriod $yearPeriod): Collection
|
||||
{
|
||||
return $user->vacations()
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->whereRelation(
|
||||
"vacationRequest",
|
||||
fn(Builder $query): Builder => $query
|
||||
->whereIn("type", $this->getLimitableVacationTypes())
|
||||
->states(VacationRequestStatesRetriever::successStates()),
|
||||
)
|
||||
->get()
|
||||
->groupBy(fn(Vacation $vacation): string => strtolower($vacation->date->englishMonth))
|
||||
->map(fn(Collection $items): int => $items->count());
|
||||
}
|
||||
|
||||
public function getPendingVacationDays(User $user, YearPeriod $yearPeriod): int
|
||||
{
|
||||
return $user
|
||||
->vacations()
|
||||
->where("year_period_id", $yearPeriod->id)
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->whereRelation(
|
||||
"vacationRequest",
|
||||
fn(Builder $query) => $query
|
||||
fn(Builder $query): Builder => $query
|
||||
->whereIn("type", $this->getLimitableVacationTypes())
|
||||
->states(VacationRequestStatesRetriever::pendingStates()),
|
||||
)
|
||||
@ -48,16 +64,26 @@ class UserVacationStatsRetriever
|
||||
{
|
||||
return $user
|
||||
->vacations()
|
||||
->where("year_period_id", $yearPeriod->id)
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->whereRelation(
|
||||
"vacationRequest",
|
||||
fn(Builder $query) => $query
|
||||
fn(Builder $query): Builder => $query
|
||||
->whereIn("type", $this->getNotLimitableVacationTypes())
|
||||
->whereNot("type", VacationType::HomeOffice)
|
||||
->states(VacationRequestStatesRetriever::successStates()),
|
||||
)
|
||||
->count();
|
||||
}
|
||||
|
||||
public function getHomeOfficeDays(User $user, YearPeriod $yearPeriod): int
|
||||
{
|
||||
return $user
|
||||
->vacations()
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->whereRelation("vacationRequest", "type", VacationType::HomeOffice)
|
||||
->count();
|
||||
}
|
||||
|
||||
public function getRemainingVacationDays(User $user, YearPeriod $yearPeriod): int
|
||||
{
|
||||
$limit = $this->getVacationDaysLimit($user, $yearPeriod);
|
||||
@ -70,24 +96,24 @@ class UserVacationStatsRetriever
|
||||
public function getVacationDaysLimit(User $user, YearPeriod $yearPeriod): int
|
||||
{
|
||||
$limit = $user->vacationLimits()
|
||||
->where("year_period_id", $yearPeriod->id)
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->first()
|
||||
->days;
|
||||
?->days;
|
||||
|
||||
return $limit ?? 0;
|
||||
}
|
||||
|
||||
protected function getLimitableVacationTypes(): Collection
|
||||
{
|
||||
$types = new Collection(VacationType::cases());
|
||||
$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 = new Collection(VacationType::cases());
|
||||
$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,17 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain;
|
||||
|
||||
use Illuminate\Contracts\Auth\Factory as Auth;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
|
||||
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
|
||||
use Toby\Domain\Events\VacationRequestApproved;
|
||||
use Toby\Domain\Events\VacationRequestCancelled;
|
||||
use Toby\Domain\Events\VacationRequestCreated;
|
||||
use Toby\Domain\Events\VacationRequestRejected;
|
||||
use Toby\Domain\Events\VacationRequestStateChanged;
|
||||
use Toby\Domain\Events\VacationRequestWaitsForAdminApproval;
|
||||
use Toby\Domain\Events\VacationRequestWaitsForTechApproval;
|
||||
use Toby\Domain\States\VacationRequest\AcceptedByAdministrative;
|
||||
use Toby\Domain\States\VacationRequest\AcceptedByTechnical;
|
||||
use Toby\Domain\States\VacationRequest\Approved;
|
||||
@ -29,63 +19,47 @@ use Toby\Eloquent\Models\VacationRequest;
|
||||
class VacationRequestStateManager
|
||||
{
|
||||
public function __construct(
|
||||
protected Auth $auth,
|
||||
protected Dispatcher $dispatcher,
|
||||
) {}
|
||||
|
||||
public function markAsCreated(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
public function markAsCreated(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$this->fireStateChangedEvent($vacationRequest, null, $vacationRequest->state, $user);
|
||||
|
||||
$this->dispatcher->dispatch(new VacationRequestCreated($vacationRequest));
|
||||
$this->createActivity($vacationRequest, null, $vacationRequest->state, $vacationRequest->creator);
|
||||
}
|
||||
|
||||
public function approve(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
{
|
||||
$this->changeState($vacationRequest, Approved::class, $user);
|
||||
|
||||
$this->dispatcher->dispatch(new VacationRequestApproved($vacationRequest));
|
||||
}
|
||||
|
||||
public function reject(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
public function reject(VacationRequest $vacationRequest, User $user): void
|
||||
{
|
||||
$this->changeState($vacationRequest, Rejected::class, $user);
|
||||
$this->dispatcher->dispatch(new VacationRequestRejected($vacationRequest));
|
||||
}
|
||||
|
||||
public function cancel(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
public function cancel(VacationRequest $vacationRequest, User $user): void
|
||||
{
|
||||
$this->changeState($vacationRequest, Cancelled::class, $user);
|
||||
|
||||
$this->dispatcher->dispatch(new VacationRequestCancelled($vacationRequest));
|
||||
}
|
||||
|
||||
public function acceptAsTechnical(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
public function acceptAsTechnical(VacationRequest $vacationRequest, User $user): void
|
||||
{
|
||||
$this->changeState($vacationRequest, AcceptedByTechnical::class, $user);
|
||||
|
||||
$this->dispatcher->dispatch(new VacationRequestAcceptedByTechnical($vacationRequest));
|
||||
}
|
||||
|
||||
public function acceptAsAdministrative(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
public function acceptAsAdministrative(VacationRequest $vacationRequest, User $user): void
|
||||
{
|
||||
$this->changeState($vacationRequest, AcceptedByAdministrative::class, $user);
|
||||
|
||||
$this->dispatcher->dispatch(new VacationRequestAcceptedByAdministrative($vacationRequest));
|
||||
}
|
||||
|
||||
public function waitForTechnical(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
public function waitForTechnical(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$this->changeState($vacationRequest, WaitingForTechnical::class, $user);
|
||||
|
||||
$this->dispatcher->dispatch(new VacationRequestWaitsForTechApproval($vacationRequest));
|
||||
$this->changeState($vacationRequest, WaitingForTechnical::class);
|
||||
}
|
||||
|
||||
public function waitForAdministrative(VacationRequest $vacationRequest, ?User $user = null): void
|
||||
public function waitForAdministrative(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$this->changeState($vacationRequest, WaitingForAdministrative::class, $user);
|
||||
|
||||
$this->dispatcher->dispatch(new VacationRequestWaitsForAdminApproval($vacationRequest));
|
||||
$this->changeState($vacationRequest, WaitingForAdministrative::class);
|
||||
}
|
||||
|
||||
protected function changeState(VacationRequest $vacationRequest, string $state, ?User $user = null): void
|
||||
@ -94,16 +68,19 @@ class VacationRequestStateManager
|
||||
$vacationRequest->state->transitionTo($state);
|
||||
$vacationRequest->save();
|
||||
|
||||
$this->fireStateChangedEvent($vacationRequest, $previousState, $vacationRequest->state, $user);
|
||||
$this->createActivity($vacationRequest, $previousState, $vacationRequest->state, $user);
|
||||
}
|
||||
|
||||
protected function fireStateChangedEvent(
|
||||
protected function createActivity(
|
||||
VacationRequest $vacationRequest,
|
||||
?VacationRequestState $from,
|
||||
VacationRequestState $to,
|
||||
?User $user = null,
|
||||
): void {
|
||||
$event = new VacationRequestStateChanged($vacationRequest, $from, $to, $user);
|
||||
$this->dispatcher->dispatch($event);
|
||||
$vacationRequest->activities()->create([
|
||||
"from" => $from,
|
||||
"to" => $to,
|
||||
"user_id" => $user?->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Toby\Domain;
|
||||
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Toby\Domain\Enums\EmploymentForm;
|
||||
use Toby\Domain\Enums\VacationType;
|
||||
|
||||
class VacationTypeConfigRetriever
|
||||
@ -13,6 +14,8 @@ class VacationTypeConfigRetriever
|
||||
public const KEY_ADMINISTRATIVE_APPROVAL = "administrative_approval";
|
||||
public const KEY_BILLABLE = "billable";
|
||||
public const KEY_HAS_LIMIT = "has_limit";
|
||||
public const KEY_AVAILABLE_FOR = "available_for";
|
||||
public const KEY_IS_VACATION = "is_vacation";
|
||||
|
||||
public function __construct(
|
||||
protected Repository $config,
|
||||
@ -38,6 +41,16 @@ class VacationTypeConfigRetriever
|
||||
return $this->getConfigFor($type)[static::KEY_HAS_LIMIT];
|
||||
}
|
||||
|
||||
public function isVacation(VacationType $type): bool
|
||||
{
|
||||
return $this->getConfigFor($type)[static::KEY_IS_VACATION];
|
||||
}
|
||||
|
||||
public function isAvailableFor(VacationType $type, EmploymentForm $employmentForm): bool
|
||||
{
|
||||
return in_array($employmentForm, $this->getConfigFor($type)[static::KEY_AVAILABLE_FOR], true);
|
||||
}
|
||||
|
||||
protected function getConfigFor(VacationType $type): array
|
||||
{
|
||||
return $this->config->get("vacation_types.{$type->value}");
|
||||
|
@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
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);
|
||||
}
|
||||
@ -41,16 +43,19 @@ class DoesNotExceedLimitRule implements VacationRequestRule
|
||||
|
||||
protected function getUserVacationLimit(User $user, YearPeriod $yearPeriod): int
|
||||
{
|
||||
return $user->vacationLimits()->where("year_period_id", $yearPeriod->id)->first()->days ?? 0;
|
||||
return $user->vacationLimits()
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->first()
|
||||
?->days ?? 0;
|
||||
}
|
||||
|
||||
protected function getVacationDaysWithLimit(User $user, YearPeriod $yearPeriod): int
|
||||
{
|
||||
return $user->vacations()
|
||||
->where("year_period_id", $yearPeriod->id)
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->whereRelation(
|
||||
"vacationRequest",
|
||||
fn(Builder $query) => $query
|
||||
fn(Builder $query): Builder => $query
|
||||
->whereIn("type", $this->getLimitableVacationTypes())
|
||||
->noStates(VacationRequestStatesRetriever::failedStates()),
|
||||
)
|
||||
@ -59,8 +64,8 @@ class DoesNotExceedLimitRule implements VacationRequestRule
|
||||
|
||||
protected function getLimitableVacationTypes(): Collection
|
||||
{
|
||||
$types = new Collection(VacationType::cases());
|
||||
$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();
|
||||
}
|
||||
|
||||
|
@ -9,5 +9,6 @@ use Toby\Eloquent\Models\VacationRequest;
|
||||
interface VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest): bool;
|
||||
|
||||
public function errorMessage(): string;
|
||||
}
|
||||
|
31
app/Domain/Validation/Rules/VacationTypeCanBeSelected.php
Normal file
31
app/Domain/Validation/Rules/VacationTypeCanBeSelected.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Toby\Domain\Enums\VacationType;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationTypeCanBeSelected implements VacationRequestRule
|
||||
{
|
||||
public function __construct(
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
) {}
|
||||
|
||||
public function check(VacationRequest $vacationRequest): bool
|
||||
{
|
||||
$employmentForm = $vacationRequest->user->profile->employment_form;
|
||||
|
||||
$availableTypes = VacationType::all()
|
||||
->filter(fn(VacationType $type): bool => $this->configRetriever->isAvailableFor($type, $employmentForm));
|
||||
|
||||
return $availableTypes->contains($vacationRequest->type);
|
||||
}
|
||||
|
||||
public function errorMessage(): string
|
||||
{
|
||||
return __("You cannot create vacation request of this type.");
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ use Toby\Domain\Validation\Rules\NoApprovedVacationRequestsInRange;
|
||||
use Toby\Domain\Validation\Rules\NoPendingVacationRequestInRange;
|
||||
use Toby\Domain\Validation\Rules\VacationRangeIsInTheSameYearRule;
|
||||
use Toby\Domain\Validation\Rules\VacationRequestRule;
|
||||
use Toby\Domain\Validation\Rules\VacationTypeCanBeSelected;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestValidator
|
||||
@ -19,6 +20,7 @@ class VacationRequestValidator
|
||||
protected array $rules = [
|
||||
VacationRangeIsInTheSameYearRule::class,
|
||||
MinimumOneVacationDayRule::class,
|
||||
VacationTypeCanBeSelected::class,
|
||||
DoesNotExceedLimitRule::class,
|
||||
NoPendingVacationRequestInRange::class,
|
||||
NoApprovedVacationRequestsInRange::class,
|
||||
|
@ -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();
|
33
app/Eloquent/Helpers/ColorGenerator.php
Normal file
33
app/Eloquent/Helpers/ColorGenerator.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Helpers;
|
||||
|
||||
class ColorGenerator
|
||||
{
|
||||
public static function generate(string $text): string
|
||||
{
|
||||
$colors = config("colors");
|
||||
$hash = static::calculateHash($text);
|
||||
|
||||
$index = $hash - count($colors) * floor($hash / count($colors));
|
||||
|
||||
return $colors[$index];
|
||||
}
|
||||
|
||||
protected static function calculateHash(string $text): int
|
||||
{
|
||||
$hash = 0;
|
||||
|
||||
if (empty($text)) {
|
||||
return $hash;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < mb_strlen($text); $i++) {
|
||||
$hash = abs((int)(($hash << 2) - $hash) + mb_ord($text[$i]));
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
}
|
@ -30,22 +30,20 @@ class YearPeriodRetriever
|
||||
|
||||
public function links(): array
|
||||
{
|
||||
$current = $this->selected();
|
||||
$selected = $this->selected();
|
||||
$current = $this->current();
|
||||
|
||||
$years = YearPeriod::query()->whereIn("year", $this->offset($current->year))->get();
|
||||
$navigation = $years->map(fn(YearPeriod $yearPeriod) => $this->toNavigation($yearPeriod));
|
||||
$years = YearPeriod::all();
|
||||
|
||||
$navigation = $years->map(fn(YearPeriod $yearPeriod): array => $this->toNavigation($yearPeriod));
|
||||
|
||||
return [
|
||||
"current" => $current->year,
|
||||
"current" => $this->toNavigation($current),
|
||||
"selected" => $this->toNavigation($selected),
|
||||
"navigation" => $navigation->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function offset(int $year): array
|
||||
{
|
||||
return range($year - 2, $year + 2);
|
||||
}
|
||||
|
||||
protected function toNavigation(YearPeriod $yearPeriod): array
|
||||
{
|
||||
return [
|
||||
|
@ -21,7 +21,6 @@ class Holiday extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
"date" => "date",
|
||||
];
|
||||
|
39
app/Eloquent/Models/Key.php
Normal file
39
app/Eloquent/Models/Key.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Models;
|
||||
|
||||
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 implements NotifiableInterface
|
||||
{
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function routeNotificationForSlack(): string
|
||||
{
|
||||
return config("services.slack.default_channel");
|
||||
}
|
||||
|
||||
protected static function newFactory(): KeyFactory
|
||||
{
|
||||
return KeyFactory::new();
|
||||
}
|
||||
}
|
63
app/Eloquent/Models/Profile.php
Normal file
63
app/Eloquent/Models/Profile.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Models;
|
||||
|
||||
use Database\Factories\ProfileFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Rackbeat\UIAvatars\HasAvatar;
|
||||
use Toby\Domain\Enums\EmploymentForm;
|
||||
use Toby\Eloquent\Helpers\ColorGenerator;
|
||||
|
||||
/**
|
||||
* @property string $first_name
|
||||
* @property string $last_name
|
||||
* @property string $position
|
||||
* @property EmploymentForm $employment_form
|
||||
* @property Carbon $employment_date
|
||||
* @property Carbon $birthday
|
||||
*/
|
||||
class Profile extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasAvatar;
|
||||
|
||||
protected $primaryKey = "user_id";
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
"employment_form" => EmploymentForm::class,
|
||||
"employment_date" => "date",
|
||||
"birthday" => "date",
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function getAvatar(): string
|
||||
{
|
||||
return $this->getAvatarGenerator()
|
||||
->backgroundColor(ColorGenerator::generate($this->full_name))
|
||||
->image();
|
||||
}
|
||||
|
||||
public function getfullNameAttribute(): string
|
||||
{
|
||||
return "{$this->first_name} {$this->last_name}";
|
||||
}
|
||||
|
||||
protected function getAvatarName(): string
|
||||
{
|
||||
return mb_substr($this->first_name, 0, 1) . mb_substr($this->last_name, 0, 1);
|
||||
}
|
||||
|
||||
protected static function newFactory(): ProfileFactory
|
||||
{
|
||||
return ProfileFactory::new();
|
||||
}
|
||||
}
|
45
app/Eloquent/Models/Resume.php
Normal file
45
app/Eloquent/Models/Resume.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Models;
|
||||
|
||||
use Database\Factories\ResumeFactory;
|
||||
use Illuminate\Database\Eloquent\Casts\AsCollection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property ?User $user
|
||||
* @property string $name
|
||||
* @property Collection $education
|
||||
* @property Collection $languages
|
||||
* @property Collection $technologies
|
||||
* @property Collection $projects
|
||||
*/
|
||||
class Resume extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
"education" => AsCollection::class,
|
||||
"languages" => AsCollection::class,
|
||||
"technologies" => AsCollection::class,
|
||||
"projects" => AsCollection::class,
|
||||
];
|
||||
protected $perPage = 50;
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
protected static function newFactory(): ResumeFactory
|
||||
{
|
||||
return ResumeFactory::new();
|
||||
}
|
||||
}
|
25
app/Eloquent/Models/Technology.php
Normal file
25
app/Eloquent/Models/Technology.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Models;
|
||||
|
||||
use Database\Factories\TechnologyFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
*/
|
||||
class Technology extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected static function newFactory(): TechnologyFactory
|
||||
{
|
||||
return TechnologyFactory::new();
|
||||
}
|
||||
}
|
@ -8,47 +8,50 @@ use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Rackbeat\UIAvatars\HasAvatar;
|
||||
use Toby\Domain\Enums\EmploymentForm;
|
||||
use Toby\Domain\Enums\Role;
|
||||
use Toby\Domain\Notifications\Notifiable as NotifiableInterface;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $first_name
|
||||
* @property string $last_name
|
||||
* @property string $email
|
||||
* @property string $avatar
|
||||
* @property string $position
|
||||
* @property string $password
|
||||
* @property Role $role
|
||||
* @property EmploymentForm $employment_form
|
||||
* @property Carbon $employment_date
|
||||
* @property Profile $profile
|
||||
* @property Collection $vacationLimits
|
||||
* @property Collection $vacationRequests
|
||||
* @property Collection $vacations
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
class User extends Authenticatable implements NotifiableInterface
|
||||
{
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
use SoftDeletes;
|
||||
use HasAvatar;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
"role" => Role::class,
|
||||
"last_active_at" => "datetime",
|
||||
"employment_form" => EmploymentForm::class,
|
||||
"employment_date" => "date",
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
"remember_token",
|
||||
];
|
||||
protected $with = [
|
||||
"profile",
|
||||
];
|
||||
protected $perPage = 50;
|
||||
|
||||
public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(Profile::class);
|
||||
}
|
||||
|
||||
public function vacationLimits(): HasMany
|
||||
{
|
||||
@ -70,30 +73,9 @@ class User extends Authenticatable
|
||||
return $this->hasMany(Vacation::class);
|
||||
}
|
||||
|
||||
public function scopeSearch(Builder $query, ?string $text): Builder
|
||||
public function keys(): HasMany
|
||||
{
|
||||
if ($text === null) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
return $query
|
||||
->where("first_name", "ILIKE", $text)
|
||||
->orWhere("last_name", "ILIKE", $text)
|
||||
->orWhere("email", "ILIKE", $text);
|
||||
}
|
||||
|
||||
public function getAvatar(): string
|
||||
{
|
||||
$colors = config("colors");
|
||||
|
||||
return $this->getAvatarGenerator()
|
||||
->backgroundColor($colors[strlen($this->fullname) % count($colors)])
|
||||
->image();
|
||||
}
|
||||
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return "{$this->first_name} {$this->last_name}";
|
||||
return $this->hasMany(Key::class);
|
||||
}
|
||||
|
||||
public function hasRole(Role $role): bool
|
||||
@ -101,9 +83,50 @@ class User extends Authenticatable
|
||||
return $this->role === $role;
|
||||
}
|
||||
|
||||
protected function getAvatarNameKey(): string
|
||||
public function hasVacationLimit(YearPeriod $yearPeriod): bool
|
||||
{
|
||||
return "fullName";
|
||||
return $this->vacationLimits()
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->whereNotNull("days")
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function scopeSearch(Builder $query, ?string $text): Builder
|
||||
{
|
||||
if ($text === null) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
return $query
|
||||
->where("email", "ILIKE", "%{$text}%")
|
||||
->orWhereRelation(
|
||||
"profile",
|
||||
fn(Builder $query): Builder => $query
|
||||
->where("first_name", "ILIKE", "%{$text}%")
|
||||
->orWhere("last_name", "ILIKE", "%{$text}%"),
|
||||
);
|
||||
}
|
||||
|
||||
public function scopeOrderByProfileField(Builder $query, string $field): Builder
|
||||
{
|
||||
$profileQuery = Profile::query()->select($field)->whereColumn("users.id", "profiles.user_id");
|
||||
|
||||
return $query->orderBy($profileQuery);
|
||||
}
|
||||
|
||||
public function scopeWithVacationLimitIn(Builder $query, YearPeriod $yearPeriod): Builder
|
||||
{
|
||||
return $query->whereRelation(
|
||||
"vacationlimits",
|
||||
fn(Builder $query): Builder => $query
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->whereNotNull("days"),
|
||||
);
|
||||
}
|
||||
|
||||
public function routeNotificationForSlack()
|
||||
{
|
||||
return $this->profile->slack_id;
|
||||
}
|
||||
|
||||
protected static function newFactory(): UserFactory
|
||||
|
@ -4,15 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Domain\VacationRequestStatesRetriever;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property Carbon $date
|
||||
* @property string $event_id
|
||||
* @property User $user
|
||||
* @property VacationRequest $vacationRequest
|
||||
* @property YearPeriod $yearPeriod
|
||||
@ -41,4 +43,28 @@ class Vacation extends Model
|
||||
{
|
||||
return $this->belongsTo(YearPeriod::class);
|
||||
}
|
||||
|
||||
public function scopeApproved(Builder $query): Builder
|
||||
{
|
||||
return $query->whereRelation(
|
||||
"vacationRequest",
|
||||
fn(Builder $query): Builder => $query->states(VacationRequestStatesRetriever::successStates()),
|
||||
);
|
||||
}
|
||||
|
||||
public function scopePending(Builder $query): Builder
|
||||
{
|
||||
return $query->whereRelation(
|
||||
"vacationRequest",
|
||||
fn(Builder $query): Builder => $query->states(VacationRequestStatesRetriever::pendingStates()),
|
||||
);
|
||||
}
|
||||
|
||||
public function scopeWhereTypes(Builder $query, Collection $types): Builder
|
||||
{
|
||||
return $query->whereRelation(
|
||||
"vacationRequest",
|
||||
fn(Builder $query): Builder => $query->whereIn("type", $types),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Toby\Eloquent\Models;
|
||||
|
||||
use Database\Factories\VacationLimitFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -37,13 +36,6 @@ class VacationLimit extends Model
|
||||
return $this->belongsTo(YearPeriod::class);
|
||||
}
|
||||
|
||||
public function scopeOrderByUserField(Builder $query, string $field): Builder
|
||||
{
|
||||
$userQuery = User::query()->select($field)->whereColumn("vacation_limits.user_id", "users.id");
|
||||
|
||||
return $query->orderBy($userQuery);
|
||||
}
|
||||
|
||||
protected static function newFactory(): VacationLimitFactory
|
||||
{
|
||||
return VacationLimitFactory::new();
|
||||
|
@ -6,10 +6,12 @@ namespace Toby\Eloquent\Models;
|
||||
|
||||
use Database\Factories\VacationRequestFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\AsCollection;
|
||||
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;
|
||||
@ -18,6 +20,7 @@ use Toby\Domain\States\VacationRequest\VacationRequestState;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property VacationType $type
|
||||
* @property VacationRequestState $state
|
||||
* @property Carbon $from
|
||||
@ -29,6 +32,7 @@ use Toby\Domain\States\VacationRequest\VacationRequestState;
|
||||
* @property YearPeriod $yearPeriod
|
||||
* @property Collection $activities
|
||||
* @property Collection $vacations
|
||||
* @property Collection $event_ids
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
@ -38,13 +42,14 @@ class VacationRequest extends Model
|
||||
use HasStates;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
"type" => VacationType::class,
|
||||
"state" => VacationRequestState::class,
|
||||
"from" => "date",
|
||||
"to" => "date",
|
||||
"event_ids" => AsCollection::class,
|
||||
];
|
||||
protected $perPage = 50;
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
@ -81,6 +86,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)
|
||||
|
@ -22,7 +22,6 @@ class VacationRequestActivity extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
"from" => VacationRequestState::class,
|
||||
"to" => VacationRequestState::class,
|
||||
|
@ -15,6 +15,7 @@ use Illuminate\Support\Collection;
|
||||
* @property int $id
|
||||
* @property int $year
|
||||
* @property Collection $vacationLimits
|
||||
* @property Collection $vacationRequests
|
||||
* @property Collection $holidays
|
||||
*/
|
||||
class YearPeriod extends Model
|
||||
@ -41,6 +42,11 @@ class YearPeriod extends Model
|
||||
return $this->hasMany(VacationLimit::class);
|
||||
}
|
||||
|
||||
public function vacationRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(VacationRequest::class);
|
||||
}
|
||||
|
||||
public function holidays(): HasMany
|
||||
{
|
||||
return $this->hasMany(Holiday::class);
|
||||
|
@ -4,19 +4,22 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Observers;
|
||||
|
||||
use Toby\Eloquent\Helpers\YearPeriodRetriever;
|
||||
use Illuminate\Contracts\Hashing\Hasher;
|
||||
use Illuminate\Support\Str;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class UserObserver
|
||||
{
|
||||
public function __construct(
|
||||
protected YearPeriodRetriever $yearPeriodRetriever,
|
||||
protected Hasher $hash,
|
||||
) {}
|
||||
|
||||
public function created(User $user): void
|
||||
public function creating(User $user): void
|
||||
{
|
||||
$user->vacationLimits()->create([
|
||||
"year_period_id" => $this->yearPeriodRetriever->current()->id,
|
||||
]);
|
||||
/**
|
||||
* A random password for user is generated because AuthenticateSession middleware needs a user's password
|
||||
* for some checks. Users use Google to login, so they don't need to know the password (GitHub issue #84)
|
||||
*/
|
||||
$user->password = $this->hash->make(Str::random(40));
|
||||
}
|
||||
}
|
||||
|
@ -4,25 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Observers;
|
||||
|
||||
use Illuminate\Contracts\Auth\Factory as Auth;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestObserver
|
||||
{
|
||||
public function __construct(
|
||||
protected Auth $auth,
|
||||
protected Dispatcher $dispatcher,
|
||||
) {}
|
||||
|
||||
public function creating(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$year = $vacationRequest->from->year;
|
||||
$count = $vacationRequest->yearPeriod->vacationRequests()->count();
|
||||
$number = $count + 1;
|
||||
|
||||
$vacationRequestNumber = $vacationRequest->user->vacationRequests()
|
||||
->whereYear("from", $year)
|
||||
->count() + 1;
|
||||
|
||||
$vacationRequest->name = "{$vacationRequestNumber}/${year}";
|
||||
$vacationRequest->name = "{$number}/{$vacationRequest->yearPeriod->year}";
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Eloquent\Scopes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Scope;
|
||||
use Toby\Eloquent\Helpers\YearPeriodRetriever;
|
||||
|
||||
class SelectedYearPeriodScope implements Scope
|
||||
{
|
||||
public function __construct(
|
||||
protected YearPeriodRetriever $yearPeriodRetriever,
|
||||
) {}
|
||||
|
||||
public function apply(Builder $builder, Model $model): Builder
|
||||
{
|
||||
return $builder->where("year_period_id", $this->yearPeriodRetriever->selected()->id);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class CreateUserCommand extends Command
|
||||
{
|
||||
protected $signature = "user:create {email : an email for the user}";
|
||||
protected $description = "Creates a user";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$email = $this->argument("email");
|
||||
|
||||
User::factory([
|
||||
"email" => $email,
|
||||
])->create();
|
||||
|
||||
$this->info("The user has been created");
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Toby\Eloquent\Models\User;
|
||||
|
||||
class MoveUserDataToProfile extends Command
|
||||
{
|
||||
protected $signature = "toby:move-user-data-to-profile";
|
||||
protected $description = "Move user data to their profiles";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$users = User::all();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->profile()->updateOrCreate(["user_id" => $user->id], [
|
||||
"first_name" => $user->first_name,
|
||||
"last_name" => $user->last_name,
|
||||
"position" => $user->position,
|
||||
"employment_form" => $user->employment_form,
|
||||
"employment_date" => $user->employment_date,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Toby\Eloquent\Models\YearPeriod;
|
||||
|
||||
class RebuildDocumentNumberingSystem extends Command
|
||||
{
|
||||
protected $signature = "toby:rebuild-document-numbering-system";
|
||||
protected $description = "Rebuilds the document numbering system to {number}/{year}";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$yearPeriods = YearPeriod::all();
|
||||
|
||||
foreach ($yearPeriods as $yearPeriod) {
|
||||
$number = 1;
|
||||
|
||||
$vacationRequests = $yearPeriod
|
||||
->vacationRequests()
|
||||
->oldest()
|
||||
->get();
|
||||
|
||||
foreach ($vacationRequests as $vacationRequest) {
|
||||
$vacationRequest->update(["name" => "{$number}/{$yearPeriod->year}"]);
|
||||
|
||||
$number++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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" => __("Daily summary for day :day", ["day" => $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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
use Toby\Eloquent\Helpers\YearPeriodRetriever;
|
||||
use Toby\Eloquent\Models\Holiday;
|
||||
use Toby\Eloquent\Models\Vacation;
|
||||
use Toby\Infrastructure\Http\Resources\SimpleVacationRequestResource;
|
||||
|
||||
class AnnualSummaryController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response
|
||||
{
|
||||
$yearPeriod = $yearPeriodRetriever->selected();
|
||||
|
||||
$holidays = $yearPeriod->holidays()
|
||||
->get();
|
||||
|
||||
$vacations = $request->user()
|
||||
->vacations()
|
||||
->with("vacationRequest.vacations")
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->approved()
|
||||
->get();
|
||||
|
||||
$pendingVacations = $request->user()
|
||||
->vacations()
|
||||
->with("vacationRequest.vacations")
|
||||
->whereBelongsTo($yearPeriod)
|
||||
->pending()
|
||||
->get();
|
||||
|
||||
return inertia("AnnualSummary", [
|
||||
"holidays" => $holidays->mapWithKeys(
|
||||
fn(Holiday $holiday): array => [$holiday->date->toDateString() => $holiday->name],
|
||||
),
|
||||
"vacations" => $vacations->mapWithKeys(
|
||||
fn(Vacation $vacation): array => [
|
||||
$vacation->date->toDateString() => new SimpleVacationRequestResource($vacation->vacationRequest),
|
||||
],
|
||||
),
|
||||
"pendingVacations" => $pendingVacations->mapWithKeys(
|
||||
fn(Vacation $vacation): array => [
|
||||
$vacation->date->toDateString() => new SimpleVacationRequestResource($vacation->vacationRequest),
|
||||
],
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user