From e147d24365eb181f1f02269162bd7bb4802dc78c Mon Sep 17 00:00:00 2001 From: Adrian Hopek Date: Mon, 24 Jan 2022 11:28:00 +0100 Subject: [PATCH] #23 - collective editing vacation days (#26) * #23 - wip * #23 - wip * #23 - wip * #23 - wip * #23 - fix * #23 - ecs fix * #23 - fix * #23 - fix * #23 - cr fix --- app/Helpers/YearPeriodRetriever.php | 57 ++++++ .../SelectYearPeriodController.php | 22 +++ .../Controllers/VacationLimitController.php | 34 ++++ app/Http/Middleware/HandleInertiaRequests.php | 7 + app/Http/Requests/VacationLimitRequest.php | 33 ++++ app/Http/Resources/UserFormDataResource.php | 2 +- app/Http/Resources/UserResource.php | 2 +- app/Http/Resources/VacationLimitResource.php | 22 +++ app/Models/User.php | 15 ++ app/Models/VacationLimit.php | 37 ++++ app/Models/YearPeriod.php | 8 + app/Observers/UserObserver.php | 8 +- app/Observers/YearPeriodObserver.php | 28 +++ app/Providers/AppServiceProvider.php | 8 +- app/Providers/ObserverServiceProvider.php | 20 ++ app/Scopes/SelectedYearPeriodScope.php | 23 +++ config/app.php | 1 + database/factories/VacationLimitFactory.php | 24 +++ ...19_140630_create_vacation_limits_table.php | 27 +++ database/seeders/DatabaseSeeder.php | 52 +++++- package-lock.json | 2 +- resources/js/Pages/VacationLimits.vue | 175 ++++++++++++++++++ resources/js/Shared/MainMenu.vue | 55 +++++- routes/web.php | 7 + tests/Feature/AuthenticationTest.php | 4 +- tests/Feature/InertiaTest.php | 4 +- tests/Feature/SelectYearPeriodTest.php | 53 ++++++ tests/Feature/UserTest.php | 12 +- tests/Feature/VacationLimitTest.php | 77 ++++++++ tests/FeatureTestCase.php | 23 +++ tests/Traits/InteractsWithYearPeriods.php | 49 +++++ tests/Unit/AvatarTest.php | 3 + tests/Unit/CheckYearPeriodTest.php | 15 +- tests/Unit/VacationLimitTest.php | 50 +++++ tests/Unit/YearPeriodRetrieverTest.php | 82 ++++++++ 35 files changed, 1000 insertions(+), 41 deletions(-) create mode 100644 app/Helpers/YearPeriodRetriever.php create mode 100644 app/Http/Controllers/SelectYearPeriodController.php create mode 100644 app/Http/Controllers/VacationLimitController.php create mode 100644 app/Http/Requests/VacationLimitRequest.php create mode 100644 app/Http/Resources/VacationLimitResource.php create mode 100644 app/Models/VacationLimit.php create mode 100644 app/Observers/YearPeriodObserver.php create mode 100644 app/Providers/ObserverServiceProvider.php create mode 100644 app/Scopes/SelectedYearPeriodScope.php create mode 100644 database/factories/VacationLimitFactory.php create mode 100644 database/migrations/2022_01_19_140630_create_vacation_limits_table.php create mode 100644 resources/js/Pages/VacationLimits.vue create mode 100644 tests/Feature/SelectYearPeriodTest.php create mode 100644 tests/Feature/VacationLimitTest.php create mode 100644 tests/FeatureTestCase.php create mode 100644 tests/Traits/InteractsWithYearPeriods.php create mode 100644 tests/Unit/VacationLimitTest.php create mode 100644 tests/Unit/YearPeriodRetrieverTest.php diff --git a/app/Helpers/YearPeriodRetriever.php b/app/Helpers/YearPeriodRetriever.php new file mode 100644 index 0000000..b474865 --- /dev/null +++ b/app/Helpers/YearPeriodRetriever.php @@ -0,0 +1,57 @@ +find($this->session->get(static::SESSION_KEY)); + + return $yearPeriod !== null ? $yearPeriod : $this->current(); + } + + public function current(): YearPeriod + { + return YearPeriod::current(); + } + + public function links(): array + { + $current = $this->selected(); + + $years = YearPeriod::query()->whereIn("year", $this->offset($current->year))->get(); + $navigation = $years->map(fn(YearPeriod $yearPeriod) => $this->toNavigation($yearPeriod)); + + return [ + "current" => $current->year, + "navigation" => $navigation->toArray(), + ]; + } + + protected function offset(int $year): array + { + return range($year - 2, $year + 2); + } + + protected function toNavigation(YearPeriod $yearPeriod): array + { + return [ + "year" => $yearPeriod->year, + "link" => route("year-periods.select", $yearPeriod->id), + ]; + } +} diff --git a/app/Http/Controllers/SelectYearPeriodController.php b/app/Http/Controllers/SelectYearPeriodController.php new file mode 100644 index 0000000..4965ef3 --- /dev/null +++ b/app/Http/Controllers/SelectYearPeriodController.php @@ -0,0 +1,22 @@ +session()->put(YearPeriodRetriever::SESSION_KEY, $yearPeriod->id); + + return redirect() + ->back() + ->with("success", __("Selected year period has been changed")); + } +} diff --git a/app/Http/Controllers/VacationLimitController.php b/app/Http/Controllers/VacationLimitController.php new file mode 100644 index 0000000..3d5e6d3 --- /dev/null +++ b/app/Http/Controllers/VacationLimitController.php @@ -0,0 +1,34 @@ + VacationLimitResource::collection(VacationLimit::query()->with("user")->get()), + ]); + } + + public function update(VacationLimitRequest $request): RedirectResponse + { + $data = $request->data(); + + foreach ($request->vacationLimits() as $limit) { + $limit->update($data[$limit->id]); + } + + return redirect() + ->back() + ->with("success", __("Vacation limits have been updated")); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 48117f8..dd63c51 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -6,10 +6,16 @@ namespace Toby\Http\Middleware; use Illuminate\Http\Request; use Inertia\Middleware; +use Toby\Helpers\YearPeriodRetriever; use Toby\Http\Resources\UserResource; class HandleInertiaRequests extends Middleware { + public function __construct( + protected YearPeriodRetriever $yearPeriodRetriever, + ) { + } + public function share(Request $request): array { $user = $request->user(); @@ -22,6 +28,7 @@ class HandleInertiaRequests extends Middleware "success" => $request->session()->get("success"), "error" => $request->session()->get("error"), ], + "years" => fn() => $user ? $this->yearPeriodRetriever->links() : [], ]); } } diff --git a/app/Http/Requests/VacationLimitRequest.php b/app/Http/Requests/VacationLimitRequest.php new file mode 100644 index 0000000..fa43bbc --- /dev/null +++ b/app/Http/Requests/VacationLimitRequest.php @@ -0,0 +1,33 @@ + ["required", "array"], + "items.*.id" => ["required", "exists:vacation_limits,id"], + "items.*.days" => ["nullable", "integer", "min:0"], + ]; + } + + public function vacationLimits(): Collection + { + return VacationLimit::query()->find($this->collect("items")->pluck("id")); + } + + public function data(): array + { + return $this->collect("items") + ->keyBy("id") + ->toArray(); + } +} diff --git a/app/Http/Resources/UserFormDataResource.php b/app/Http/Resources/UserFormDataResource.php index df7da1f..1ee44f3 100644 --- a/app/Http/Resources/UserFormDataResource.php +++ b/app/Http/Resources/UserFormDataResource.php @@ -8,7 +8,7 @@ use Illuminate\Http\Resources\Json\JsonResource; class UserFormDataResource extends JsonResource { - public static $wrap = false; + public static $wrap = null; public function toArray($request): array { diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index c9217a6..132092b 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -8,7 +8,7 @@ use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { - public static $wrap = false; + public static $wrap = null; public function toArray($request): array { diff --git a/app/Http/Resources/VacationLimitResource.php b/app/Http/Resources/VacationLimitResource.php new file mode 100644 index 0000000..85692b9 --- /dev/null +++ b/app/Http/Resources/VacationLimitResource.php @@ -0,0 +1,22 @@ + $this->id, + "user" => new UserResource($this->user), + "hasVacation" => $this->hasVacation(), + "days" => $this->days, + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index aa76021..8ab0872 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,10 +6,12 @@ namespace Toby\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; 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 Toby\Enums\EmploymentForm; /** @@ -19,6 +21,7 @@ use Toby\Enums\EmploymentForm; * @property string $avatar * @property EmploymentForm $employment_form * @property Carbon $employment_date + * @property Collection $vacationLimits */ class User extends Authenticatable { @@ -43,6 +46,11 @@ class User extends Authenticatable "remember_token", ]; + public function vacationLimits(): HasMany + { + return $this->hasMany(VacationLimit::class); + } + public function scopeSearch(Builder $query, ?string $text): Builder { if ($text === null) { @@ -53,4 +61,11 @@ class User extends Authenticatable ->where("name", "LIKE", "%{$text}%") ->orWhere("email", "LIKE", "%{$text}%"); } + + public function saveAvatar(string $path): void + { + $this->avatar = $path; + + $this->save(); + } } diff --git a/app/Models/VacationLimit.php b/app/Models/VacationLimit.php new file mode 100644 index 0000000..6e6a361 --- /dev/null +++ b/app/Models/VacationLimit.php @@ -0,0 +1,37 @@ +days !== null; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function yearPeriod(): BelongsTo + { + return $this->belongsTo(YearPeriod::class); + } +} diff --git a/app/Models/YearPeriod.php b/app/Models/YearPeriod.php index bcf2096..569ad03 100644 --- a/app/Models/YearPeriod.php +++ b/app/Models/YearPeriod.php @@ -7,10 +7,13 @@ namespace Toby\Models; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Collection; /** * @property int $id * @property int $year + * @property Collection $vacationLimits */ class YearPeriod extends Model { @@ -27,4 +30,9 @@ class YearPeriod extends Model return $year; } + + public function vacationLimits(): HasMany + { + return $this->hasMany(VacationLimit::class); + } } diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index 32f6f9c..3462cdf 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -6,20 +6,24 @@ namespace Toby\Observers; use Illuminate\Support\Facades\Storage; use Toby\Helpers\UserAvatarGenerator; +use Toby\Helpers\YearPeriodRetriever; use Toby\Models\User; class UserObserver { public function __construct( protected UserAvatarGenerator $generator, + protected YearPeriodRetriever $yearPeriodRetriever, ) { } public function created(User $user): void { - $user->avatar = $this->generator->generateFor($user); + $user->saveAvatar($this->generator->generateFor($user)); - $user->save(); + $user->vacationLimits()->create([ + "year_period_id" => $this->yearPeriodRetriever->current()->id, + ]); } public function updating(User $user): void diff --git a/app/Observers/YearPeriodObserver.php b/app/Observers/YearPeriodObserver.php new file mode 100644 index 0000000..71caa82 --- /dev/null +++ b/app/Observers/YearPeriodObserver.php @@ -0,0 +1,28 @@ +vacationLimits()->create([ + "user_id" => $user->id, + ]); + } + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index db3deaa..cf3fe47 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -6,15 +6,15 @@ namespace Toby\Providers; use Illuminate\Support\Carbon; use Illuminate\Support\ServiceProvider; -use Toby\Models\User; -use Toby\Observers\UserObserver; +use Toby\Models\VacationLimit; +use Toby\Scopes\SelectedYearPeriodScope; class AppServiceProvider extends ServiceProvider { public function boot(): void { - User::observe(UserObserver::class); - Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y")); + + VacationLimit::addGlobalScope($this->app->make(SelectedYearPeriodScope::class)); } } diff --git a/app/Providers/ObserverServiceProvider.php b/app/Providers/ObserverServiceProvider.php new file mode 100644 index 0000000..16badc4 --- /dev/null +++ b/app/Providers/ObserverServiceProvider.php @@ -0,0 +1,20 @@ +where("year_period_id", $this->yearPeriodRetriever->selected()->id); + } +} diff --git a/config/app.php b/config/app.php index 77122a5..3fa85eb 100644 --- a/config/app.php +++ b/config/app.php @@ -42,5 +42,6 @@ return [ Toby\Providers\EventServiceProvider::class, Toby\Providers\RouteServiceProvider::class, Toby\Providers\TelescopeServiceProvider::class, + Toby\Providers\ObserverServiceProvider::class, ], ]; diff --git a/database/factories/VacationLimitFactory.php b/database/factories/VacationLimitFactory.php new file mode 100644 index 0000000..d5f3d6d --- /dev/null +++ b/database/factories/VacationLimitFactory.php @@ -0,0 +1,24 @@ +faker->boolean(75); + + return [ + "user_id" => User::factory(), + "year_period_id" => YearPeriod::factory(), + "has_vacation" => $hasVacation, + "days" => $hasVacation ? $this->faker->numberBetween(20, 26) : null, + ]; + } +} diff --git a/database/migrations/2022_01_19_140630_create_vacation_limits_table.php b/database/migrations/2022_01_19_140630_create_vacation_limits_table.php new file mode 100644 index 0000000..c2b29d5 --- /dev/null +++ b/database/migrations/2022_01_19_140630_create_vacation_limits_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(YearPeriod::class)->constrained()->cascadeOnDelete(); + $table->integer("days")->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("vacation_limits"); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index af2722b..d6a4f27 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -6,23 +6,61 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Carbon; +use Illuminate\Support\Collection; +use Toby\Helpers\UserAvatarGenerator; use Toby\Models\User; +use Toby\Models\VacationLimit; use Toby\Models\YearPeriod; class DatabaseSeeder extends Seeder { + public function __construct( + protected UserAvatarGenerator $avatarGenerator, + ) { + } + public function run(): void { - User::factory(35)->create(); + User::unsetEventDispatcher(); + YearPeriod::unsetEventDispatcher(); + + User::factory(9)->create(); User::factory([ "email" => env("LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE"), ])->create(); - YearPeriod::factory([ - "year" => Carbon::now()->year, - ])->create(); - YearPeriod::factory([ - "year" => Carbon::now()->year + 1, - ])->create(); + $users = User::all(); + + $this->generateAvatarsForUsers($users); + + YearPeriod::factory() + ->count(3) + ->sequence( + [ + "year" => Carbon::now()->year - 1, + ], + [ + "year" => Carbon::now()->year, + ], + [ + "year" => Carbon::now()->year + 1, + ], + ) + ->afterCreating(function (YearPeriod $yearPeriod) use ($users): void { + foreach ($users as $user) { + VacationLimit::factory() + ->for($yearPeriod) + ->for($user) + ->create(); + } + }) + ->create(); + } + + protected function generateAvatarsForUsers(Collection $users): void + { + foreach ($users as $user) { + $user->saveAvatar($this->avatarGenerator->generateFor($user)); + } } } diff --git a/package-lock.json b/package-lock.json index c24807a..7a47b79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "application", + "name": "toby", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/resources/js/Pages/VacationLimits.vue b/resources/js/Pages/VacationLimits.vue new file mode 100644 index 0000000..04afda6 --- /dev/null +++ b/resources/js/Pages/VacationLimits.vue @@ -0,0 +1,175 @@ + + + diff --git a/resources/js/Shared/MainMenu.vue b/resources/js/Shared/MainMenu.vue index 7373e89..4966645 100644 --- a/resources/js/Shared/MainMenu.vue +++ b/resources/js/Shared/MainMenu.vue @@ -19,6 +19,53 @@