Merge branch 'main' into #22-vacation-calendar
This commit is contained in:
42
app/Domain/VacationDaysCalculator.php
Normal file
42
app/Domain/VacationDaysCalculator.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Eloquent\Models\YearPeriod;
|
||||
|
||||
class VacationDaysCalculator
|
||||
{
|
||||
public function calculateDays(YearPeriod $yearPeriod, CarbonInterface $from, CarbonInterface $to): Collection
|
||||
{
|
||||
$period = CarbonPeriod::create($from, $to);
|
||||
$holidays = $yearPeriod->holidays()->pluck("date");
|
||||
|
||||
$validDays = collect();
|
||||
|
||||
foreach ($period as $day) {
|
||||
if ($this->passes($day, $holidays)) {
|
||||
$validDays->add($day);
|
||||
}
|
||||
}
|
||||
|
||||
return $validDays;
|
||||
}
|
||||
|
||||
protected function passes(CarbonInterface $day, Collection $holidays): bool
|
||||
{
|
||||
if ($day->isWeekend()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($holidays->contains($day)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Closure;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class ApprovedVacationDaysInSameRange implements VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest, Closure $next)
|
||||
{
|
||||
return $next($vacationRequest);
|
||||
}
|
||||
}
|
@@ -4,13 +4,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Closure;
|
||||
use Toby\Domain\VacationTypeConfigRetriever;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class DoesNotExceedLimitRule implements VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest, Closure $next)
|
||||
public function __construct(
|
||||
protected VacationTypeConfigRetriever $configRetriever,
|
||||
) {
|
||||
}
|
||||
|
||||
public function check(VacationRequest $vacationRequest): bool
|
||||
{
|
||||
return $next($vacationRequest);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function errorMessage(): string
|
||||
{
|
||||
return __("You have exceeded your vacation limit.");
|
||||
}
|
||||
}
|
||||
|
@@ -4,13 +4,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Closure;
|
||||
use Toby\Domain\VacationDaysCalculator;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class MinimumOneVacationDayRule implements VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest, Closure $next)
|
||||
public function __construct(
|
||||
protected VacationDaysCalculator $vacationDaysCalculator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function check(VacationRequest $vacationRequest): bool
|
||||
{
|
||||
return $next($vacationRequest);
|
||||
return $this->vacationDaysCalculator
|
||||
->calculateDays($vacationRequest->yearPeriod, $vacationRequest->from, $vacationRequest->to)
|
||||
->isNotEmpty();
|
||||
}
|
||||
|
||||
public function errorMessage(): string
|
||||
{
|
||||
return __("Vacation needs minimum one day.");
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Toby\Domain\Enums\VacationRequestState;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class NoApprovedVacationRequestsInRange implements VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest): bool
|
||||
{
|
||||
return !$vacationRequest
|
||||
->user
|
||||
->vacationRequests()
|
||||
->overlapsWith($vacationRequest)
|
||||
->states(VacationRequestState::successStates())
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function errorMessage(): string
|
||||
{
|
||||
return __("You have approved vacation request in this range.");
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Toby\Domain\Enums\VacationRequestState;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class NoPendingVacationRequestInRange implements VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest): bool
|
||||
{
|
||||
return !$vacationRequest
|
||||
->user
|
||||
->vacationRequests()
|
||||
->overlapsWith($vacationRequest)
|
||||
->states(VacationRequestState::pendingStates())
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function errorMessage(): string
|
||||
{
|
||||
return __("You have pending vacation request in this range.");
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Closure;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class PendingVacationRequestInSameRange implements VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest, Closure $next)
|
||||
{
|
||||
return $next($vacationRequest);
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Closure;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class UsedVacationDaysInSameRange
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest, Closure $next)
|
||||
{
|
||||
return $next($vacationRequest);
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRangeIsInTheSameYearRule implements VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest): bool
|
||||
{
|
||||
return $vacationRequest->from->isSameYear($vacationRequest->to);
|
||||
}
|
||||
|
||||
public function errorMessage(): string
|
||||
{
|
||||
return __("The vacation request cannot be created at the turn of the year.");
|
||||
}
|
||||
}
|
@@ -4,10 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation\Rules;
|
||||
|
||||
use Closure;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
|
||||
interface VacationRequestRule
|
||||
{
|
||||
public function check(VacationRequest $vacationRequest, Closure $next);
|
||||
public function check(VacationRequest $vacationRequest): bool;
|
||||
public function errorMessage(): string;
|
||||
}
|
||||
|
@@ -4,32 +4,49 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Domain\Validation;
|
||||
|
||||
use Illuminate\Pipeline\Pipeline;
|
||||
use Toby\Domain\Validation\Rules\ApprovedVacationDaysInSameRange;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Toby\Domain\Validation\Rules\DoesNotExceedLimitRule;
|
||||
use Toby\Domain\Validation\Rules\MinimumOneVacationDayRule;
|
||||
use Toby\Domain\Validation\Rules\PendingVacationRequestInSameRange;
|
||||
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\Eloquent\Models\VacationRequest;
|
||||
|
||||
class VacationRequestValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
VacationRangeIsInTheSameYearRule::class,
|
||||
MinimumOneVacationDayRule::class,
|
||||
DoesNotExceedLimitRule::class,
|
||||
PendingVacationRequestInSameRange::class,
|
||||
ApprovedVacationDaysInSameRange::class,
|
||||
NoPendingVacationRequestInRange::class,
|
||||
NoApprovedVacationRequestsInRange::class,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Pipeline $pipeline,
|
||||
protected Container $container,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function validate(VacationRequest $vacationRequest): void
|
||||
{
|
||||
$this->pipeline
|
||||
->send($vacationRequest)
|
||||
->through($this->rules)
|
||||
->via("check");
|
||||
foreach ($this->rules as $rule) {
|
||||
$rule = $this->makeRule($rule);
|
||||
|
||||
if (!$rule->check($vacationRequest)) {
|
||||
throw ValidationException::withMessages([
|
||||
"vacationRequest" => $rule->errorMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function makeRule(string $class): VacationRequestRule
|
||||
{
|
||||
return $this->container->make($class);
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ use Toby\Domain\Enums\Role;
|
||||
* @property string $last_name
|
||||
* @property string $email
|
||||
* @property string $avatar
|
||||
* @property string $position
|
||||
* @property Role $role
|
||||
* @property EmploymentForm $employment_form
|
||||
* @property Carbon $employment_date
|
||||
|
@@ -21,9 +21,13 @@ use Toby\Domain\Enums\VacationType;
|
||||
* @property VacationRequestState $state
|
||||
* @property Carbon $from
|
||||
* @property Carbon $to
|
||||
* @property int $estimated_days
|
||||
* @property string $comment
|
||||
* @property User $user
|
||||
* @property YearPeriod $yearPeriod
|
||||
* @property Collection $activities
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class VacationRequest extends Model
|
||||
{
|
||||
@@ -43,6 +47,11 @@ class VacationRequest extends Model
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function yearPeriod(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(YearPeriod::class);
|
||||
}
|
||||
|
||||
public function activities(): HasMany
|
||||
{
|
||||
return $this->hasMany(VacationRequestActivity::class);
|
||||
@@ -60,6 +69,12 @@ class VacationRequest extends Model
|
||||
return $query->whereIn("state", $states);
|
||||
}
|
||||
|
||||
public function scopeOverlapsWith(Builder $query, self $vacationRequest): Builder
|
||||
{
|
||||
return $query->where("from", "<=", $vacationRequest->to)
|
||||
->where("to", ">=", $vacationRequest->from);
|
||||
}
|
||||
|
||||
protected static function newFactory(): VacationRequestFactory
|
||||
{
|
||||
return VacationRequestFactory::new();
|
||||
|
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Toby\Domain\VacationDaysCalculator;
|
||||
use Toby\Infrastructure\Http\Controllers\Controller;
|
||||
use Toby\Infrastructure\Http\Requests\Api\CalculateVacationDaysRequest;
|
||||
|
||||
class CalculateVacationDaysController extends Controller
|
||||
{
|
||||
public function __invoke(CalculateVacationDaysRequest $request, VacationDaysCalculator $calculator): JsonResponse
|
||||
{
|
||||
$days = $calculator->calculateDays($request->yearPeriod(), $request->from(), $request->to());
|
||||
|
||||
return new JsonResponse($days->all());
|
||||
}
|
||||
}
|
@@ -4,13 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Controllers;
|
||||
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response as LaravelResponse;
|
||||
use Inertia\Response;
|
||||
use Toby\Domain\Enums\VacationRequestState;
|
||||
use Toby\Domain\Enums\VacationType;
|
||||
use Toby\Domain\VacationDaysCalculator;
|
||||
use Toby\Domain\VacationRequestStateManager;
|
||||
use Toby\Domain\Validation\VacationRequestValidator;
|
||||
use Toby\Eloquent\Helpers\YearPeriodRetriever;
|
||||
use Toby\Eloquent\Models\VacationRequest;
|
||||
use Toby\Infrastructure\Http\Requests\VacationRequestRequest;
|
||||
use Toby\Infrastructure\Http\Resources\VacationRequestActivityResource;
|
||||
@@ -18,12 +22,13 @@ use Toby\Infrastructure\Http\Resources\VacationRequestResource;
|
||||
|
||||
class VacationRequestController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
public function index(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response
|
||||
{
|
||||
$status = $request->get("status", "all");
|
||||
|
||||
$vacationRequests = $request->user()
|
||||
->vacationRequests()
|
||||
->where("year_period_id", $yearPeriodRetriever->selected()->id)
|
||||
->latest()
|
||||
->states(VacationRequestState::filterByStatus($status))
|
||||
->paginate();
|
||||
@@ -44,6 +49,15 @@ class VacationRequestController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function download(VacationRequest $vacationRequest): LaravelResponse
|
||||
{
|
||||
$pdf = PDF::loadView("pdf.vacation-request", [
|
||||
"vacationRequest" => $vacationRequest,
|
||||
]);
|
||||
|
||||
return $pdf->stream();
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return inertia("VacationRequest/Create", [
|
||||
@@ -55,9 +69,15 @@ class VacationRequestController extends Controller
|
||||
VacationRequestRequest $request,
|
||||
VacationRequestValidator $vacationRequestValidator,
|
||||
VacationRequestStateManager $stateManager,
|
||||
VacationDaysCalculator $vacationDaysCalculator,
|
||||
): RedirectResponse {
|
||||
/** @var VacationRequest $vacationRequest */
|
||||
$vacationRequest = $request->user()->vacationRequests()->make($request->data());
|
||||
$vacationRequest->estimated_days = $vacationDaysCalculator->calculateDays(
|
||||
$vacationRequest->yearPeriod,
|
||||
$vacationRequest->from,
|
||||
$vacationRequest->to,
|
||||
)->count();
|
||||
|
||||
$vacationRequestValidator->validate($vacationRequest);
|
||||
|
||||
|
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Infrastructure\Http\Requests\Api;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Toby\Eloquent\Models\YearPeriod;
|
||||
use Toby\Infrastructure\Http\Rules\YearPeriodExists;
|
||||
|
||||
class CalculateVacationDaysRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
"from" => ["required", "date_format:Y-m-d", new YearPeriodExists()],
|
||||
"to" => ["required", "date_format:Y-m-d", new YearPeriodExists()],
|
||||
];
|
||||
}
|
||||
|
||||
public function from(): Carbon
|
||||
{
|
||||
return Carbon::create($this->request->get("from"));
|
||||
}
|
||||
|
||||
public function to(): Carbon
|
||||
{
|
||||
return Carbon::create($this->request->get("to"));
|
||||
}
|
||||
|
||||
public function yearPeriod(): YearPeriod
|
||||
{
|
||||
return YearPeriod::findByYear(Carbon::create($this->request->get("from"))->year);
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ class UserRequest extends FormRequest
|
||||
"lastName" => ["required", "min:3", "max:80"],
|
||||
"email" => ["required", "email", Rule::unique("users", "email")->ignore($this->user)],
|
||||
"role" => ["required", new Enum(Role::class)],
|
||||
"position" => ["required"],
|
||||
"employmentForm" => ["required", new Enum(EmploymentForm::class)],
|
||||
"employmentDate" => ["required", "date_format:Y-m-d"],
|
||||
];
|
||||
@@ -30,6 +31,7 @@ class UserRequest extends FormRequest
|
||||
"first_name" => $this->get("firstName"),
|
||||
"last_name" => $this->get("lastName"),
|
||||
"email" => $this->get("email"),
|
||||
"position" => $this->get("position"),
|
||||
"role" => $this->get("role"),
|
||||
"employment_form" => $this->get("employmentForm"),
|
||||
"employment_date" => $this->get("employmentDate"),
|
||||
|
@@ -5,8 +5,10 @@ declare(strict_types=1);
|
||||
namespace Toby\Infrastructure\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
use Toby\Domain\Enums\VacationType;
|
||||
use Toby\Eloquent\Models\YearPeriod;
|
||||
use Toby\Infrastructure\Http\Rules\YearPeriodExists;
|
||||
|
||||
class VacationRequestRequest extends FormRequest
|
||||
@@ -23,10 +25,13 @@ class VacationRequestRequest extends FormRequest
|
||||
|
||||
public function data(): array
|
||||
{
|
||||
$from = $this->get("from");
|
||||
|
||||
return [
|
||||
"type" => $this->get("type"),
|
||||
"from" => $this->get("from"),
|
||||
"from" => $from,
|
||||
"to" => $this->get("to"),
|
||||
"year_period_id" => YearPeriod::findByYear(Carbon::create($from)->year)->id,
|
||||
"comment" => $this->get("comment"),
|
||||
];
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ class UserFormDataResource extends JsonResource
|
||||
"lastName" => $this->last_name,
|
||||
"email" => $this->email,
|
||||
"role" => $this->role,
|
||||
"position" => $this->position,
|
||||
"employmentForm" => $this->employment_form,
|
||||
"employmentDate" => $this->employment_date->toDateString(),
|
||||
];
|
||||
|
@@ -17,6 +17,7 @@ class UserResource extends JsonResource
|
||||
"name" => $this->fullName,
|
||||
"email" => $this->email,
|
||||
"role" => $this->role->label(),
|
||||
"position" => $this->position,
|
||||
"avatar" => asset($this->avatar),
|
||||
"deleted" => $this->trashed(),
|
||||
"employmentForm" => $this->employment_form->label(),
|
||||
|
@@ -20,6 +20,7 @@ class VacationRequestResource extends JsonResource
|
||||
"state" => $this->state->label(),
|
||||
"from" => $this->from->toDisplayString(),
|
||||
"to" => $this->to->toDisplayString(),
|
||||
"estimatedDays" => $this->estimated_days,
|
||||
"comment" => $this->comment,
|
||||
];
|
||||
}
|
||||
|
Reference in New Issue
Block a user