diff --git a/app/Domain/VacationDaysCalculator.php b/app/Domain/VacationDaysCalculator.php new file mode 100644 index 0000000..fcbc9e8 --- /dev/null +++ b/app/Domain/VacationDaysCalculator.php @@ -0,0 +1,42 @@ +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; + } +} diff --git a/app/Domain/Validation/Rules/ApprovedVacationDaysInSameRange.php b/app/Domain/Validation/Rules/ApprovedVacationDaysInSameRange.php deleted file mode 100644 index eb39bf4..0000000 --- a/app/Domain/Validation/Rules/ApprovedVacationDaysInSameRange.php +++ /dev/null @@ -1,16 +0,0 @@ -vacationDaysCalculator + ->calculateDays($vacationRequest->yearPeriod, $vacationRequest->from, $vacationRequest->to) + ->isNotEmpty(); + } + + public function errorMessage(): string + { + return __("Vacation needs minimum one day."); } } diff --git a/app/Domain/Validation/Rules/NoApprovedVacationRequestsInRange.php b/app/Domain/Validation/Rules/NoApprovedVacationRequestsInRange.php new file mode 100644 index 0000000..d06fd95 --- /dev/null +++ b/app/Domain/Validation/Rules/NoApprovedVacationRequestsInRange.php @@ -0,0 +1,26 @@ +user + ->vacationRequests() + ->overlapsWith($vacationRequest) + ->states(VacationRequestState::successStates()) + ->exists(); + } + + public function errorMessage(): string + { + return __("You have approved vacation request in this range."); + } +} diff --git a/app/Domain/Validation/Rules/NoPendingVacationRequestInRange.php b/app/Domain/Validation/Rules/NoPendingVacationRequestInRange.php new file mode 100644 index 0000000..3031b56 --- /dev/null +++ b/app/Domain/Validation/Rules/NoPendingVacationRequestInRange.php @@ -0,0 +1,26 @@ +user + ->vacationRequests() + ->overlapsWith($vacationRequest) + ->states(VacationRequestState::pendingStates()) + ->exists(); + } + + public function errorMessage(): string + { + return __("You have pending vacation request in this range."); + } +} diff --git a/app/Domain/Validation/Rules/PendingVacationRequestInSameRange.php b/app/Domain/Validation/Rules/PendingVacationRequestInSameRange.php deleted file mode 100644 index a0081b6..0000000 --- a/app/Domain/Validation/Rules/PendingVacationRequestInSameRange.php +++ /dev/null @@ -1,16 +0,0 @@ -from->isSameYear($vacationRequest->to); + } + + public function errorMessage(): string + { + return __("The vacation request cannot be created at the turn of the year."); + } +} diff --git a/app/Domain/Validation/Rules/VacationRequestRule.php b/app/Domain/Validation/Rules/VacationRequestRule.php index e50c92b..07af8d2 100644 --- a/app/Domain/Validation/Rules/VacationRequestRule.php +++ b/app/Domain/Validation/Rules/VacationRequestRule.php @@ -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; } diff --git a/app/Domain/Validation/VacationRequestValidator.php b/app/Domain/Validation/VacationRequestValidator.php index f3cfa23..2bc39e6 100644 --- a/app/Domain/Validation/VacationRequestValidator.php +++ b/app/Domain/Validation/VacationRequestValidator.php @@ -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); } } diff --git a/app/Eloquent/Models/VacationRequest.php b/app/Eloquent/Models/VacationRequest.php index 9c7ada0..0a81620 100644 --- a/app/Eloquent/Models/VacationRequest.php +++ b/app/Eloquent/Models/VacationRequest.php @@ -21,6 +21,7 @@ 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 @@ -68,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(); diff --git a/app/Infrastructure/Http/Controllers/Api/CalculateVacationDaysController.php b/app/Infrastructure/Http/Controllers/Api/CalculateVacationDaysController.php new file mode 100644 index 0000000..b8a8c33 --- /dev/null +++ b/app/Infrastructure/Http/Controllers/Api/CalculateVacationDaysController.php @@ -0,0 +1,20 @@ +calculateDays($request->yearPeriod(), $request->from(), $request->to()); + + return new JsonResponse($days->all()); + } +} diff --git a/app/Infrastructure/Http/Controllers/VacationRequestController.php b/app/Infrastructure/Http/Controllers/VacationRequestController.php index 3c2eaf8..ca8be78 100644 --- a/app/Infrastructure/Http/Controllers/VacationRequestController.php +++ b/app/Infrastructure/Http/Controllers/VacationRequestController.php @@ -11,6 +11,7 @@ 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; @@ -64,9 +65,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); diff --git a/app/Infrastructure/Http/Requests/Api/CalculateVacationDaysRequest.php b/app/Infrastructure/Http/Requests/Api/CalculateVacationDaysRequest.php new file mode 100644 index 0000000..72e37de --- /dev/null +++ b/app/Infrastructure/Http/Requests/Api/CalculateVacationDaysRequest.php @@ -0,0 +1,36 @@ + ["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); + } +} diff --git a/app/Infrastructure/Http/Resources/VacationRequestResource.php b/app/Infrastructure/Http/Resources/VacationRequestResource.php index 859dfea..009eeb6 100644 --- a/app/Infrastructure/Http/Resources/VacationRequestResource.php +++ b/app/Infrastructure/Http/Resources/VacationRequestResource.php @@ -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, ]; } diff --git a/config/cors.php b/config/cors.php index c025685..34e1c4f 100644 --- a/config/cors.php +++ b/config/cors.php @@ -10,5 +10,5 @@ return [ "allowed_headers" => ["*"], "exposed_headers" => [], "max_age" => 0, - "supports_credentials" => false, + "supports_credentials" => true, ]; diff --git a/database/factories/VacationRequestFactory.php b/database/factories/VacationRequestFactory.php index e8bec48..3939367 100644 --- a/database/factories/VacationRequestFactory.php +++ b/database/factories/VacationRequestFactory.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Database\Factories; use Carbon\CarbonImmutable; +use Carbon\CarbonPeriod; use Illuminate\Database\Eloquent\Factories\Factory; use Toby\Domain\Enums\VacationRequestState; use Toby\Domain\Enums\VacationType; @@ -15,6 +16,7 @@ use Toby\Eloquent\Models\YearPeriod; class VacationRequestFactory extends Factory { protected $model = VacationRequest::class; + private static int $number = 1; public function definition(): array { @@ -29,6 +31,7 @@ class VacationRequestFactory extends Factory "state" => $this->faker->randomElement(VacationRequestState::cases()), "from" => $from, "to" => $from->addDays($days), + "estimated_days" => fn(array $attributes) => $this->estimateDays($attributes), "comment" => $this->faker->boolean ? $this->faker->paragraph() : null, ]; } @@ -36,12 +39,15 @@ class VacationRequestFactory extends Factory protected function generateName(array $attributes): string { $year = YearPeriod::find($attributes["year_period_id"])->year; - $user = User::find($attributes["user_id"]); - - $number = $user->vacationRequests() - ->whereYear("from", $year) - ->count() + 1; + $number = static::$number++; return "{$number}/{$year}"; } + + protected function estimateDays(array $attributes): int + { + $period = CarbonPeriod::create($attributes["from"], $attributes["to"]); + + return $period->count(); + } } diff --git a/database/migrations/2022_01_26_100039_create_vacation_requests_table.php b/database/migrations/2022_01_26_100039_create_vacation_requests_table.php index f5270da..b3f4e48 100644 --- a/database/migrations/2022_01_26_100039_create_vacation_requests_table.php +++ b/database/migrations/2022_01_26_100039_create_vacation_requests_table.php @@ -18,6 +18,7 @@ return new class() extends Migration { $table->foreignIdFor(YearPeriod::class)->constrained()->cascadeOnDelete(); $table->string("type"); $table->string("state")->nullable(); + $table->integer("estimated_days"); $table->date("from"); $table->date("to"); $table->text("comment")->nullable(); diff --git a/package-lock.json b/package-lock.json index 64ebfd5..7795438 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "application", + "name": "toby", "lockfileVersion": 2, "requires": true, "packages": { @@ -15,6 +15,7 @@ "@tailwindcss/typography": "^0.5.0", "@vue/compiler-sfc": "^3.2.26", "autoprefixer": "^10.4.2", + "axios": "^0.25.0", "echarts": "^5.2.2", "flatpickr": "^4.6.9", "laravel-mix": "^6.0.6", @@ -1741,6 +1742,14 @@ "vue": "^3.0.0" } }, + "node_modules/@inertiajs/inertia/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/@inertiajs/progress": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@inertiajs/progress/-/progress-0.2.7.tgz", @@ -2642,11 +2651,11 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.7" } }, "node_modules/babel-loader": { @@ -10664,6 +10673,16 @@ "axios": "^0.21.1", "deepmerge": "^4.0.0", "qs": "^6.9.0" + }, + "dependencies": { + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + } } }, "@inertiajs/inertia-vue3": { @@ -11468,11 +11487,11 @@ } }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.7" } }, "babel-loader": { diff --git a/package.json b/package.json index 6a0d9af..c69812f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@tailwindcss/typography": "^0.5.0", "@vue/compiler-sfc": "^3.2.26", "autoprefixer": "^10.4.2", + "axios": "^0.25.0", "echarts": "^5.2.2", "flatpickr": "^4.6.9", "laravel-mix": "^6.0.6", diff --git a/resources/js/Pages/VacationRequest/Create.vue b/resources/js/Pages/VacationRequest/Create.vue index 9333545..d2cef55 100644 --- a/resources/js/Pages/VacationRequest/Create.vue +++ b/resources/js/Pages/VacationRequest/Create.vue @@ -10,8 +10,26 @@ class="border-t border-gray-200 px-6" @submit.prevent="createForm" > +
- {{ form.errors.vacationType }} + {{ form.errors.type }}
- {{ form.errors.dateFrom }} + {{ form.errors.from }}
- {{ form.errors.dateTo }} + {{ form.errors.to }}
Proszę o {{ mb_strtolower($vacationRequest->type->label()) }} w okresie od dnia {{ $vacationRequest->from->format("d.m.Y") }} - do dnia {{ $vacationRequest->to->format("d.m.Y") }} włącznie tj. x dni roboczych za rok {{ $vacationRequest->yearPeriod->year }}. + do dnia {{ $vacationRequest->to->format("d.m.Y") }} włącznie tj. {{ $vacationRequest->estimated_days }} dni roboczych za rok {{ $vacationRequest->yearPeriod->year }}.