diff --git a/app/Enums/VacationType.php b/app/Enums/VacationType.php new file mode 100644 index 0000000..dd17efe --- /dev/null +++ b/app/Enums/VacationType.php @@ -0,0 +1,36 @@ +value); + } + + public static function casesToSelect(): array + { + $cases = collect(VacationType::cases()); + + return $cases->map( + fn(VacationType $enum) => [ + "label" => $enum->label(), + "value" => $enum->value, + ], + )->toArray(); + } +} diff --git a/app/Helpers/Rules/MinimumOneVacationDayRule.php b/app/Helpers/Rules/MinimumOneVacationDayRule.php new file mode 100644 index 0000000..23641e5 --- /dev/null +++ b/app/Helpers/Rules/MinimumOneVacationDayRule.php @@ -0,0 +1,16 @@ +pipeline + ->send($vacationRequest) + ->through($this->rules) + ->via("check") + ->then(fn(VacationRequest $vacationRequest) => $vacationRequest); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/VacationRequestController.php b/app/Http/Controllers/VacationRequestController.php new file mode 100644 index 0000000..0a88189 --- /dev/null +++ b/app/Http/Controllers/VacationRequestController.php @@ -0,0 +1,31 @@ +user() + ->vacationRequests() + ->paginate(); + + return inertia("VacationRequest/Index", [ + "requests" => VacationRequestResource::collection($requests), + ]); + } + + public function create(): Response + { + return inertia("VacationRequest/Create", [ + "vacationTypes" => VacationType::casesToSelect(), + ]); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 3d330ee..1777f43 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -23,6 +23,7 @@ use Illuminate\Routing\Middleware\ValidateSignature; use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; +use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; use Toby\Http\Middleware\Authenticate; use Toby\Http\Middleware\HandleInertiaRequests; use Toby\Http\Middleware\RedirectIfAuthenticated; @@ -52,6 +53,7 @@ class Kernel extends HttpKernel HandleInertiaRequests::class, ], "api" => [ + EnsureFrontendRequestsAreStateful::class, "throttle:api", SubstituteBindings::class, ], diff --git a/app/Http/Requests/VacationRequestRequest.php b/app/Http/Requests/VacationRequestRequest.php new file mode 100644 index 0000000..34b185b --- /dev/null +++ b/app/Http/Requests/VacationRequestRequest.php @@ -0,0 +1,33 @@ + ["required", new Enum(VacationType::class)], + "from" => ["required", "date_format:Y-m-d", new YearPeriodExists()], + "to" => ["required", "date_format:Y-m-d", new YearPeriodExists()], + "comment" => ["nullable"], + ]; + } + + public function data(): array + { + return [ + "type" => $this->get("type"), + "from" => $this->get("from"), + "to" => $this->get("to"), + "comment" => $this->get("comment"), + ]; + } +} diff --git a/app/Http/Resources/HolidayResource.php b/app/Http/Resources/HolidayResource.php index 3852f2a..e9bf998 100644 --- a/app/Http/Resources/HolidayResource.php +++ b/app/Http/Resources/HolidayResource.php @@ -15,6 +15,7 @@ class HolidayResource extends JsonResource return [ "id" => $this->id, "name" => $this->name, + "date" => $this->date->toDateString(), "displayDate" => $this->date->toDisplayString(), "dayOfWeek" => $this->date->dayName, ]; diff --git a/app/Http/Resources/UserFormDataResource.php b/app/Http/Resources/UserFormDataResource.php index ef4a9a3..1ca5220 100644 --- a/app/Http/Resources/UserFormDataResource.php +++ b/app/Http/Resources/UserFormDataResource.php @@ -18,7 +18,7 @@ class UserFormDataResource extends JsonResource "lastName" => $this->last_name, "email" => $this->email, "employmentForm" => $this->employment_form, - "employmentDate" => $this->employment_date, + "employmentDate" => $this->employment_date->toDateString(), ]; } } diff --git a/app/Http/Resources/VacationRequestResource.php b/app/Http/Resources/VacationRequestResource.php new file mode 100644 index 0000000..da8a0ef --- /dev/null +++ b/app/Http/Resources/VacationRequestResource.php @@ -0,0 +1,24 @@ + $this->id, + "user" => new UserResource($this->user), + "type" => $this->type->label(), + "from" => $this->from->toDisplayString(), + "to" => $this->to->toDisplayString(), + "commment" => $this->comment, + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 22b2d6b..e9101fc 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -23,6 +23,7 @@ use Toby\Enums\EmploymentForm; * @property EmploymentForm $employment_form * @property Carbon $employment_date * @property Collection $vacationLimits + * @property Collection $vacationRequests */ class User extends Authenticatable { @@ -46,6 +47,11 @@ class User extends Authenticatable return $this->hasMany(VacationLimit::class); } + public function vacationRequests(): HasMany + { + return $this->hasMany(VacationRequest::class); + } + public function scopeSearch(Builder $query, ?string $text): Builder { if ($text === null) { diff --git a/app/Models/VacationRequest.php b/app/Models/VacationRequest.php new file mode 100644 index 0000000..a71ef36 --- /dev/null +++ b/app/Models/VacationRequest.php @@ -0,0 +1,33 @@ + VacationType::class, + "from" => "date", + "to" => "date", + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/composer.json b/composer.json index b464c21..f2962a5 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "guzzlehttp/guzzle": "^7.0.1", "inertiajs/inertia-laravel": "^0.5.1", "laravel/framework": "^8.75", + "laravel/sanctum": "^2.14", "laravel/socialite": "^5.2", "laravel/telescope": "^4.6", "laravel/tinker": "^2.5", diff --git a/composer.lock b/composer.lock index 7984d2b..3e222cd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e3c6ffae4c01db02d0471c52d2370b79", + "content-hash": "bf5f7d8f40ecadea64ae8564a3df3110", "packages": [ { "name": "asm89/stack-cors", @@ -1358,6 +1358,70 @@ }, "time": "2022-01-12T16:12:41+00:00" }, + { + "name": "laravel/sanctum", + "version": "v2.14.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "0647a87140c7522e75826cffcadb3ad6e01f71e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/0647a87140c7522e75826cffcadb3ad6e01f71e9", + "reference": "0647a87140c7522e75826cffcadb3ad6e01f71e9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/contracts": "^6.9|^7.0|^8.0|^9.0", + "illuminate/database": "^6.9|^7.0|^8.0|^9.0", + "illuminate/support": "^6.9|^7.0|^8.0|^9.0", + "php": "^7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2022-01-12T15:07:43+00:00" + }, { "name": "laravel/serializable-closure", "version": "v1.0.5", diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..bc4d2f8 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,20 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '' + ))), + 'guard' => ['web'], + 'expiration' => null, + 'middleware' => [ + 'verify_csrf_token' => VerifyCsrfToken::class, + 'encrypt_cookies' => EncryptCookies::class, + ], +]; diff --git a/database/factories/VacationRequestFactory.php b/database/factories/VacationRequestFactory.php new file mode 100644 index 0000000..bd95718 --- /dev/null +++ b/database/factories/VacationRequestFactory.php @@ -0,0 +1,23 @@ + User::factory(), + "type" => $this->faker->randomElement(VacationType::cases()), + "from" => $this->faker->date, + "to" => $this->faker->date, + "comment" => $this->faker->boolean ? $this->faker->paragraph() : null, + ]; + } +} diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php deleted file mode 100644 index ee820cf..0000000 --- a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->morphs("tokenable"); - $table->string("name"); - $table->string("token", 64)->unique(); - $table->text("abilities")->nullable(); - $table->timestamp("last_used_at")->nullable(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists("personal_access_tokens"); - } -}; 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 new file mode 100644 index 0000000..4dda32b --- /dev/null +++ b/database/migrations/2022_01_26_100039_create_vacation_requests_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); + $table->string("type"); + $table->date("from"); + $table->date("to"); + $table->text("comment")->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('vacation_requests'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index e3f9f68..6ae8420 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -28,7 +28,9 @@ class DatabaseSeeder extends Seeder User::factory(9)->create(); User::factory([ "email" => env("LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE"), - ])->create(); + ]) + ->hasVacationRequests(5) + ->create(); $users = User::all(); diff --git a/package-lock.json b/package-lock.json index 7a47b79..3302339 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,14 @@ "@tailwindcss/typography": "^0.5.0", "@vue/compiler-sfc": "^3.2.26", "autoprefixer": "^10.4.2", + "echarts": "^5.2.2", "flatpickr": "^4.6.9", "laravel-mix": "^6.0.6", "lodash": "^4.17.21", "postcss": "^8.4.5", "tailwindcss": "^3.0.13", "vue": "^3.2.26", + "vue-echarts": "^6.0.2", "vue-flatpickr-component": "^9.0.5", "vue-loader": "^17.0.0" }, @@ -3986,6 +3988,20 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, + "node_modules/echarts": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.2.2.tgz", + "integrity": "sha512-yxuBfeIH5c+0FsoRP60w4De6omXhA06c7eUYBsC1ykB6Ys2yK5fSteIYWvkJ4xJVLQgCvAdO8C4mN6MLeJpBaw==", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.2.1" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7797,6 +7813,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "node_modules/resize-detector": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.3.0.tgz", + "integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==" + }, "node_modules/resolve": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", @@ -8854,6 +8875,51 @@ "@vue/shared": "3.2.26" } }, + "node_modules/vue-echarts": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.0.2.tgz", + "integrity": "sha512-9xDokauJtAc389MNKbwi1I0VDmp4Y6ndAJTQ8T9K7H0ffosTe1OJSJbUtkT7/fVLDFzlCcmg2TfAKaMzbpg5yQ==", + "hasInstallScript": true, + "dependencies": { + "resize-detector": "^0.3.0", + "vue-demi": "^0.12.1" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.5", + "echarts": "^5.1.2", + "vue": "^2.6.12 || ^3.1.1" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-echarts/node_modules/vue-demi": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.1.tgz", + "integrity": "sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/vue-eslint-parser": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.0.1.tgz", @@ -9444,6 +9510,19 @@ "engines": { "node": ">=12" } + }, + "node_modules/zrender": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.2.1.tgz", + "integrity": "sha512-M3bPGZuyLTNBC6LiNKXJwSCtglMp8XUEqEBG+2MdICDI3d1s500Y4P0CzldQGsqpRVB7fkvf3BKQQRxsEaTlsw==", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } }, "dependencies": { @@ -12439,6 +12518,22 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, + "echarts": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.2.2.tgz", + "integrity": "sha512-yxuBfeIH5c+0FsoRP60w4De6omXhA06c7eUYBsC1ykB6Ys2yK5fSteIYWvkJ4xJVLQgCvAdO8C4mN6MLeJpBaw==", + "requires": { + "tslib": "2.3.0", + "zrender": "5.2.1" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -15167,6 +15262,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "resize-detector": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.3.0.tgz", + "integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==" + }, "resolve": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", @@ -15964,6 +16064,23 @@ "@vue/shared": "3.2.26" } }, + "vue-echarts": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.0.2.tgz", + "integrity": "sha512-9xDokauJtAc389MNKbwi1I0VDmp4Y6ndAJTQ8T9K7H0ffosTe1OJSJbUtkT7/fVLDFzlCcmg2TfAKaMzbpg5yQ==", + "requires": { + "resize-detector": "^0.3.0", + "vue-demi": "^0.12.1" + }, + "dependencies": { + "vue-demi": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.1.tgz", + "integrity": "sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==", + "requires": {} + } + } + }, "vue-eslint-parser": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.0.1.tgz", @@ -16364,6 +16481,21 @@ "version": "21.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==" + }, + "zrender": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.2.1.tgz", + "integrity": "sha512-M3bPGZuyLTNBC6LiNKXJwSCtglMp8XUEqEBG+2MdICDI3d1s500Y4P0CzldQGsqpRVB7fkvf3BKQQRxsEaTlsw==", + "requires": { + "tslib": "2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } } } } diff --git a/package.json b/package.json index f0212fa..6a0d9af 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,14 @@ "@tailwindcss/typography": "^0.5.0", "@vue/compiler-sfc": "^3.2.26", "autoprefixer": "^10.4.2", + "echarts": "^5.2.2", "flatpickr": "^4.6.9", "laravel-mix": "^6.0.6", "lodash": "^4.17.21", "postcss": "^8.4.5", "tailwindcss": "^3.0.13", "vue": "^3.2.26", + "vue-echarts": "^6.0.2", "vue-flatpickr-component": "^9.0.5", "vue-loader": "^17.0.0" }, diff --git a/resources/css/app.css b/resources/css/app.css index 0bee166..350af46 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -47,3 +47,24 @@ -webkit-box-shadow: -5px 0 0 #527ABA, 5px 0 0 #527ABA; box-shadow: -5px 0 0 #527ABA, 5px 0 0 #527ABA; } + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + border-radius: 100vh; + background: transparent; +} + +::-webkit-scrollbar-thumb { + border-radius: 8px; + background: #dadce0; + border: 4px solid transparent; +} + +::-webkit-scrollbar-thumb:hover { + background: #dadce0; +} + diff --git a/resources/js/Pages/VacationRequest/Create.vue b/resources/js/Pages/VacationRequest/Create.vue new file mode 100644 index 0000000..751566a --- /dev/null +++ b/resources/js/Pages/VacationRequest/Create.vue @@ -0,0 +1,238 @@ +