* #28 - holidays management * #28 - fix * #28 - fix * #28 - fix * #28 - fix * #28 - fix * #28 - fix * #28 - cr fix
This commit is contained in:
parent
6854c7a9f8
commit
026bfe485f
34
app/Helpers/PolishHolidaysRetriever.php
Normal file
34
app/Helpers/PolishHolidaysRetriever.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Helpers;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Models\YearPeriod;
|
||||
use Yasumi\Holiday;
|
||||
use Yasumi\Yasumi;
|
||||
|
||||
class PolishHolidaysRetriever
|
||||
{
|
||||
protected const PROVIDER_KEY = "Poland";
|
||||
protected const LANG_KEY = "pl";
|
||||
|
||||
public function getForYearPeriod(YearPeriod $yearPeriod): Collection
|
||||
{
|
||||
$polishProvider = Yasumi::create(static::PROVIDER_KEY, $yearPeriod->year);
|
||||
|
||||
$holidays = $polishProvider->getHolidays();
|
||||
|
||||
return $this->prepareHolidays($holidays);
|
||||
}
|
||||
|
||||
protected function prepareHolidays(array $holidays): Collection
|
||||
{
|
||||
return collect($holidays)->map(fn(Holiday $holiday) => [
|
||||
"name" => $holiday->getName([static::LANG_KEY]),
|
||||
"date" => Carbon::createFromTimestamp($holiday->getTimestamp()),
|
||||
])->values();
|
||||
}
|
||||
}
|
65
app/Http/Controllers/HolidayController.php
Normal file
65
app/Http/Controllers/HolidayController.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Response;
|
||||
use Toby\Http\Requests\HolidayRequest;
|
||||
use Toby\Http\Resources\HolidayFormDataResource;
|
||||
use Toby\Http\Resources\HolidayResource;
|
||||
use Toby\Models\Holiday;
|
||||
|
||||
class HolidayController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
$holidays = Holiday::query()
|
||||
->orderBy("date")
|
||||
->get();
|
||||
|
||||
return inertia("Holidays/Index", [
|
||||
"holidays" => HolidayResource::collection($holidays),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
return inertia("Holidays/Create");
|
||||
}
|
||||
|
||||
public function store(HolidayRequest $request): RedirectResponse
|
||||
{
|
||||
Holiday::query()->create($request->data());
|
||||
|
||||
return redirect()
|
||||
->route("holidays.index")
|
||||
->with("success", __("Holiday has been created"));
|
||||
}
|
||||
|
||||
public function edit(Holiday $holiday): Response
|
||||
{
|
||||
return inertia("Holidays/Edit", [
|
||||
"holiday" => new HolidayFormDataResource($holiday),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(HolidayRequest $request, Holiday $holiday): RedirectResponse
|
||||
{
|
||||
$holiday->update($request->data());
|
||||
|
||||
return redirect()
|
||||
->route("holidays.index")
|
||||
->with("success", __("Holiday has been updated"));
|
||||
}
|
||||
|
||||
public function destroy(Holiday $holiday): RedirectResponse
|
||||
{
|
||||
$holiday->delete();
|
||||
|
||||
return redirect()
|
||||
->route("holidays.index")
|
||||
->with("success", __("Holiday has been deleted"));
|
||||
}
|
||||
}
|
37
app/Http/Requests/HolidayRequest.php
Normal file
37
app/Http/Requests/HolidayRequest.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Toby\Models\YearPeriod;
|
||||
use Toby\Rules\YearPeriodExists;
|
||||
|
||||
class HolidayRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
"name" => ["required", "min:3", "max:150"],
|
||||
"date" => ["required",
|
||||
"date_format:Y-m-d",
|
||||
Rule::unique("holidays", "date")->ignore($this->holiday),
|
||||
new YearPeriodExists(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function data(): array
|
||||
{
|
||||
$date = $this->get("date");
|
||||
|
||||
return [
|
||||
"name" => $this->get("name"),
|
||||
"date" => $date,
|
||||
"year_period_id" => YearPeriod::findByYear(Carbon::create($date)->year)->id,
|
||||
];
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ class UserRequest extends FormRequest
|
||||
"lastName" => ["required", "min:3", "max:80"],
|
||||
"email" => ["required", "email", Rule::unique("users", "email")->ignore($this->user)],
|
||||
"employmentForm" => ["required", new Enum(EmploymentForm::class)],
|
||||
"employmentDate" => ["required", "date"],
|
||||
"employmentDate" => ["required", "date_format:Y-m-d"],
|
||||
];
|
||||
}
|
||||
|
||||
|
21
app/Http/Resources/HolidayFormDataResource.php
Normal file
21
app/Http/Resources/HolidayFormDataResource.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class HolidayFormDataResource extends JsonResource
|
||||
{
|
||||
public static $wrap = null;
|
||||
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
"id" => $this->id,
|
||||
"name" => $this->name,
|
||||
"date" => $this->date->toDateString(),
|
||||
];
|
||||
}
|
||||
}
|
22
app/Http/Resources/HolidayResource.php
Normal file
22
app/Http/Resources/HolidayResource.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class HolidayResource extends JsonResource
|
||||
{
|
||||
public static $wrap = null;
|
||||
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
"id" => $this->id,
|
||||
"name" => $this->name,
|
||||
"displayDate" => $this->date->toDisplayString(),
|
||||
"dayOfWeek" => $this->date->dayName,
|
||||
];
|
||||
}
|
||||
}
|
32
app/Models/Holiday.php
Normal file
32
app/Models/Holiday.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?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;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property Carbon $date
|
||||
* @property YearPeriod $yearPeriod
|
||||
*/
|
||||
class Holiday extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
"date" => "date",
|
||||
];
|
||||
|
||||
public function yearPeriod(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(YearPeriod::class);
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ class User extends Authenticatable
|
||||
|
||||
protected $casts = [
|
||||
"employment_form" => EmploymentForm::class,
|
||||
"employment_date" => "datetime",
|
||||
"employment_date" => "date",
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
|
@ -4,29 +4,33 @@ declare(strict_types=1);
|
||||
|
||||
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\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $year
|
||||
* @property Collection $vacationLimits
|
||||
* @property Collection $holidays
|
||||
*/
|
||||
class YearPeriod extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
"year",
|
||||
];
|
||||
protected $guarded = [];
|
||||
|
||||
public static function current(): ?static
|
||||
{
|
||||
return static::findByYear(Carbon::now()->year);
|
||||
}
|
||||
|
||||
public static function findByYear(int $year): ?static
|
||||
{
|
||||
/** @var YearPeriod $year */
|
||||
$year = static::query()->where("year", Carbon::now()->year)->first();
|
||||
$year = static::query()->where("year", $year)->first();
|
||||
|
||||
return $year;
|
||||
}
|
||||
@ -35,4 +39,9 @@ class YearPeriod extends Model
|
||||
{
|
||||
return $this->hasMany(VacationLimit::class);
|
||||
}
|
||||
|
||||
public function holidays(): HasMany
|
||||
{
|
||||
return $this->hasMany(Holiday::class);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Toby\Observers;
|
||||
|
||||
use Toby\Helpers\PolishHolidaysRetriever;
|
||||
use Toby\Helpers\UserAvatarGenerator;
|
||||
use Toby\Models\User;
|
||||
use Toby\Models\YearPeriod;
|
||||
@ -12,10 +13,17 @@ class YearPeriodObserver
|
||||
{
|
||||
public function __construct(
|
||||
protected UserAvatarGenerator $generator,
|
||||
protected PolishHolidaysRetriever $polishHolidaysRetriever,
|
||||
) {
|
||||
}
|
||||
|
||||
public function created(YearPeriod $yearPeriod): void
|
||||
{
|
||||
$this->createVacationLimitsFor($yearPeriod);
|
||||
$this->createHolidaysFor($yearPeriod);
|
||||
}
|
||||
|
||||
protected function createVacationLimitsFor(YearPeriod $yearPeriod): void
|
||||
{
|
||||
$users = User::all();
|
||||
|
||||
@ -25,4 +33,16 @@ class YearPeriodObserver
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function createHolidaysFor(YearPeriod $yearPeriod): void
|
||||
{
|
||||
$holidays = $this->polishHolidaysRetriever->getForYearPeriod($yearPeriod);
|
||||
|
||||
foreach ($holidays as $holiday) {
|
||||
$yearPeriod->holidays()->create([
|
||||
"name" => $holiday["name"],
|
||||
"date" => $holiday["date"],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ namespace Toby\Providers;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Toby\Models\Holiday;
|
||||
use Toby\Models\VacationLimit;
|
||||
use Toby\Scopes\SelectedYearPeriodScope;
|
||||
|
||||
@ -15,6 +16,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y"));
|
||||
|
||||
VacationLimit::addGlobalScope($this->app->make(SelectedYearPeriodScope::class));
|
||||
$selectedYearPeriodScope = $this->app->make(SelectedYearPeriodScope::class);
|
||||
|
||||
VacationLimit::addGlobalScope($selectedYearPeriodScope);
|
||||
Holiday::addGlobalScope($selectedYearPeriodScope);
|
||||
}
|
||||
}
|
||||
|
24
app/Rules/YearPeriodExists.php
Normal file
24
app/Rules/YearPeriodExists.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Toby\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Toby\Models\YearPeriod;
|
||||
|
||||
class YearPeriodExists implements Rule
|
||||
{
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$yearPeriod = YearPeriod::findByYear(Carbon::create($value)->year);
|
||||
|
||||
return $yearPeriod !== null;
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
{
|
||||
return "The year period for given year doesn't exist.";
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-pdo": "*",
|
||||
"azuyalabs/yasumi": "^2.4",
|
||||
"fruitcake/laravel-cors": "^2.0",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"inertiajs/inertia-laravel": "^0.5.1",
|
||||
|
75
composer.lock
generated
75
composer.lock
generated
@ -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": "3412dd2a403927b829237ae4db36351a",
|
||||
"content-hash": "e3c6ffae4c01db02d0471c52d2370b79",
|
||||
"packages": [
|
||||
{
|
||||
"name": "asm89/stack-cors",
|
||||
@ -62,6 +62,79 @@
|
||||
},
|
||||
"time": "2022-01-18T09:12:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "azuyalabs/yasumi",
|
||||
"version": "2.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/azuyalabs/yasumi.git",
|
||||
"reference": "083a0d0579fee17e68d688d463bc01098ac2691f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/azuyalabs/yasumi/zipball/083a0d0579fee17e68d688d463bc01098ac2691f",
|
||||
"reference": "083a0d0579fee17e68d688d463bc01098ac2691f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": ">=7.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.16",
|
||||
"infection/infection": "^0.17 | ^0.22",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"phan/phan": "^4.0",
|
||||
"phpstan/phpstan": "^0.12.66",
|
||||
"phpunit/phpunit": "^8.5 | ^9.4",
|
||||
"vimeo/psalm": "^4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-calendar": "For calculating the date of Easter"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Yasumi\\": "src/Yasumi/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sacha Telgenhof",
|
||||
"email": "me@sachatelgenhof.com",
|
||||
"role": "Maintainer"
|
||||
}
|
||||
],
|
||||
"description": "The easy PHP Library for calculating holidays.",
|
||||
"homepage": "https://www.yasumi.dev",
|
||||
"keywords": [
|
||||
"Bank",
|
||||
"calculation",
|
||||
"calendar",
|
||||
"celebration",
|
||||
"date",
|
||||
"holiday",
|
||||
"holidays",
|
||||
"national",
|
||||
"time"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://www.yasumi.dev",
|
||||
"issues": "https://github.com/azuyalabs/yasumi/issues",
|
||||
"source": "https://github.com/azuyalabs/yasumi"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.buymeacoffee.com/sachatelgenhof",
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2021-05-09T09:03:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.9.3",
|
||||
|
20
database/factories/HolidayFactory.php
Normal file
20
database/factories/HolidayFactory.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Toby\Models\YearPeriod;
|
||||
|
||||
class HolidayFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
"name" => $this->faker->word,
|
||||
"date" => $this->faker->unique->date,
|
||||
"year_period_id" => YearPeriod::current()->id,
|
||||
];
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Toby\Enums\EmploymentForm;
|
||||
|
||||
@ -17,7 +18,7 @@ class UserFactory extends Factory
|
||||
"last_name" => $this->faker->lastName(),
|
||||
"email" => $this->faker->unique()->safeEmail(),
|
||||
"employment_form" => $this->faker->randomElement(EmploymentForm::cases()),
|
||||
"employment_date" => $this->faker->dateTimeBetween("2020-10-27"),
|
||||
"employment_date" => Carbon::createFromInterface($this->faker->dateTimeBetween("2020-10-27"))->toDateString(),
|
||||
"remember_token" => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class YearPeriodFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
"year" => $this->faker->unique()->year,
|
||||
"year" => (int)$this->faker->unique()->year,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ return new class() extends Migration {
|
||||
$table->string("email")->unique();
|
||||
$table->string("avatar")->nullable();
|
||||
$table->string("employment_form");
|
||||
$table->dateTime("employment_date");
|
||||
$table->date("employment_date");
|
||||
$table->rememberToken();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Toby\Models\YearPeriod;
|
||||
|
||||
return new class() extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create("holidays", function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignIdFor(YearPeriod::class)->constrained()->cascadeOnDelete();
|
||||
$table->string("name");
|
||||
$table->date("date")->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists("holidays");
|
||||
}
|
||||
};
|
@ -7,6 +7,7 @@ namespace Database\Seeders;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Toby\Helpers\PolishHolidaysRetriever;
|
||||
use Toby\Helpers\UserAvatarGenerator;
|
||||
use Toby\Models\User;
|
||||
use Toby\Models\VacationLimit;
|
||||
@ -54,6 +55,16 @@ class DatabaseSeeder extends Seeder
|
||||
->create();
|
||||
}
|
||||
})
|
||||
->afterCreating(function (YearPeriod $yearPeriod): void {
|
||||
$polishHolidaysRetriever = new PolishHolidaysRetriever();
|
||||
|
||||
foreach ($polishHolidaysRetriever->getForYearPeriod($yearPeriod) as $holiday) {
|
||||
$yearPeriod->holidays()->create([
|
||||
"name" => $holiday["name"],
|
||||
"date" => $holiday["date"],
|
||||
]);
|
||||
}
|
||||
})
|
||||
->create();
|
||||
}
|
||||
|
||||
|
106
resources/js/Pages/Holidays/Create.vue
Normal file
106
resources/js/Pages/Holidays/Create.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<InertiaHead title="Dodaj dzień wolny" />
|
||||
<div class="bg-white sm:rounded-lg shadow-md">
|
||||
<div class="p-4 sm:px-6">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||
Dodaj dzień wolny
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Użytkownik nie będzie miał możliwości wzięcia urlopu w dzień wolny.
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
class="border-t border-gray-200 px-6"
|
||||
@submit.prevent="createHoliday"
|
||||
>
|
||||
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
|
||||
<label
|
||||
for="name"
|
||||
class="block text-sm font-medium text-gray-700 sm:mt-px"
|
||||
>
|
||||
Nazwa
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="block w-full max-w-lg shadow-sm rounded-md sm:text-sm"
|
||||
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.name, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.name }"
|
||||
>
|
||||
<p
|
||||
v-if="form.errors.name"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
>
|
||||
{{ form.errors.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
|
||||
<label
|
||||
for="date"
|
||||
class="block text-sm font-medium text-gray-700 sm:mt-px"
|
||||
>
|
||||
Data
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<FlatPickr
|
||||
id="date"
|
||||
v-model="form.date"
|
||||
placeholder="Wybierz datę"
|
||||
class="block w-full max-w-lg shadow-sm rounded-md sm:text-sm"
|
||||
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.date, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.date }"
|
||||
/>
|
||||
<p
|
||||
v-if="form.errors.date"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
>
|
||||
{{ form.errors.date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end py-3">
|
||||
<div class="space-x-3">
|
||||
<InertiaLink
|
||||
href="/holidays"
|
||||
class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500"
|
||||
>
|
||||
Anuluj
|
||||
</InertiaLink>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useForm } from '@inertiajs/inertia-vue3';
|
||||
import FlatPickr from 'vue-flatpickr-component';
|
||||
|
||||
export default {
|
||||
name: 'HolidayCreate',
|
||||
components: {
|
||||
FlatPickr,
|
||||
},
|
||||
setup() {
|
||||
const form = useForm({
|
||||
name: null,
|
||||
date: null,
|
||||
});
|
||||
|
||||
return { form };
|
||||
},
|
||||
methods: {
|
||||
createHoliday() {
|
||||
this.form.post('/holidays');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
113
resources/js/Pages/Holidays/Edit.vue
Normal file
113
resources/js/Pages/Holidays/Edit.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<InertiaHead title="Edytuj dzień wolny" />
|
||||
<div class="bg-white sm:rounded-lg shadow-md">
|
||||
<div class="p-4 sm:px-6">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900">
|
||||
Edytuj dzień wolny
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Użytkownik nie będzie miał możliwości wzięcia urlopu w dzień wolny.
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
class="border-t border-gray-200 px-6"
|
||||
@submit.prevent="editHoliday"
|
||||
>
|
||||
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
|
||||
<label
|
||||
for="name"
|
||||
class="block text-sm font-medium text-gray-700 sm:mt-px"
|
||||
>
|
||||
Nazwa
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="block w-full max-w-lg shadow-sm rounded-md sm:text-sm"
|
||||
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.name, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.name }"
|
||||
>
|
||||
<p
|
||||
v-if="form.errors.name"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
>
|
||||
{{ form.errors.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
|
||||
<label
|
||||
for="date"
|
||||
class="block text-sm font-medium text-gray-700 sm:mt-px"
|
||||
>
|
||||
Data
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<FlatPickr
|
||||
id="date"
|
||||
v-model="form.date"
|
||||
placeholder="Wybierz datę"
|
||||
class="block w-full max-w-lg shadow-sm rounded-md sm:text-sm"
|
||||
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.date, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.date }"
|
||||
/>
|
||||
<p
|
||||
v-if="form.errors.date"
|
||||
class="mt-2 text-sm text-red-600"
|
||||
>
|
||||
{{ form.errors.date }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end py-3">
|
||||
<div class="space-x-3">
|
||||
<InertiaLink
|
||||
href="/holidays"
|
||||
class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500"
|
||||
>
|
||||
Anuluj
|
||||
</InertiaLink>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useForm } from '@inertiajs/inertia-vue3';
|
||||
import FlatPickr from 'vue-flatpickr-component';
|
||||
|
||||
export default {
|
||||
name: 'HolidayEdit',
|
||||
components: {
|
||||
FlatPickr,
|
||||
},
|
||||
props: {
|
||||
holiday: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const form = useForm({
|
||||
name: props.holiday.name,
|
||||
date: props.holiday.date,
|
||||
});
|
||||
|
||||
return { form };
|
||||
},
|
||||
methods: {
|
||||
editHoliday() {
|
||||
this.form
|
||||
.put(`/holidays/${this.holiday.id}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
167
resources/js/Pages/Holidays/Index.vue
Normal file
167
resources/js/Pages/Holidays/Index.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<InertiaHead title="Dni wolne od pracy" />
|
||||
<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">
|
||||
Dni wolne od pracy
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Lista dni wolnych od pracy w danym roku
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<InertiaLink
|
||||
href="holidays/create"
|
||||
class="inline-flex items-center px-4 py-3 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blumilk-600 hover:bg-blumilk-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500"
|
||||
>
|
||||
Dodaj dzień wolny
|
||||
</InertiaLink>
|
||||
</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">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Nazwa
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Data
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Dzień tygodnia
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
<tr
|
||||
v-for="holiday in holidays.data"
|
||||
:key="holiday.id"
|
||||
class="hover:bg-blumilk-25"
|
||||
>
|
||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 font-semibold capitalize">
|
||||
{{ holiday.name }}
|
||||
</td>
|
||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ holiday.displayDate }}
|
||||
</td>
|
||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ holiday.dayOfWeek }}
|
||||
</td>
|
||||
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 text-right">
|
||||
<Menu
|
||||
as="div"
|
||||
class="relative inline-block text-left"
|
||||
>
|
||||
<MenuButton class="rounded-full flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-blumilk-500">
|
||||
<DotsVerticalIcon
|
||||
class="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
<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-56 z-10 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div class="py-1">
|
||||
<MenuItem
|
||||
v-slot="{ active }"
|
||||
class="flex"
|
||||
>
|
||||
<InertiaLink
|
||||
:href="`/holidays/${holiday.id}/edit`"
|
||||
:class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'font-medium block px-4 py-2 text-sm']"
|
||||
>
|
||||
<PencilIcon
|
||||
class="mr-2 h-5 w-5 text-blue-500"
|
||||
aria-hidden="true"
|
||||
/> Edytuj
|
||||
</InertiaLink>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-slot="{ active }"
|
||||
class="flex"
|
||||
>
|
||||
<InertiaLink
|
||||
as="button"
|
||||
method="delete"
|
||||
:preserve-scroll="true"
|
||||
:href="`/holidays/${holiday.id}`"
|
||||
:class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'block w-full text-left font-medium px-4 py-2 text-sm']"
|
||||
>
|
||||
<TrashIcon
|
||||
class="mr-2 h-5 w-5 text-red-500"
|
||||
aria-hidden="true"
|
||||
/> Usuń
|
||||
</InertiaLink>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="!holidays.data.length"
|
||||
>
|
||||
<td
|
||||
colspan="100%"
|
||||
class="text-center py-4 text-xl leading-5 text-gray-700"
|
||||
>
|
||||
Brak danych
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DotsVerticalIcon, PencilIcon, TrashIcon } from '@heroicons/vue/solid';
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
|
||||
|
||||
export default {
|
||||
name: 'HolidayINdex',
|
||||
components: {
|
||||
DotsVerticalIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuItems,
|
||||
},
|
||||
props: {
|
||||
holidays: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
@ -211,7 +211,7 @@ export default {
|
||||
lastName: null,
|
||||
email: null,
|
||||
employmentForm: props.employmentForms[0],
|
||||
employmentDate: new Date(),
|
||||
employmentDate: null,
|
||||
});
|
||||
|
||||
return { form };
|
||||
|
@ -215,7 +215,7 @@ export default {
|
||||
lastName: props.user.lastName,
|
||||
email: props.user.email,
|
||||
employmentForm: props.employmentForms.find(form => form.value === props.user.employmentForm),
|
||||
employmentDate: new Date(props.user.employmentDate),
|
||||
employmentDate: props.user.employmentDate,
|
||||
});
|
||||
|
||||
return { form };
|
||||
|
@ -322,8 +322,7 @@ export default {
|
||||
{name: 'Strona główna', href: '/', current: true},
|
||||
{name: 'Użytkownicy', href: '/users', current: false},
|
||||
{name: 'Dostępne urlopy', href: '/vacation-limits', current: false},
|
||||
{name: 'Company Directory', href: '#', current: false},
|
||||
{name: 'Openings', href: '#', current: false},
|
||||
{name: 'Dni wolne', href: '/holidays', current: false},
|
||||
];
|
||||
const userNavigation = [
|
||||
{name: 'Your Profile', href: '#'},
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Toby\Http\Controllers\GoogleController;
|
||||
use Toby\Http\Controllers\HolidayController;
|
||||
use Toby\Http\Controllers\LogoutController;
|
||||
use Toby\Http\Controllers\SelectYearPeriodController;
|
||||
use Toby\Http\Controllers\UserController;
|
||||
@ -14,7 +15,9 @@ Route::middleware("auth")->group(function (): void {
|
||||
Route::post("/logout", LogoutController::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::resource("holidays", HolidayController::class);
|
||||
|
||||
Route::get("/vacation-limits", [VacationLimitController::class, "edit"])->name("vacation.limits");
|
||||
Route::put("/vacation-limits", [VacationLimitController::class, "update"]);
|
||||
|
127
tests/Feature/HolidayTest.php
Normal file
127
tests/Feature/HolidayTest.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\FeatureTestCase;
|
||||
use Toby\Models\Holiday;
|
||||
use Toby\Models\User;
|
||||
use Toby\Models\YearPeriod;
|
||||
|
||||
class HolidayTest extends FeatureTestCase
|
||||
{
|
||||
use DatabaseMigrations;
|
||||
|
||||
public function testUserCanSeeHolidayList(): void
|
||||
{
|
||||
Holiday::factory()->count(10)->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->assertDatabaseCount("holidays", 10);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get("/holidays")
|
||||
->assertInertia(
|
||||
fn(Assert $page) => $page
|
||||
->component("Holidays/Index")
|
||||
->has("holidays.data", 10),
|
||||
);
|
||||
}
|
||||
|
||||
public function testAdminCanCreateHoliday(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$currentYearPeriod = YearPeriod::current();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post("/holidays", [
|
||||
"name" => "Holiday 1",
|
||||
"date" => Carbon::create($currentYearPeriod->year, 5, 20)->toDateString(),
|
||||
])
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas("holidays", [
|
||||
"name" => "Holiday 1",
|
||||
"date" => Carbon::create($currentYearPeriod->year, 5, 20),
|
||||
"year_period_id" => YearPeriod::current()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testAdminCannotCreateHolidayForYearPeriodThatDoesntExist(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$year = YearPeriod::query()->max("year") + 1;
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post("/holidays", [
|
||||
"name" => "Holiday 1",
|
||||
"date" => Carbon::create($year, 5, 20)->toDateString(),
|
||||
])
|
||||
->assertSessionHasErrors(["date"]);
|
||||
}
|
||||
|
||||
public function testAdminCannotCreateHolidayIfGivenDataIsUsed(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$currentYearPeriod = YearPeriod::current();
|
||||
$sameDate = Carbon::create($currentYearPeriod->year, 5, 20)->toDateString();
|
||||
|
||||
Holiday::factory()->create([
|
||||
"name" => "Holiday",
|
||||
"date" => $sameDate,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post("/holidays", [
|
||||
"name" => "Holiday 1",
|
||||
"date" => $sameDate,
|
||||
])
|
||||
->assertSessionHasErrors(["date"]);
|
||||
}
|
||||
|
||||
public function testAdminCanEditHoliday(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$currentYearPeriod = YearPeriod::current();
|
||||
|
||||
$holiday = Holiday::factory()->create([
|
||||
"name" => "Name to change",
|
||||
"date" => Carbon::create($currentYearPeriod->year, 5, 20),
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas("holidays", [
|
||||
"name" => $holiday->name,
|
||||
"date" => $holiday->date->toDateString(),
|
||||
"year_period_id" => $currentYearPeriod->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->put("/holidays/{$holiday->id}", [
|
||||
"name" => "Holiday 1",
|
||||
"date" => Carbon::create($currentYearPeriod->year, 10, 25)->toDateString(),
|
||||
])
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas("holidays", [
|
||||
"name" => "Holiday 1",
|
||||
"date" => Carbon::create($currentYearPeriod->year, 10, 25)->toDateString(),
|
||||
"year_period_id" => $currentYearPeriod->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testAdminCanDeleteHoliday(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$holiday = Holiday::factory()->create();
|
||||
|
||||
$this->actingAs($admin)
|
||||
->delete("/holidays/{$holiday->id}")
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
$this->assertDeleted($holiday);
|
||||
}
|
||||
}
|
@ -89,7 +89,7 @@ class UserTest extends FeatureTestCase
|
||||
"lastName" => "Doe",
|
||||
"email" => "john.doe@example.com",
|
||||
"employmentForm" => EmploymentForm::B2B_CONTRACT->value,
|
||||
"employmentDate" => Carbon::now()->toDateTimeString(),
|
||||
"employmentDate" => Carbon::now()->toDateString(),
|
||||
])
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
@ -98,7 +98,7 @@ class UserTest extends FeatureTestCase
|
||||
"last_name" => "Doe",
|
||||
"email" => "john.doe@example.com",
|
||||
"employment_form" => EmploymentForm::B2B_CONTRACT->value,
|
||||
"employment_date" => Carbon::now()->toDateTimeString(),
|
||||
"employment_date" => Carbon::now()->toDateString(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -114,7 +114,7 @@ class UserTest extends FeatureTestCase
|
||||
"last_name" => $user->last_name,
|
||||
"email" => $user->email,
|
||||
"employment_form" => $user->employment_form->value,
|
||||
"employment_date" => $user->employment_date->toDateTimeString(),
|
||||
"employment_date" => $user->employment_date->toDateString(),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
@ -123,7 +123,7 @@ class UserTest extends FeatureTestCase
|
||||
"lastName" => "Doe",
|
||||
"email" => "john.doe@example.com",
|
||||
"employmentForm" => EmploymentForm::B2B_CONTRACT->value,
|
||||
"employmentDate" => Carbon::now()->toDateTimeString(),
|
||||
"employmentDate" => Carbon::now()->toDateString(),
|
||||
])
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
@ -132,7 +132,7 @@ class UserTest extends FeatureTestCase
|
||||
"last_name" => "Doe",
|
||||
"email" => "john.doe@example.com",
|
||||
"employment_form" => EmploymentForm::B2B_CONTRACT->value,
|
||||
"employment_date" => Carbon::now()->toDateTimeString(),
|
||||
"employment_date" => Carbon::now()->toDateString(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ trait InteractsWithYearPeriods
|
||||
public function createYearPeriod(int $year): YearPeriod
|
||||
{
|
||||
/** @var YearPeriod $yearPeriod */
|
||||
$yearPeriod = YearPeriod::factory()->create([
|
||||
$yearPeriod = YearPeriod::factory()->createQuietly([
|
||||
"year" => $year,
|
||||
]);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user