diff --git a/app/Domain/VacationDaysCalculator.php b/app/Domain/VacationDaysCalculator.php new file mode 100644 index 0000000..865740d --- /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; + } +} \ No newline at end of file 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..df883cc --- /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..6d988fe --- /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..5d36a66 100644 --- a/app/Domain/Validation/Rules/VacationRequestRule.php +++ b/app/Domain/Validation/Rules/VacationRequestRule.php @@ -4,10 +4,18 @@ declare(strict_types=1); namespace Toby\Domain\Validation\Rules; -use Closure; +use Illuminate\Validation\ValidationException; use Toby\Eloquent\Models\VacationRequest; -interface VacationRequestRule +abstract class VacationRequestRule { - public function check(VacationRequest $vacationRequest, Closure $next); + public function check(VacationRequest $vacationRequest): void + { + if (! $this->passes($vacationRequest)) { + throw ValidationException::withMessages(["vacationRequest" => $this->errorMessage()]); + } + } + + public abstract function passes(VacationRequest $vacationRequest): bool; + public abstract function errorMessage(): string; } diff --git a/app/Domain/Validation/VacationRequestValidator.php b/app/Domain/Validation/VacationRequestValidator.php index f3cfa23..4f5cbdb 100644 --- a/app/Domain/Validation/VacationRequestValidator.php +++ b/app/Domain/Validation/VacationRequestValidator.php @@ -5,19 +5,21 @@ declare(strict_types=1); namespace Toby\Domain\Validation; use Illuminate\Pipeline\Pipeline; -use Toby\Domain\Validation\Rules\ApprovedVacationDaysInSameRange; +use Toby\Domain\Validation\Rules\NoApprovedVacationRequestsInRange; use Toby\Domain\Validation\Rules\DoesNotExceedLimitRule; use Toby\Domain\Validation\Rules\MinimumOneVacationDayRule; -use Toby\Domain\Validation\Rules\PendingVacationRequestInSameRange; +use Toby\Domain\Validation\Rules\NoPendingVacationRequestInRange; +use Toby\Domain\Validation\Rules\VacationRangeIsInTheSameYearRule; 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( @@ -27,9 +29,8 @@ class VacationRequestValidator public function validate(VacationRequest $vacationRequest): void { - $this->pipeline - ->send($vacationRequest) - ->through($this->rules) - ->via("check"); + foreach ($this->rules as $rule) { + app($rule)->check($vacationRequest); + } } } diff --git a/app/Eloquent/Models/VacationRequest.php b/app/Eloquent/Models/VacationRequest.php index 9c7ada0..bb57d4a 100644 --- a/app/Eloquent/Models/VacationRequest.php +++ b/app/Eloquent/Models/VacationRequest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Toby\Eloquent\Models; +use Carbon\CarbonInterface; use Database\Factories\VacationRequestFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -68,6 +69,12 @@ class VacationRequest extends Model return $query->whereIn("state", $states); } + public function scopeOverlapsWith(Builder $query, VacationRequest $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..359d290 100644 --- a/app/Infrastructure/Http/Controllers/VacationRequestController.php +++ b/app/Infrastructure/Http/Controllers/VacationRequestController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Toby\Infrastructure\Http\Controllers; use Barryvdh\DomPDF\Facade\Pdf; +use Carbon\Carbon; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response as LaravelResponse; 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/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..a2b8f31 100644 --- a/database/factories/VacationRequestFactory.php +++ b/database/factories/VacationRequestFactory.php @@ -42,6 +42,8 @@ class VacationRequestFactory extends Factory ->whereYear("from", $year) ->count() + 1; + dump($user->vacationRequests()->count()); + return "{$number}/{$year}"; } } 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..5fa2919 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" > +
+
+
+ +
+
+

+ Wniosek nie mógł zostać utworzony +

+
+ {{ form.errors.vacationRequest }} +
+
+
+
@@ -21,9 +39,9 @@
- {{ form.vacationType.label }} + {{ form.type.label }} @@ -38,15 +56,15 @@ class="absolute z-10 mt-1 w-full max-w-lg bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" >
  • - {{ vacationType.label }} + {{ type.label }}

    - {{ form.errors.vacationType }} + {{ form.errors.type }}

  • @@ -77,18 +95,18 @@

    - {{ form.errors.dateFrom }} + {{ form.errors.from }}

    @@ -102,25 +120,25 @@

    - {{ form.errors.dateTo }} + {{ form.errors.to }}

    Liczba dni urlopu
    - 1 + {{ estimatedDays.length }}
    @@ -164,8 +182,9 @@ import {useForm} from '@inertiajs/inertia-vue3' import FlatPickr from 'vue-flatpickr-component' import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions} from '@headlessui/vue' -import {CheckIcon, SelectorIcon} from '@heroicons/vue/solid' -import {reactive} from 'vue' +import {CheckIcon, SelectorIcon, XCircleIcon} from '@heroicons/vue/solid' +import {reactive, ref} from 'vue' +import axios from 'axios' export default { name: 'VacationRequestCreate', @@ -178,6 +197,7 @@ export default { ListboxOptions, CheckIcon, SelectorIcon, + XCircleIcon, }, props: { vacationTypes: { @@ -191,12 +211,14 @@ export default { }, setup(props) { const form = useForm({ - dateFrom: null, - dateTo: null, - vacationType: props.vacationTypes[0], + from: null, + to: null, + type: props.vacationTypes[0], comment: null, }) + const estimatedDays = ref([]) + const disableDates = [ date => (date.getDay() === 0 || date.getDay() === 6), ] @@ -213,6 +235,7 @@ export default { return { form, + estimatedDays, fromInputConfig, toInputConfig, } @@ -221,18 +244,26 @@ export default { createForm() { this.form .transform(data => ({ - from: data.dateFrom, - to: data.dateTo, - type: data.vacationType.value, - comment: data.comment, + ...data, + type: data.type.value, })) .post('/vacation-requests') }, onFromChange(selectedDates, dateStr) { this.toInputConfig.minDate = dateStr + + this.refreshEstimatedDays(this.form.from, this.form.to) }, onToChange(selectedDates, dateStr) { this.fromInputConfig.maxDate = dateStr + + this.refreshEstimatedDays(this.form.from, this.form.to) + }, + refreshEstimatedDays(from, to) { + if (from && to) { + axios.post('/api/calculate-vacation-days', {from, to}) + .then(res => this.estimatedDays = res.data) + } }, }, diff --git a/routes/api.php b/routes/api.php index 174d7fd..f418a99 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,3 +1,10 @@ group(function (): void { + Route::post("calculate-vacation-days", CalculateVacationDaysController::class); +});