#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
This commit is contained in:
Adrian Hopek 2022-01-24 11:28:00 +01:00 committed by GitHub
parent 652587dbf1
commit e147d24365
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1000 additions and 41 deletions

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Toby\Helpers;
use Illuminate\Contracts\Session\Session;
use Toby\Models\YearPeriod;
class YearPeriodRetriever
{
public const SESSION_KEY = "selected_year_period";
public function __construct(
protected Session $session,
) {
}
public function selected(): YearPeriod
{
/** @var YearPeriod $yearPeriod */
$yearPeriod = YearPeriod::query()->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),
];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Toby\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Toby\Helpers\YearPeriodRetriever;
use Toby\Models\YearPeriod;
class SelectYearPeriodController extends Controller
{
public function __invoke(Request $request, YearPeriod $yearPeriod): RedirectResponse
{
$request->session()->put(YearPeriodRetriever::SESSION_KEY, $yearPeriod->id);
return redirect()
->back()
->with("success", __("Selected year period has been changed"));
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Toby\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Inertia\Response;
use Toby\Http\Requests\VacationLimitRequest;
use Toby\Http\Resources\VacationLimitResource;
use Toby\Models\VacationLimit;
class VacationLimitController extends Controller
{
public function edit(): Response
{
return inertia("VacationLimits", [
"limits" => 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"));
}
}

View File

@ -6,10 +6,16 @@ namespace Toby\Http\Middleware;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Middleware; use Inertia\Middleware;
use Toby\Helpers\YearPeriodRetriever;
use Toby\Http\Resources\UserResource; use Toby\Http\Resources\UserResource;
class HandleInertiaRequests extends Middleware class HandleInertiaRequests extends Middleware
{ {
public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever,
) {
}
public function share(Request $request): array public function share(Request $request): array
{ {
$user = $request->user(); $user = $request->user();
@ -22,6 +28,7 @@ class HandleInertiaRequests extends Middleware
"success" => $request->session()->get("success"), "success" => $request->session()->get("success"),
"error" => $request->session()->get("error"), "error" => $request->session()->get("error"),
], ],
"years" => fn() => $user ? $this->yearPeriodRetriever->links() : [],
]); ]);
} }
} }

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Toby\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Collection;
use Toby\Models\VacationLimit;
class VacationLimitRequest extends FormRequest
{
public function rules(): array
{
return [
"items" => ["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();
}
}

View File

@ -8,7 +8,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
class UserFormDataResource extends JsonResource class UserFormDataResource extends JsonResource
{ {
public static $wrap = false; public static $wrap = null;
public function toArray($request): array public function toArray($request): array
{ {

View File

@ -8,7 +8,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource class UserResource extends JsonResource
{ {
public static $wrap = false; public static $wrap = null;
public function toArray($request): array public function toArray($request): array
{ {

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Toby\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class VacationLimitResource extends JsonResource
{
public static $wrap = null;
public function toArray($request): array
{
return [
"id" => $this->id,
"user" => new UserResource($this->user),
"hasVacation" => $this->hasVacation(),
"days" => $this->days,
];
}
}

View File

@ -6,10 +6,12 @@ namespace Toby\Models;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Enums\EmploymentForm; use Toby\Enums\EmploymentForm;
/** /**
@ -19,6 +21,7 @@ use Toby\Enums\EmploymentForm;
* @property string $avatar * @property string $avatar
* @property EmploymentForm $employment_form * @property EmploymentForm $employment_form
* @property Carbon $employment_date * @property Carbon $employment_date
* @property Collection $vacationLimits
*/ */
class User extends Authenticatable class User extends Authenticatable
{ {
@ -43,6 +46,11 @@ class User extends Authenticatable
"remember_token", "remember_token",
]; ];
public function vacationLimits(): HasMany
{
return $this->hasMany(VacationLimit::class);
}
public function scopeSearch(Builder $query, ?string $text): Builder public function scopeSearch(Builder $query, ?string $text): Builder
{ {
if ($text === null) { if ($text === null) {
@ -53,4 +61,11 @@ class User extends Authenticatable
->where("name", "LIKE", "%{$text}%") ->where("name", "LIKE", "%{$text}%")
->orWhere("email", "LIKE", "%{$text}%"); ->orWhere("email", "LIKE", "%{$text}%");
} }
public function saveAvatar(string $path): void
{
$this->avatar = $path;
$this->save();
}
} }

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Toby\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property User $user
* @property YearPeriod $yearPeriod
* @property int $days
*/
class VacationLimit extends Model
{
use HasFactory;
protected $guarded = [];
public function hasVacation(): bool
{
return $this->days !== null;
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function yearPeriod(): BelongsTo
{
return $this->belongsTo(YearPeriod::class);
}
}

View File

@ -7,10 +7,13 @@ namespace Toby\Models;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
/** /**
* @property int $id * @property int $id
* @property int $year * @property int $year
* @property Collection $vacationLimits
*/ */
class YearPeriod extends Model class YearPeriod extends Model
{ {
@ -27,4 +30,9 @@ class YearPeriod extends Model
return $year; return $year;
} }
public function vacationLimits(): HasMany
{
return $this->hasMany(VacationLimit::class);
}
} }

View File

@ -6,20 +6,24 @@ namespace Toby\Observers;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Toby\Helpers\UserAvatarGenerator; use Toby\Helpers\UserAvatarGenerator;
use Toby\Helpers\YearPeriodRetriever;
use Toby\Models\User; use Toby\Models\User;
class UserObserver class UserObserver
{ {
public function __construct( public function __construct(
protected UserAvatarGenerator $generator, protected UserAvatarGenerator $generator,
protected YearPeriodRetriever $yearPeriodRetriever,
) { ) {
} }
public function created(User $user): void 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 public function updating(User $user): void

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Toby\Observers;
use Toby\Helpers\UserAvatarGenerator;
use Toby\Models\User;
use Toby\Models\YearPeriod;
class YearPeriodObserver
{
public function __construct(
protected UserAvatarGenerator $generator,
) {
}
public function created(YearPeriod $yearPeriod): void
{
$users = User::all();
foreach ($users as $user) {
$yearPeriod->vacationLimits()->create([
"user_id" => $user->id,
]);
}
}
}

View File

@ -6,15 +6,15 @@ namespace Toby\Providers;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Toby\Models\User; use Toby\Models\VacationLimit;
use Toby\Observers\UserObserver; use Toby\Scopes\SelectedYearPeriodScope;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
public function boot(): void public function boot(): void
{ {
User::observe(UserObserver::class);
Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y")); Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y"));
VacationLimit::addGlobalScope($this->app->make(SelectedYearPeriodScope::class));
} }
} }

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Providers;
use Illuminate\Support\ServiceProvider;
use Toby\Models\User;
use Toby\Models\YearPeriod;
use Toby\Observers\UserObserver;
use Toby\Observers\YearPeriodObserver;
class ObserverServiceProvider extends ServiceProvider
{
public function boot(): void
{
User::observe(UserObserver::class);
YearPeriod::observe(YearPeriodObserver::class);
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Toby\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Toby\Helpers\YearPeriodRetriever;
class SelectedYearPeriodScope implements Scope
{
public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever,
) {
}
public function apply(Builder $builder, Model $model): Builder
{
return $builder->where("year_period_id", $this->yearPeriodRetriever->selected()->id);
}
}

View File

@ -42,5 +42,6 @@ return [
Toby\Providers\EventServiceProvider::class, Toby\Providers\EventServiceProvider::class,
Toby\Providers\RouteServiceProvider::class, Toby\Providers\RouteServiceProvider::class,
Toby\Providers\TelescopeServiceProvider::class, Toby\Providers\TelescopeServiceProvider::class,
Toby\Providers\ObserverServiceProvider::class,
], ],
]; ];

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Toby\Models\User;
use Toby\Models\YearPeriod;
class VacationLimitFactory extends Factory
{
public function definition(): array
{
$hasVacation = $this->faker->boolean(75);
return [
"user_id" => User::factory(),
"year_period_id" => YearPeriod::factory(),
"has_vacation" => $hasVacation,
"days" => $hasVacation ? $this->faker->numberBetween(20, 26) : null,
];
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Toby\Models\User;
use Toby\Models\YearPeriod;
return new class() extends Migration {
public function up(): void
{
Schema::create("vacation_limits", function (Blueprint $table): void {
$table->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");
}
};

View File

@ -6,23 +6,61 @@ namespace Database\Seeders;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Helpers\UserAvatarGenerator;
use Toby\Models\User; use Toby\Models\User;
use Toby\Models\VacationLimit;
use Toby\Models\YearPeriod; use Toby\Models\YearPeriod;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
public function __construct(
protected UserAvatarGenerator $avatarGenerator,
) {
}
public function run(): void public function run(): void
{ {
User::factory(35)->create(); User::unsetEventDispatcher();
YearPeriod::unsetEventDispatcher();
User::factory(9)->create();
User::factory([ User::factory([
"email" => env("LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE"), "email" => env("LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE"),
])->create(); ])->create();
YearPeriod::factory([ $users = User::all();
$this->generateAvatarsForUsers($users);
YearPeriod::factory()
->count(3)
->sequence(
[
"year" => Carbon::now()->year - 1,
],
[
"year" => Carbon::now()->year, "year" => Carbon::now()->year,
])->create(); ],
YearPeriod::factory([ [
"year" => Carbon::now()->year + 1, "year" => Carbon::now()->year + 1,
])->create(); ],
)
->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));
}
} }
} }

2
package-lock.json generated
View File

@ -1,5 +1,5 @@
{ {
"name": "application", "name": "toby",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@ -0,0 +1,175 @@
<template>
<InertiaHead title="Użytkownicy" />
<div class="bg-white sm:rounded-lg shadow-md">
<div class="flex justify-between items-center p-4 sm:px-6">
<div>
<h2 class="text-lg leading-6 font-medium text-gray-900">
Dostępne dni urlopu dla użytkowników
</h2>
<p class="mt-1 text-sm text-gray-500">
Zarządzaj dostepnymi dniami urlopów dla użytkowników.
</p>
</div>
</div>
<div class="border-t border-gray-200">
<div class="overflow-x-auto xl:overflow-x-visible overflow-y-auto xl:overflow-y-visible">
<form @submit.prevent="submitVacationDays">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Imię i nazwisko
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Forma zatrudnienia
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Posiada urlop?
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Dostępne dni w roku
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
<tr
v-for="(item, index) in form.items"
:key="item.id"
class="hover:bg-blumilk-25"
>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="flex">
<span
class="inline-flex items-center justify-center h-10 w-10 rounded-full"
>
<img
class="h-10 w-10 rounded-full"
:src="item.user.avatar"
alt=""
>
</span>
<div class="ml-3">
<p class="text-sm font-medium break-all text-gray-900">
{{ item.user.name }}
</p>
<p class="text-sm break-all text-gray-500">
{{ item.user.email }}
</p>
</div>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{{ item.user.employmentForm }}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<Switch
v-model="item.hasVacation"
:class="[item.hasVacation ? 'bg-blumilk-500' : 'bg-gray-200', 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500']"
>
<span
:class="[item.hasVacation ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200']"
/>
</Switch>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="mt-1 sm:mt-0 sm:col-span-2">
<input
v-model="item.days"
type="number"
min="0"
class="block w-full shadow-sm rounded-md sm:text-sm disabled:bg-slate-50 disabled:text-slate-500 disabled:border-slate-200 disabled:shadow-none disabled:cursor-not-allowed"
:disabled="!item.hasVacation"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors[`items.${index}.days`], 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors[`items.${index}.days`] }"
>
<p
v-if="form.errors[`items.${index}.days`]"
class="mt-2 text-sm text-red-600"
>
{{ form.errors[`items.${index}.days`] }}
</p>
</div>
</td>
</tr>
<tr
v-if="!form.items.length"
>
<td
colspan="100%"
class="text-center py-4 text-xl leading-5 text-gray-700"
>
Brak danych
</td>
</tr>
</tbody>
</table>
<div class="flex justify-end py-3 px-4">
<button
type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blumilk-600 hover:bg-blumilk-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500"
>
Zapisz
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import {Switch} from '@headlessui/vue';
import {useForm} from '@inertiajs/inertia-vue3';
export default {
name: 'VacationLimits',
components: {
Switch,
},
props: {
limits: {
type: Object,
default: () => null,
},
years: {
type: Object,
default: () => null,
},
},
setup(props) {
const form = useForm({
items: props.limits.data,
});
return {
form,
};
},
methods: {
submitVacationDays() {
this.form
.transform(data => ({
items: data.items.map(item => ({
id: item.id,
days: item.hasVacation ? item.days : null,
})),
}))
.put('/vacation-limits', {
preserveState: (page) => Object.keys(page.props.errors).length,
preserveScroll: true,
});
},
},
};
</script>

View File

@ -19,6 +19,53 @@
<!-- Right section on desktop --> <!-- Right section on desktop -->
<div class="hidden lg:ml-4 lg:flex lg:items-center lg:py-5 lg:pr-0.5"> <div class="hidden lg:ml-4 lg:flex lg:items-center lg:py-5 lg:pr-0.5">
<div class="mr-4">
<Menu
as="div"
class="relative inline-block text-left"
>
<div>
<MenuButton class="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-1 focus:ring-gray-300">
{{ years.current }}
<ChevronDownIcon class="-mr-1 ml-2 h-5 w-5" />
</MenuButton>
</div>
<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems class="origin-top-right absolute right-0 mt-2 w-32 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<div class="py-1">
<MenuItem
v-for="(item, index) in years.navigation"
:key="index"
v-slot="{ active }"
>
<InertiaLink
:href="item.link"
as="button"
method="post"
:preserve-state="false"
:class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'flex w-full px-4 py-2 text-sm']"
>
{{ item.year }}
<CheckIcon
v-if="item.year === years.current"
class="h-5 w-5 text-blumilk-500 ml-2"
/>
</InertiaLink>
</MenuItem>
</div>
</MenuItems>
</transition>
</Menu>
</div>
<button <button
type="button" type="button"
class="flex-shrink-0 p-1 text-cyan-200 rounded-full hover:text-white hover:bg-white hover:bg-opacity-10 focus:outline-none focus:ring-2 focus:ring-white" class="flex-shrink-0 p-1 text-cyan-200 rounded-full hover:text-white hover:bg-white hover:bg-opacity-10 focus:outline-none focus:ring-2 focus:ring-white"
@ -246,6 +293,7 @@ import {
import {BellIcon, MenuIcon, XIcon} from '@heroicons/vue/outline'; import {BellIcon, MenuIcon, XIcon} from '@heroicons/vue/outline';
import {computed} from 'vue'; import {computed} from 'vue';
import {usePage} from '@inertiajs/inertia-vue3'; import {usePage} from '@inertiajs/inertia-vue3';
import {ChevronDownIcon, CheckIcon} from '@heroicons/vue/solid';
export default { export default {
name: 'MainMenu', name: 'MainMenu',
@ -263,13 +311,17 @@ export default {
BellIcon, BellIcon,
MenuIcon, MenuIcon,
XIcon, XIcon,
ChevronDownIcon,
CheckIcon,
}, },
setup() { setup() {
const user = computed(() => usePage().props.value.auth.user); const user = computed(() => usePage().props.value.auth.user);
const years = computed(() => usePage().props.value.years);
const navigation = [ const navigation = [
{name: 'Strona główna', href: '/', current: true}, {name: 'Strona główna', href: '/', current: true},
{name: 'Użytkownicy', href: '/users', current: false}, {name: 'Użytkownicy', href: '/users', current: false},
{name: 'Resources', href: '#', current: false}, {name: 'Dostępne urlopy', href: '/vacation-limits', current: false},
{name: 'Company Directory', href: '#', current: false}, {name: 'Company Directory', href: '#', current: false},
{name: 'Openings', href: '#', current: false}, {name: 'Openings', href: '#', current: false},
]; ];
@ -281,6 +333,7 @@ export default {
return { return {
user, user,
years,
navigation, navigation,
userNavigation, userNavigation,
}; };

View File

@ -5,7 +5,9 @@ declare(strict_types=1);
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Toby\Http\Controllers\GoogleController; use Toby\Http\Controllers\GoogleController;
use Toby\Http\Controllers\LogoutController; use Toby\Http\Controllers\LogoutController;
use Toby\Http\Controllers\SelectYearPeriodController;
use Toby\Http\Controllers\UserController; use Toby\Http\Controllers\UserController;
use Toby\Http\Controllers\VacationLimitController;
Route::middleware("auth")->group(function (): void { Route::middleware("auth")->group(function (): void {
Route::get("/", fn() => inertia("Dashboard"))->name("dashboard"); Route::get("/", fn() => inertia("Dashboard"))->name("dashboard");
@ -13,6 +15,11 @@ Route::middleware("auth")->group(function (): void {
Route::resource("users", UserController::class); Route::resource("users", UserController::class);
Route::post("users/{user}/restore", [UserController::class, "restore"])->withTrashed(); Route::post("users/{user}/restore", [UserController::class, "restore"])->withTrashed();
Route::get("/vacation-limits", [VacationLimitController::class, "edit"])->name("vacation.limits");
Route::put("/vacation-limits", [VacationLimitController::class, "update"]);
Route::post("year-periods/{yearPeriod}/select", SelectYearPeriodController::class)->name("year-periods.select");
}); });
Route::middleware("guest")->group(function (): void { Route::middleware("guest")->group(function (): void {

View File

@ -5,10 +5,10 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\TestCase; use Tests\FeatureTestCase;
use Toby\Models\User; use Toby\Models\User;
class AuthenticationTest extends TestCase class AuthenticationTest extends FeatureTestCase
{ {
use DatabaseMigrations; use DatabaseMigrations;

View File

@ -6,10 +6,10 @@ namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseMigrations;
use Inertia\Testing\AssertableInertia as Assert; use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase; use Tests\FeatureTestCase;
use Toby\Models\User; use Toby\Models\User;
class InertiaTest extends TestCase class InertiaTest extends FeatureTestCase
{ {
use DatabaseMigrations; use DatabaseMigrations;

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Carbon;
use Tests\FeatureTestCase;
use Toby\Helpers\YearPeriodRetriever;
use Toby\Models\User;
class SelectYearPeriodTest extends FeatureTestCase
{
use DatabaseMigrations;
protected YearPeriodRetriever $yearPeriodRetriever;
protected function setUp(): void
{
parent::setUp();
$this->yearPeriodRetriever = $this->app->make(YearPeriodRetriever::class);
}
public function testUserCanSelectNextYearPeriod(): void
{
$nextYearPeriod = $this->createYearPeriod(Carbon::now()->year + 1);
$user = User::factory()->create();
$this->actingAs($user)
->post("/year-periods/{$nextYearPeriod->id}/select")
->assertRedirect();
$this->assertSame($nextYearPeriod->id, $this->yearPeriodRetriever->selected()->id);
}
public function testUserCannotSelectNextYearPeriodIfDoesntExist(): void
{
$user = User::factory()->create();
$this->actingAs($user)
->post("/year-periods/25/select")
->assertNotFound();
}
public function testIfUserDoesntSelectAnyYearPeriodCurrentActsAsSelected(): void
{
$currentYearPeriod = $this->yearPeriodRetriever->current();
$this->assertSame($currentYearPeriod->id, $this->yearPeriodRetriever->selected()->id);
}
}

View File

@ -6,23 +6,15 @@ namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Inertia\Testing\AssertableInertia as Assert; use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase; use Tests\FeatureTestCase;
use Toby\Enums\EmploymentForm; use Toby\Enums\EmploymentForm;
use Toby\Models\User; use Toby\Models\User;
class UserTest extends TestCase class UserTest extends FeatureTestCase
{ {
use DatabaseMigrations; use DatabaseMigrations;
protected function setUp(): void
{
parent::setUp();
Storage::fake();
}
public function testAdminCanSeeUsersList(): void public function testAdminCanSeeUsersList(): void
{ {
User::factory()->count(10)->create(); User::factory()->count(10)->create();

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\FeatureTestCase;
use Toby\Models\User;
use Toby\Models\VacationLimit;
class VacationLimitTest extends FeatureTestCase
{
use DatabaseMigrations;
public function testAdminCanSeeVacationLimits(): void
{
$admin = User::factory()->createQuietly();
User::factory(10)->create();
$this->actingAs($admin)
->get("/vacation-limits")
->assertOk()
->assertInertia(
fn(Assert $page) => $page
->component("VacationLimits")
->has("limits.data", 10),
);
}
public function testAdminCanUpdateVacationLimits(): void
{
$admin = User::factory()->createQuietly();
User::factory(3)->create();
[$limit1, $limit2, $limit3] = VacationLimit::all();
$data = [
[
"id" => $limit1->id,
"days" => 25,
],
[
"id" => $limit2->id,
"days" => null,
],
[
"id" => $limit3->id,
"days" => 20,
],
];
$this->actingAs($admin)
->put("/vacation-limits", [
"items" => $data,
])
->assertRedirect();
$this->assertDatabaseHas("vacation_limits", [
"id" => $limit1->id,
"days" => 25,
]);
$this->assertDatabaseHas("vacation_limits", [
"id" => $limit2->id,
"days" => null,
]);
$this->assertDatabaseHas("vacation_limits", [
"id" => $limit3->id,
"days" => 20,
]);
}
}

23
tests/FeatureTestCase.php Normal file
View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Carbon;
use Tests\Traits\InteractsWithYearPeriods;
abstract class FeatureTestCase extends BaseTestCase
{
use CreatesApplication;
use InteractsWithYearPeriods;
protected function setUp(): void
{
parent::setUp();
Carbon::setTestNow(Carbon::now());
$this->createCurrentYearPeriod();
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Tests\Traits;
use Illuminate\Foundation\Testing\Concerns\InteractsWithSession;
use Illuminate\Support\Carbon;
use Toby\Helpers\YearPeriodRetriever;
use Toby\Models\YearPeriod;
trait InteractsWithYearPeriods
{
use InteractsWithSession;
public function createYearPeriod(int $year): YearPeriod
{
/** @var YearPeriod $yearPeriod */
$yearPeriod = YearPeriod::factory()->create([
"year" => $year,
]);
return $yearPeriod;
}
public function createCurrentYearPeriod(): YearPeriod
{
return $this->createYearPeriod(Carbon::now()->year);
}
public function markYearPeriodAsSelected(YearPeriod $yearPeriod): void
{
$this->session([
YearPeriodRetriever::SESSION_KEY => $yearPeriod->id,
]);
}
public function clearSelectedYearPeriod(): void
{
$this->session([]);
}
public function cleanYearPeriods(): void
{
$this->clearSelectedYearPeriod();
YearPeriod::query()->delete();
}
}

View File

@ -7,16 +7,19 @@ namespace Tests\Unit;
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Tests\TestCase; use Tests\TestCase;
use Tests\Traits\InteractsWithYearPeriods;
use Toby\Models\User; use Toby\Models\User;
class AvatarTest extends TestCase class AvatarTest extends TestCase
{ {
use DatabaseMigrations; use DatabaseMigrations;
use InteractsWithYearPeriods;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->createCurrentYearPeriod();
Storage::fake(); Storage::fake();
} }

View File

@ -7,12 +7,13 @@ namespace Tests\Unit;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Tests\TestCase; use Tests\TestCase;
use Tests\Traits\InteractsWithYearPeriods;
use Toby\Jobs\CheckYearPeriod; use Toby\Jobs\CheckYearPeriod;
use Toby\Models\YearPeriod;
class CheckYearPeriodTest extends TestCase class CheckYearPeriodTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
use InteractsWithYearPeriods;
public function testYearPeriodsAreCreatedWhenDontExist(): void public function testYearPeriodsAreCreatedWhenDontExist(): void
{ {
@ -54,9 +55,7 @@ class CheckYearPeriodTest extends TestCase
$now = Carbon::now(); $now = Carbon::now();
Carbon::setTestNow($now); Carbon::setTestNow($now);
YearPeriod::factory([ $this->createCurrentYearPeriod();
"year" => $now->year,
]);
$this->assertDatabaseMissing("year_periods", [ $this->assertDatabaseMissing("year_periods", [
"year" => $now->year + 1, "year" => $now->year + 1,
@ -74,12 +73,8 @@ class CheckYearPeriodTest extends TestCase
$now = Carbon::now(); $now = Carbon::now();
Carbon::setTestNow($now); Carbon::setTestNow($now);
YearPeriod::factory([ $this->createCurrentYearPeriod();
"year" => $now->year, $this->createYearPeriod($now->year + 1);
])->create();
YearPeriod::factory([
"year" => $now->year + 1,
])->create();
$this->assertDatabaseCount("year_periods", 2); $this->assertDatabaseCount("year_periods", 2);

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\TestCase;
use Tests\Traits\InteractsWithYearPeriods;
use Toby\Models\User;
use Toby\Models\YearPeriod;
class VacationLimitTest extends TestCase
{
use DatabaseMigrations;
use InteractsWithYearPeriods;
protected function setUp(): void
{
parent::setUp();
$this->createCurrentYearPeriod();
}
public function testWhenUserIsCreatedThenVacationLimitIsCreatedForCurrentYearPeriod(): void
{
$this->assertDatabaseCount("vacation_limits", 0);
$currentYearPeriod = YearPeriod::current();
$user = User::factory()->create();
$this->assertDatabaseCount("vacation_limits", 1);
$this->assertDatabaseHas("vacation_limits", [
"user_id" => $user->id,
"year_period_id" => $currentYearPeriod->id,
]);
}
public function testWhenYearPeriodIsCreatedThenVacationLimitsForThisYearPeriodAreCreated(): void
{
$this->assertDatabaseCount("vacation_limits", 0);
User::factory(10)->createQuietly();
YearPeriod::factory()->create();
$this->assertDatabaseCount("vacation_limits", 10);
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use Illuminate\Foundation\Testing\Concerns\InteractsWithSession;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase;
use Tests\Traits\InteractsWithYearPeriods;
use Toby\Helpers\YearPeriodRetriever;
use Toby\Models\YearPeriod;
class YearPeriodRetrieverTest extends TestCase
{
use RefreshDatabase;
use InteractsWithSession;
use InteractsWithYearPeriods;
public Carbon $current;
public YearPeriod $previousYearPeriod;
public YearPeriod $currentYearPeriod;
public YearPeriod $nextYearPeriod;
public YearPeriodRetriever $yearPeriodRetriever;
protected function setUp(): void
{
parent::setUp();
$this->current = Carbon::now();
Carbon::setTestNow($this->current);
$this->yearPeriodRetriever = $this->app->make(YearPeriodRetriever::class);
$this->previousYearPeriod = $this->createYearPeriod($this->current->year - 1);
$this->currentYearPeriod = $this->createCurrentYearPeriod();
$this->nextYearPeriod = $this->createYearPeriod($this->current->year + 1);
}
public function testRetrievesCorrectCurrentYearPeriod(): void
{
$this->assertSame($this->currentYearPeriod->id, $this->yearPeriodRetriever->current()->id);
}
public function testRetrievesCurrentYearPeriodWhenNoneIsSelected(): void
{
$this->clearSelectedYearPeriod();
$this->assertSame($this->currentYearPeriod->id, $this->yearPeriodRetriever->selected()->id);
}
public function testRetrievesCorrectYearPeriodWhenSelected(): void
{
$this->markYearPeriodAsSelected($this->nextYearPeriod);
$this->assertSame($this->nextYearPeriod->id, $this->yearPeriodRetriever->selected()->id);
}
public function testLinks(): void
{
$expected = [
"current" => $this->current->year,
"navigation" => [
[
"year" => $this->previousYearPeriod->year,
"link" => route("year-periods.select", $this->previousYearPeriod),
],
[
"year" => $this->currentYearPeriod->year,
"link" => route("year-periods.select", $this->currentYearPeriod),
],
[
"year" => $this->nextYearPeriod->year,
"link" => route("year-periods.select", $this->nextYearPeriod),
],
],
];
$this->assertSame($expected, $this->yearPeriodRetriever->links());
}
}