#43 - vacation summary for employee (#66)

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* #5 - bump dependencies

* #43 - wip

* #43 - add composer script

* #43 - fix

* #43 - fix

* #43 - wip

* #43 - ecs fix

* #43 - cr fix

* #43 - cr fix

* #43 - fix

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
This commit is contained in:
Adrian Hopek 2022-03-03 09:03:17 +01:00 committed by GitHub
parent d825dd727f
commit 3d9726039c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 3122 additions and 1762 deletions

View File

@ -6,9 +6,6 @@ namespace Toby\Architecture\Providers;
use Illuminate\Support\Carbon;
use Illuminate\Support\ServiceProvider;
use Toby\Eloquent\Models\Holiday;
use Toby\Eloquent\Models\VacationLimit;
use Toby\Eloquent\Scopes\SelectedYearPeriodScope;
class AppServiceProvider extends ServiceProvider
{
@ -16,10 +13,5 @@ class AppServiceProvider extends ServiceProvider
{
Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y"));
Carbon::macro("toDisplayDate", fn() => $this->translatedFormat("d.m.Y"));
$selectedYearPeriodScope = $this->app->make(SelectedYearPeriodScope::class);
VacationLimit::addGlobalScope($selectedYearPeriodScope);
Holiday::addGlobalScope($selectedYearPeriodScope);
}
}

View File

@ -16,8 +16,7 @@ class CalendarGenerator
{
public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever,
) {
}
) {}
public function generate(Carbon $month): array
{

View File

@ -7,7 +7,7 @@ namespace Toby\Domain\Enums;
enum EmploymentForm: string
{
case EmploymentContract = "employment_contract";
case ComissionContract = "commission_contract";
case CommissionContract = "commission_contract";
case B2bContract = "b2b_contract";
case BoardMemberContract = "board_member_contract";

View File

@ -15,6 +15,5 @@ class VacationRequestAcceptedByAdministrative
public function __construct(
public VacationRequest $vacationRequest,
) {
}
) {}
}

View File

@ -15,6 +15,5 @@ class VacationRequestAcceptedByTechnical
public function __construct(
public VacationRequest $vacationRequest,
) {
}
) {}
}

View File

@ -15,6 +15,5 @@ class VacationRequestApproved
public function __construct(
public VacationRequest $vacationRequest,
) {
}
) {}
}

View File

@ -15,6 +15,5 @@ class VacationRequestCancelled
public function __construct(
public VacationRequest $vacationRequest,
) {
}
) {}
}

View File

@ -15,6 +15,5 @@ class VacationRequestCreated
public function __construct(
public VacationRequest $vacationRequest,
) {
}
) {}
}

View File

@ -15,6 +15,5 @@ class VacationRequestRejected
public function __construct(
public VacationRequest $vacationRequest,
) {
}
) {}
}

View File

@ -20,6 +20,5 @@ class VacationRequestStateChanged
public ?VacationRequestState $from,
public VacationRequestState $to,
public ?User $user = null,
) {
}
) {}
}

View File

@ -15,6 +15,5 @@ class VacationRequestWaitsForAdminApproval
public function __construct(
public VacationRequest $vacationRequest,
) {
}
) {}
}

View File

@ -15,6 +15,5 @@ class VacationRequestWaitsForTechApproval
public function __construct(
public VacationRequest $vacationRequest,
) {
}
) {}
}

View File

@ -11,8 +11,7 @@ class HandleAcceptedByAdministrativeVacationRequest
{
public function __construct(
protected VacationRequestStateManager $stateManager,
) {
}
) {}
public function handle(VacationRequestAcceptedByAdministrative $event): void
{

View File

@ -13,8 +13,7 @@ class HandleAcceptedByTechnicalVacationRequest
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationRequestStateManager $stateManager,
) {
}
) {}
public function handle(VacationRequestAcceptedByTechnical $event): void
{

View File

@ -13,8 +13,7 @@ class HandleCreatedVacationRequest
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationRequestStateManager $stateManager,
) {
}
) {}
public function handle(VacationRequestCreated $event): void
{

View File

@ -13,8 +13,7 @@ use Toby\Eloquent\Models\User;
class SendApprovedVacationRequestNotification
{
public function __construct(
) {
}
) {}
public function handle(VacationRequestApproved $event): void
{

View File

@ -13,8 +13,7 @@ use Toby\Eloquent\Models\User;
class SendCancelledVacationRequestNotification
{
public function __construct(
) {
}
) {}
public function handle(VacationRequestCancelled $event): void
{

View File

@ -11,8 +11,7 @@ use Toby\Domain\Notifications\VacationRequestCreatedOnEmployeeBehalf;
class SendCreatedVacationRequestNotification
{
public function __construct(
) {
}
) {}
public function handle(VacationRequestCreated $event): void
{

View File

@ -13,8 +13,7 @@ use Toby\Eloquent\Models\User;
class SendRejectedVacationRequestNotification
{
public function __construct(
) {
}
) {}
public function handle(VacationRequestRejected $event): void
{

View File

@ -13,8 +13,7 @@ use Toby\Eloquent\Models\User;
class SendWaitedForAdministrativeVacationRequestNotification
{
public function __construct(
) {
}
) {}
public function handle(VacationRequestWaitsForAdminApproval $event): void
{

View File

@ -13,8 +13,7 @@ use Toby\Eloquent\Models\User;
class SendWaitedForTechnicalVacationRequestNotification
{
public function __construct(
) {
}
) {}
public function handle(VacationRequestWaitsForTechApproval $event): void
{

View File

@ -18,8 +18,7 @@ class VacationRequestApprovedNotification extends Notification
public function __construct(
protected VacationRequest $vacationRequest,
protected User $user,
) {
}
) {}
public function via(): array
{

View File

@ -18,8 +18,7 @@ class VacationRequestCancelledNotification extends Notification
public function __construct(
protected VacationRequest $vacationRequest,
protected User $user,
) {
}
) {}
public function via(): array
{

View File

@ -16,8 +16,7 @@ class VacationRequestCreatedNotification extends Notification
public function __construct(
protected VacationRequest $vacationRequest,
) {
}
) {}
public function via(): array
{

View File

@ -16,8 +16,7 @@ class VacationRequestCreatedOnEmployeeBehalf extends Notification
public function __construct(
protected VacationRequest $vacationRequest,
) {
}
) {}
public function via(): array
{

View File

@ -18,8 +18,7 @@ class VacationRequestRejectedNotification extends Notification
public function __construct(
protected VacationRequest $vacationRequest,
protected User $user,
) {
}
) {}
public function via(): array
{

View File

@ -18,8 +18,7 @@ class VacationRequestWaitsForAdminApprovalNotification extends Notification
public function __construct(
protected VacationRequest $vacationRequest,
protected User $user,
) {
}
) {}
public function via(): array
{

View File

@ -18,8 +18,7 @@ class VacationRequestWaitsForTechApprovalNotification extends Notification
public function __construct(
protected VacationRequest $vacationRequest,
protected User $user,
) {
}
) {}
public function via(): array
{

View File

@ -41,8 +41,7 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
public function __construct(
protected User $user,
protected Carbon $month,
) {
}
) {}
public function title(): string
{

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Toby\Domain\Enums\VacationType;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\YearPeriod;
class UserVacationStatsRetriever
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
) {}
public function getUsedVacationDays(User $user, YearPeriod $yearPeriod): int
{
return $user
->vacations()
->where("year_period_id", $yearPeriod->id)
->whereRelation(
"vacationRequest",
fn(Builder $query) => $query
->whereIn("type", $this->getLimitableVacationTypes())
->states(VacationRequestStatesRetriever::successStates()),
)
->count();
}
public function getPendingVacationDays(User $user, YearPeriod $yearPeriod): int
{
return $user
->vacations()
->where("year_period_id", $yearPeriod->id)
->whereRelation(
"vacationRequest",
fn(Builder $query) => $query
->whereIn("type", $this->getLimitableVacationTypes())
->states(VacationRequestStatesRetriever::pendingStates()),
)
->count();
}
public function getOtherApprovedVacationDays(User $user, YearPeriod $yearPeriod): int
{
return $user
->vacations()
->where("year_period_id", $yearPeriod->id)
->whereRelation(
"vacationRequest",
fn(Builder $query) => $query
->whereIn("type", $this->getNotLimitableVacationTypes())
->states(VacationRequestStatesRetriever::successStates()),
)
->count();
}
public function getRemainingVacationDays(User $user, YearPeriod $yearPeriod): int
{
$limit = $this->getVacationDaysLimit($user, $yearPeriod);
$used = $this->getUsedVacationDays($user, $yearPeriod);
$pending = $this->getPendingVacationDays($user, $yearPeriod);
return $limit - $used - $pending;
}
public function getVacationDaysLimit(User $user, YearPeriod $yearPeriod): int
{
$limit = $user->vacationLimits()
->where("year_period_id", $yearPeriod->id)
->first()
->days;
return $limit ?? 0;
}
protected function getLimitableVacationTypes(): Collection
{
$types = new Collection(VacationType::cases());
return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type));
}
protected function getNotLimitableVacationTypes(): Collection
{
$types = new Collection(VacationType::cases());
return $types->filter(fn(VacationType $type) => !$this->configRetriever->hasLimit($type));
}
}

View File

@ -31,8 +31,7 @@ class VacationRequestStateManager
public function __construct(
protected Auth $auth,
protected Dispatcher $dispatcher,
) {
}
) {}
public function markAsCreated(VacationRequest $vacationRequest, ?User $user = null): void
{

View File

@ -16,8 +16,7 @@ class VacationTypeConfigRetriever
public function __construct(
protected Repository $config,
) {
}
) {}
public function needsTechnicalApproval(VacationType $type): bool
{

View File

@ -19,8 +19,7 @@ class DoesNotExceedLimitRule implements VacationRequestRule
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationDaysCalculator $vacationDaysCalculator,
) {
}
) {}
public function check(VacationRequest $vacationRequest): bool
{

View File

@ -11,8 +11,7 @@ class MinimumOneVacationDayRule implements VacationRequestRule
{
public function __construct(
protected VacationDaysCalculator $vacationDaysCalculator,
) {
}
) {}
public function check(VacationRequest $vacationRequest): bool
{

View File

@ -26,8 +26,7 @@ class VacationRequestValidator
public function __construct(
protected Container $container,
) {
}
) {}
/**
* @throws ValidationException

View File

@ -14,8 +14,7 @@ class UserAvatarGenerator
{
public function __construct(
protected InitialAvatar $generator,
) {
}
) {}
public function generateFor(User $user): string
{

View File

@ -13,8 +13,7 @@ class YearPeriodRetriever
public function __construct(
protected Session $session,
) {
}
) {}
public function selected(): YearPeriod
{

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Database\Factories\VacationRequestActivityFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Toby\Domain\States\VacationRequest\VacationRequestState;
@ -17,6 +19,8 @@ use Toby\Domain\States\VacationRequest\VacationRequestState;
*/
class VacationRequestActivity extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
@ -33,4 +37,9 @@ class VacationRequestActivity extends Model
{
return $this->belongsTo(VacationRequest::class);
}
protected static function newFactory(): VacationRequestActivityFactory
{
return VacationRequestActivityFactory::new();
}
}

View File

@ -14,8 +14,7 @@ class UserObserver
public function __construct(
protected UserAvatarGenerator $generator,
protected YearPeriodRetriever $yearPeriodRetriever,
) {
}
) {}
public function created(User $user): void
{

View File

@ -13,8 +13,7 @@ class VacationRequestObserver
public function __construct(
protected Auth $auth,
protected Dispatcher $dispatcher,
) {
}
) {}
public function creating(VacationRequest $vacationRequest): void
{

View File

@ -14,8 +14,7 @@ class YearPeriodObserver
public function __construct(
protected UserAvatarGenerator $generator,
protected PolishHolidaysRetriever $polishHolidaysRetriever,
) {
}
) {}
public function created(YearPeriod $yearPeriod): void
{

View File

@ -13,8 +13,7 @@ class SelectedYearPeriodScope implements Scope
{
public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever,
) {
}
) {}
public function apply(Builder $builder, Model $model): Builder
{

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Controllers\Api;
use Illuminate\Http\JsonResponse;
use Toby\Domain\UserVacationStatsRetriever;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\User;
use Toby\Infrastructure\Http\Controllers\Controller;
use Toby\Infrastructure\Http\Requests\Api\CalculateVacationStatsRequest;
class CalculateUserVacationStatsController extends Controller
{
public function __invoke(
CalculateVacationStatsRequest $request,
UserVacationStatsRetriever $vacationStatsRetriever,
YearPeriodRetriever $yearPeriodRetriever,
): JsonResponse {
/** @var User $user */
$user = User::query()->find($request->get("user"));
$yearPeriod = $yearPeriodRetriever->selected();
$limit = $vacationStatsRetriever->getVacationDaysLimit($user, $yearPeriod);
$used = $vacationStatsRetriever->getUsedVacationDays($user, $yearPeriod);
$pending = $vacationStatsRetriever->getPendingVacationDays($user, $yearPeriod);
$other = $vacationStatsRetriever->getOtherApprovedVacationDays($user, $yearPeriod);
$remaining = $limit - $used - $pending;
return new JsonResponse([
"limit" => $limit,
"remaining" => $remaining,
"used" => $used,
"pending" => $pending,
"other" => $other,
]);
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Inertia\Response;
use Toby\Domain\UserVacationStatsRetriever;
use Toby\Domain\VacationRequestStatesRetriever;
use Toby\Eloquent\Models\Holiday;
use Toby\Eloquent\Models\Vacation;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Eloquent\Models\YearPeriod;
use Toby\Infrastructure\Http\Resources\AbsenceResource;
use Toby\Infrastructure\Http\Resources\HolidayResource;
use Toby\Infrastructure\Http\Resources\VacationRequestResource;
class DashboardController extends Controller
{
public function __invoke(Request $request, UserVacationStatsRetriever $vacationStatsRetriever): Response
{
$user = $request->user();
$now = Carbon::now();
$yearPeriod = YearPeriod::findByYear($now->year);
$absences = Vacation::query()
->with(["user", "vacationRequest"])
->whereDate("date", $now)
->whereRelation(
"vacationRequest",
fn(Builder $query) => $query->states(VacationRequestStatesRetriever::successStates()),
)
->get();
$vacationRequests = VacationRequest::query()
->latest("updated_at")
->limit(3)
->get();
$holidays = Holiday::query()
->whereDate("date", ">=", $now)
->latest()
->limit(3)
->get();
$limit = $vacationStatsRetriever->getVacationDaysLimit($user, $yearPeriod);
$used = $vacationStatsRetriever->getUsedVacationDays($user, $yearPeriod);
$pending = $vacationStatsRetriever->getPendingVacationDays($user, $yearPeriod);
$other = $vacationStatsRetriever->getOtherApprovedVacationDays($user, $yearPeriod);
$remaining = $limit - $used - $pending;
return inertia("Dashboard", [
"absences" => AbsenceResource::collection($absences),
"vacationRequests" => VacationRequestResource::collection($vacationRequests),
"holidays" => HolidayResource::collection($holidays),
"stats" => [
"limit" => $limit,
"remaining" => $remaining,
"used" => $used,
"pending" => $pending,
"other" => $other,
],
]);
}
}

View File

@ -8,6 +8,7 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Response;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\Holiday;
use Toby\Infrastructure\Http\Requests\HolidayRequest;
use Toby\Infrastructure\Http\Resources\HolidayFormDataResource;
@ -15,9 +16,12 @@ use Toby\Infrastructure\Http\Resources\HolidayResource;
class HolidayController extends Controller
{
public function index(Request $request): Response
public function index(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response
{
$holidays = Holiday::query()
$yearPeriod = $yearPeriodRetriever->selected();
$holidays = $yearPeriod
->holidays()
->orderBy("date")
->get();

View File

@ -6,24 +6,41 @@ namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Inertia\Response;
use Toby\Domain\UserVacationStatsRetriever;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\VacationLimit;
use Toby\Eloquent\Models\YearPeriod;
use Toby\Infrastructure\Http\Requests\VacationLimitRequest;
use Toby\Infrastructure\Http\Resources\VacationLimitResource;
use Toby\Infrastructure\Http\Resources\UserResource;
class VacationLimitController extends Controller
{
public function edit(): Response
public function edit(YearPeriodRetriever $yearPeriodRetriever, UserVacationStatsRetriever $statsRetriever): Response
{
$this->authorize("manageVacationLimits");
$limits = VacationLimit::query()
$yearPeriod = $yearPeriodRetriever->selected();
$previousYearPeriod = YearPeriod::findByYear($yearPeriod->year - 1);
$limits = $yearPeriod
->vacationLimits()
->with("user")
->orderByUserField("last_name")
->orderByUserField("first_name")
->get();
$limitsResource = $limits->map(fn(VacationLimit $limit) => [
"id" => $limit->id,
"user" => new UserResource($limit->user),
"hasVacation" => $limit->hasVacation(),
"days" => $limit->days,
"remainingLastYear" => $previousYearPeriod
? $statsRetriever->getRemainingVacationDays($limit->user, $previousYearPeriod)
: 0,
]);
return inertia("VacationLimits", [
"limits" => VacationLimitResource::collection($limits),
"limits" => $limitsResource,
]);
}

View File

@ -6,6 +6,7 @@ namespace Toby\Infrastructure\Http\Controllers;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response as LaravelResponse;
@ -88,9 +89,15 @@ class VacationRequestController extends Controller
return $pdf->stream();
}
public function create(Request $request): Response
public function create(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response
{
$yearPeriod = $yearPeriodRetriever->selected();
$users = User::query()
->whereRelation(
"vacationlimits",
fn(Builder $query) => $query->where("year_period_id", $yearPeriod->id)->whereNotNull("days"),
)
->orderBy("last_name")
->orderBy("first_name")
->get();

View File

@ -13,8 +13,7 @@ class HandleInertiaRequests extends Middleware
{
public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever,
) {
}
) {}
public function share(Request $request): array
{

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class CalculateVacationStatsRequest extends FormRequest
{
public function rules(): array
{
return [
"user" => ["required", "exists:users,id"],
];
}
}

View File

@ -6,7 +6,7 @@ namespace Toby\Infrastructure\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class VacationLimitResource extends JsonResource
class AbsenceResource extends JsonResource
{
public static $wrap = null;
@ -15,8 +15,7 @@ class VacationLimitResource extends JsonResource
return [
"id" => $this->id,
"user" => new UserResource($this->user),
"hasVacation" => $this->hasVacation(),
"days" => $this->days,
"date" => $this->date->toDisplayString(),
];
}
}

View File

@ -18,8 +18,7 @@ class ClearVacationRequestDaysInGoogleCalendar implements ShouldQueue
public function __construct(
protected VacationRequest $vacationRequest,
) {
}
) {}
public function handle(): void
{

View File

@ -18,8 +18,7 @@ class SendVacationRequestDaysToGoogleCalendar implements ShouldQueue
public function __construct(
protected VacationRequest $vacationRequest,
) {
}
) {}
public function handle(): void
{

View File

@ -23,7 +23,7 @@
"maatwebsite/excel": "^3.1"
},
"require-dev": {
"blumilksoftware/codestyle": "^0.9.0",
"blumilksoftware/codestyle": "^0.10.0",
"spatie/laravel-ignition": "^1.0",
"fakerphp/faker": "^1.9.1",
"laravel/dusk": "^6.21",
@ -47,6 +47,11 @@
"ecs": "./vendor/bin/ecs check --clear-cache",
"ecsf": "./vendor/bin/ecs check --clear-cache --fix",
"test": "@php artisan test",
"fresh": "@php artisan migrate:fresh",
"fresh:demo": [
"composer fresh",
"@php artisan db:seed --class=DemoSeeder"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"

713
composer.lock generated
View File

@ -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": "c090431972dc8bfbe198ce9fc9f816f3",
"content-hash": "d3f019c6e743a3249af78baefe7acb01",
"packages": [
{
"name": "asm89/stack-cors",
@ -7468,21 +7468,22 @@
"packages-dev": [
{
"name": "blumilksoftware/codestyle",
"version": "v0.9.0",
"version": "v0.10.0",
"source": {
"type": "git",
"url": "https://github.com/blumilksoftware/codestyle.git",
"reference": "c1e06d9df22adaf1f0ae646dfbc0620b28e9a79a"
"reference": "39da9ec4922e633e8f0bd065f244365217e9d00a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/blumilksoftware/codestyle/zipball/c1e06d9df22adaf1f0ae646dfbc0620b28e9a79a",
"reference": "c1e06d9df22adaf1f0ae646dfbc0620b28e9a79a",
"url": "https://api.github.com/repos/blumilksoftware/codestyle/zipball/39da9ec4922e633e8f0bd065f244365217e9d00a",
"reference": "39da9ec4922e633e8f0bd065f244365217e9d00a",
"shasum": ""
},
"require": {
"kubawerlos/php-cs-fixer-custom-fixers": "^3.7",
"php": "^8.0",
"symplify/easy-coding-standard": "10.0.8"
"symplify/easy-coding-standard": "10.1.0"
},
"require-dev": {
"composer/composer": "2.*",
@ -7509,9 +7510,299 @@
"description": "Blumilk codestyle configurator",
"support": {
"issues": "https://github.com/blumilksoftware/codestyle/issues",
"source": "https://github.com/blumilksoftware/codestyle/tree/v0.9.0"
"source": "https://github.com/blumilksoftware/codestyle/tree/v0.10.0"
},
"time": "2022-01-04T07:21:36+00:00"
"time": "2022-02-23T07:26:50+00:00"
},
{
"name": "composer/pcre",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd",
"reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.3",
"phpstan/phpstan-strict-rules": "^1.1",
"symfony/phpunit-bridge": "^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.0.0"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2022-02-25T20:21:48+00:00"
},
{
"name": "composer/semver",
"version": "3.2.9",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "a951f614bd64dcd26137bc9b7b2637ddcfc57649"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/a951f614bd64dcd26137bc9b7b2637ddcfc57649",
"reference": "a951f614bd64dcd26137bc9b7b2637ddcfc57649",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.4",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.2.9"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2022-02-04T13:58:43+00:00"
},
{
"name": "composer/xdebug-handler",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
"reference": "ced299686f41dce890debac69273b47ffe98a40c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c",
"reference": "ced299686f41dce890debac69273b47ffe98a40c",
"shasum": ""
},
"require": {
"composer/pcre": "^1 || ^2 || ^3",
"php": "^7.2.5 || ^8.0",
"psr/log": "^1 || ^2 || ^3"
},
"require-dev": {
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-strict-rules": "^1.1",
"symfony/phpunit-bridge": "^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Composer\\XdebugHandler\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "John Stevenson",
"email": "john-stevenson@blueyonder.co.uk"
}
],
"description": "Restarts a process without Xdebug.",
"keywords": [
"Xdebug",
"performance"
],
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/xdebug-handler/issues",
"source": "https://github.com/composer/xdebug-handler/tree/3.0.3"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2022-02-25T21:32:43+00:00"
},
{
"name": "doctrine/annotations",
"version": "1.13.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/annotations.git",
"reference": "5b668aef16090008790395c02c893b1ba13f7e08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08",
"reference": "5b668aef16090008790395c02c893b1ba13f7e08",
"shasum": ""
},
"require": {
"doctrine/lexer": "1.*",
"ext-tokenizer": "*",
"php": "^7.1 || ^8.0",
"psr/cache": "^1 || ^2 || ^3"
},
"require-dev": {
"doctrine/cache": "^1.11 || ^2.0",
"doctrine/coding-standard": "^6.0 || ^8.1",
"phpstan/phpstan": "^0.12.20",
"phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5",
"symfony/cache": "^4.4 || ^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Docblock Annotations Parser",
"homepage": "https://www.doctrine-project.org/projects/annotations.html",
"keywords": [
"annotations",
"docblock",
"parser"
],
"support": {
"issues": "https://github.com/doctrine/annotations/issues",
"source": "https://github.com/doctrine/annotations/tree/1.13.2"
},
"time": "2021-08-05T19:00:23+00:00"
},
{
"name": "doctrine/instantiator",
@ -7720,6 +8011,95 @@
],
"time": "2022-01-07T12:00:00+00:00"
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
"reference": "1975e4453eb2726d1f50da0ce7fa91295029a4fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1975e4453eb2726d1f50da0ce7fa91295029a4fa",
"reference": "1975e4453eb2726d1f50da0ce7fa91295029a4fa",
"shasum": ""
},
"require": {
"composer/semver": "^3.2",
"composer/xdebug-handler": "^3.0",
"doctrine/annotations": "^1.13",
"ext-json": "*",
"ext-tokenizer": "*",
"php": "^7.4 || ^8.0",
"php-cs-fixer/diff": "^2.0",
"symfony/console": "^5.4 || ^6.0",
"symfony/event-dispatcher": "^5.4 || ^6.0",
"symfony/filesystem": "^5.4 || ^6.0",
"symfony/finder": "^5.4 || ^6.0",
"symfony/options-resolver": "^5.4 || ^6.0",
"symfony/polyfill-mbstring": "^1.23",
"symfony/polyfill-php80": "^1.23",
"symfony/polyfill-php81": "^1.23",
"symfony/process": "^5.4 || ^6.0",
"symfony/stopwatch": "^5.4 || ^6.0"
},
"require-dev": {
"justinrainbow/json-schema": "^5.2",
"keradus/cli-executor": "^1.5",
"mikey179/vfsstream": "^1.6.10",
"php-coveralls/php-coveralls": "^2.5.2",
"php-cs-fixer/accessible-object": "^1.1",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1",
"phpspec/prophecy": "^1.15",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"phpunitgoodpractices/polyfill": "^1.5",
"phpunitgoodpractices/traits": "^1.9.1",
"symfony/phpunit-bridge": "^6.0",
"symfony/yaml": "^5.4 || ^6.0"
},
"suggest": {
"ext-dom": "For handling output formats in XML",
"ext-mbstring": "For handling non-UTF8 characters."
},
"bin": [
"php-cs-fixer"
],
"type": "application",
"autoload": {
"psr-4": {
"PhpCsFixer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Dariusz Rumiński",
"email": "dariusz.ruminski@gmail.com"
}
],
"description": "A tool to automatically fix PHP code style",
"support": {
"issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
"source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.6.0"
},
"funding": [
{
"url": "https://github.com/keradus",
"type": "github"
}
],
"time": "2022-02-07T18:02:40+00:00"
},
{
"name": "hamcrest/hamcrest-php",
"version": "v2.0.1",
@ -7771,6 +8151,53 @@
},
"time": "2020-07-09T08:09:16+00:00"
},
{
"name": "kubawerlos/php-cs-fixer-custom-fixers",
"version": "v3.8.1",
"source": {
"type": "git",
"url": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers.git",
"reference": "cddb6d7a365ce95dd554c357ebc16318b5e6bd0d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/cddb6d7a365ce95dd554c357ebc16318b5e6bd0d",
"reference": "cddb6d7a365ce95dd554c357ebc16318b5e6bd0d",
"shasum": ""
},
"require": {
"ext-filter": "*",
"ext-tokenizer": "*",
"friendsofphp/php-cs-fixer": "^3.6.0",
"php": "^7.4 || ^8.0",
"symfony/finder": "^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5.3 || ^9.1.1"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpCsFixerCustomFixers\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kuba Werłos",
"email": "werlos@gmail.com"
}
],
"description": "A set of custom fixers for PHP CS Fixer",
"support": {
"issues": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues",
"source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.8.1"
},
"time": "2022-02-15T17:06:25+00:00"
},
{
"name": "laravel/dusk",
"version": "v6.22.1",
@ -8172,6 +8599,58 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "php-cs-fixer/diff",
"version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/diff.git",
"reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3",
"reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0",
"symfony/process": "^3.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
}
],
"description": "sebastian/diff v3 backport support for PHP 5.6+",
"homepage": "https://github.com/PHP-CS-Fixer",
"keywords": [
"diff"
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/diff/issues",
"source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2"
},
"time": "2020-10-14T08:32:19+00:00"
},
{
"name": "php-webdriver/webdriver",
"version": "1.12.0",
@ -9977,16 +10456,16 @@
},
{
"name": "spatie/ignition",
"version": "1.0.5",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/ignition.git",
"reference": "6b7bb804f4834b080f5ac941f6ac6800a485011e"
"reference": "8ecde033600064e3ffdbf804deec0dcb05004387"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/ignition/zipball/6b7bb804f4834b080f5ac941f6ac6800a485011e",
"reference": "6b7bb804f4834b080f5ac941f6ac6800a485011e",
"url": "https://api.github.com/repos/spatie/ignition/zipball/8ecde033600064e3ffdbf804deec0dcb05004387",
"reference": "8ecde033600064e3ffdbf804deec0dcb05004387",
"shasum": ""
},
"require": {
@ -10044,7 +10523,7 @@
"type": "github"
}
],
"time": "2022-02-17T21:40:47+00:00"
"time": "2022-03-01T17:01:33+00:00"
},
{
"name": "spatie/laravel-ignition",
@ -10134,17 +10613,209 @@
"time": "2022-02-15T11:02:15+00:00"
},
{
"name": "symplify/easy-coding-standard",
"version": "10.0.8",
"name": "symfony/filesystem",
"version": "v6.0.5",
"source": {
"type": "git",
"url": "https://github.com/symplify/easy-coding-standard.git",
"reference": "12ef3b0f500d485e6313926e5f2900a74c578394"
"url": "https://github.com/symfony/filesystem.git",
"reference": "6646c13f787057d64701a3a0235cf9567c6ccbbd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symplify/easy-coding-standard/zipball/12ef3b0f500d485e6313926e5f2900a74c578394",
"reference": "12ef3b0f500d485e6313926e5f2900a74c578394",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/6646c13f787057d64701a3a0235cf9567c6ccbbd",
"reference": "6646c13f787057d64701a3a0235cf9567c6ccbbd",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v6.0.5"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-02-28T07:42:30+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v6.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "51f7006670febe4cbcbae177cbffe93ff833250d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/51f7006670febe4cbcbae177cbffe93ff833250d",
"reference": "51f7006670febe4cbcbae177cbffe93ff833250d",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.1|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\OptionsResolver\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an improved replacement for the array_replace PHP function",
"homepage": "https://symfony.com",
"keywords": [
"config",
"configuration",
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v6.0.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-01-02T09:55:41+00:00"
},
{
"name": "symfony/stopwatch",
"version": "v6.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
"reference": "f2c1780607ec6502f2121d9729fd8150a655d337"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/f2c1780607ec6502f2121d9729fd8150a655d337",
"reference": "f2c1780607ec6502f2121d9729fd8150a655d337",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/service-contracts": "^1|^2|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Stopwatch\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/stopwatch/tree/v6.0.5"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-02-21T17:15:17+00:00"
},
{
"name": "symplify/easy-coding-standard",
"version": "10.1.0",
"source": {
"type": "git",
"url": "https://github.com/symplify/easy-coding-standard.git",
"reference": "dcad0cff507fc3e56bf345dcd634dbea3ce0917e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symplify/easy-coding-standard/zipball/dcad0cff507fc3e56bf345dcd634dbea3ce0917e",
"reference": "dcad0cff507fc3e56bf345dcd634dbea3ce0917e",
"shasum": ""
},
"require": {
@ -10174,7 +10845,7 @@
],
"description": "Prefixed scoped version of ECS package",
"support": {
"source": "https://github.com/symplify/easy-coding-standard/tree/10.0.8"
"source": "https://github.com/symplify/easy-coding-standard/tree/10.1.0"
},
"funding": [
{
@ -10186,7 +10857,7 @@
"type": "github"
}
],
"time": "2021-12-31T12:19:54+00:00"
"time": "2022-02-21T11:18:44+00:00"
},
{
"name": "theseer/tokenizer",

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Eloquent\Models\VacationRequestActivity;
class VacationRequestActivityFactory extends Factory
{
protected $model = VacationRequestActivity::class;
public function definition(): array
{
return [
"from" => $this->faker->randomElement(VacationRequestState::cases()),
"to" => $this->faker->randomElement(VacationRequestState::cases()),
];
}
}

View File

@ -19,8 +19,7 @@ class DatabaseSeeder extends Seeder
{
public function __construct(
protected UserAvatarGenerator $avatarGenerator,
) {
}
) {}
public function run(): void
{

View File

@ -0,0 +1,330 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\Role;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\PolishHolidaysRetriever;
use Toby\Domain\VacationDaysCalculator;
use Toby\Eloquent\Helpers\UserAvatarGenerator;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationLimit;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Eloquent\Models\VacationRequestActivity;
use Toby\Eloquent\Models\YearPeriod;
class DemoSeeder extends Seeder
{
public function __construct(
protected UserAvatarGenerator $avatarGenerator,
) {}
public function run(): void
{
User::unsetEventDispatcher();
YearPeriod::unsetEventDispatcher();
VacationRequest::unsetEventDispatcher();
$employee1 = User::factory([
"first_name" => "Jan",
"last_name" => "Kowalski",
"email" => env("LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE"),
"employment_form" => EmploymentForm::EmploymentContract,
"position" => "programista",
"role" => Role::Employee,
"employment_date" => Carbon::createFromDate(2021, 12, 31),
"remember_token" => Str::random(10),
])
->create();
$employee2 = User::factory([
"first_name" => "Anna",
"last_name" => "Nowak",
"email" => "anna.nowak@example.com",
"employment_form" => EmploymentForm::CommissionContract,
"position" => "tester",
"role" => Role::Employee,
"employment_date" => Carbon::createFromDate(2021, 5, 10),
"remember_token" => Str::random(10),
])
->create();
$employee3 = User::factory([
"first_name" => "Tola",
"last_name" => "Sawicka",
"email" => "tola.sawicka@example.com",
"employment_form" => EmploymentForm::B2bContract,
"position" => "programista",
"role" => Role::Employee,
"employment_date" => Carbon::createFromDate(2021, 1, 4),
"remember_token" => Str::random(10),
])
->create();
$technicalApprover = User::factory([
"first_name" => "Maciej",
"last_name" => "Ziółkowski",
"email" => "maciej.ziolkowski@example.com",
"employment_form" => EmploymentForm::BoardMemberContract,
"position" => "programista",
"role" => Role::TechnicalApprover,
"employment_date" => Carbon::createFromDate(2021, 1, 4),
"remember_token" => Str::random(10),
])
->create();
$administrativeApprover = User::factory([
"first_name" => "Katarzyna",
"last_name" => "Zając",
"email" => "katarzyna.zajac@example.com",
"employment_form" => EmploymentForm::EmploymentContract,
"position" => "dyrektor",
"role" => Role::AdministrativeApprover,
"employment_date" => Carbon::createFromDate(2021, 1, 4),
"remember_token" => Str::random(10),
])
->create();
$admin = User::factory([
"first_name" => "Miłosz",
"last_name" => "Borowski",
"email" => "milosz.borowski@example.com",
"employment_form" => EmploymentForm::EmploymentContract,
"position" => "administrator",
"role" => Role::Administrator,
"employment_date" => Carbon::createFromDate(2021, 1, 4),
"remember_token" => Str::random(10),
])
->create();
$users = User::all();
$this->generateAvatarsForUsers($users);
$year = 2021;
YearPeriod::factory()
->count(2)
->sequence(
[
"year" => Carbon::createFromDate($year)->year,
],
[
"year" => Carbon::createFromDate($year + 1)->year,
],
)
->afterCreating(function (YearPeriod $yearPeriod) use ($users): void {
foreach ($users as $user) {
VacationLimit::factory([
"days" => $user->employment_form === EmploymentForm::EmploymentContract ? 26 : null,
])
->for($yearPeriod)
->for($user)
->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();
$currentYearPeriod = YearPeriod::query()->where("year", 2022)->first();
/** @var VacationRequest $vacationRequestApproved */
$vacationRequestApproved = VacationRequest::factory([
"type" => VacationType::Vacation->value,
"state" => VacationRequestState::Created,
"from" => Carbon::create($currentYearPeriod->year, 1, 31)->toDateString(),
"to" => Carbon::create($currentYearPeriod->year, 2, 4)->toDateString(),
"comment" => "Komentarz do wniosku urlopowego.",
])
->for($employee1)
->for($employee1, "creator")
->for($currentYearPeriod)
->afterCreating(function (VacationRequest $vacationRequest): void {
$days = app(VacationDaysCalculator::class)->calculateDays(
$vacationRequest->yearPeriod,
$vacationRequest->from,
$vacationRequest->to,
);
foreach ($days as $day) {
$vacationRequest->vacations()->create([
"date" => $day,
"user_id" => $vacationRequest->user->id,
"year_period_id" => $vacationRequest->yearPeriod->id,
]);
}
})
->create();
VacationRequestActivity::factory([
"from" => null,
"to" => VacationRequestState::Created,
])->for($vacationRequestApproved)
->for($employee1)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::Created,
"to" => VacationRequestState::WaitingForTechnical,
])->for($vacationRequestApproved)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::WaitingForTechnical,
"to" => VacationRequestState::AcceptedByTechnical,
])->for($vacationRequestApproved)
->for($technicalApprover)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::AcceptedByTechnical,
"to" => VacationRequestState::WaitingForAdministrative,
])->for($vacationRequestApproved)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::WaitingForAdministrative,
"to" => VacationRequestState::AcceptedByAdministrative,
])->for($vacationRequestApproved)
->for($administrativeApprover)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::AcceptedByAdministrative,
"to" => VacationRequestState::Approved,
])->for($vacationRequestApproved)
->create();
$vacationRequestApproved->changeStateTo(VacationRequestState::Approved);
/** @var VacationRequest $vacationRequestWaitsForAdminApproval */
$vacationRequestWaitsForAdminApproval = VacationRequest::factory([
"type" => VacationType::Vacation->value,
"state" => VacationRequestState::Created,
"from" => Carbon::create($currentYearPeriod->year, 2, 14)->toDateString(),
"to" => Carbon::create($currentYearPeriod->year, 2, 14)->toDateString(),
"comment" => "Komentarz do wniosku urlopowego.",
])
->for($employee1)
->for($employee1, "creator")
->for($currentYearPeriod)
->afterCreating(function (VacationRequest $vacationRequest): void {
$days = app(VacationDaysCalculator::class)->calculateDays(
$vacationRequest->yearPeriod,
$vacationRequest->from,
$vacationRequest->to,
);
foreach ($days as $day) {
$vacationRequest->vacations()->create([
"date" => $day,
"user_id" => $vacationRequest->user->id,
"year_period_id" => $vacationRequest->yearPeriod->id,
]);
}
})
->create();
VacationRequestActivity::factory([
"from" => null,
"to" => VacationRequestState::Created,
])->for($vacationRequestWaitsForAdminApproval)
->for($employee1)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::Created,
"to" => VacationRequestState::WaitingForTechnical,
])->for($vacationRequestWaitsForAdminApproval)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::WaitingForTechnical,
"to" => VacationRequestState::AcceptedByTechnical,
])->for($vacationRequestWaitsForAdminApproval)
->for($technicalApprover)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::AcceptedByTechnical,
"to" => VacationRequestState::WaitingForAdministrative,
])->for($vacationRequestWaitsForAdminApproval)
->create();
$vacationRequestWaitsForAdminApproval->changeStateTo(VacationRequestState::WaitingForAdministrative);
/** @var VacationRequest $vacationRequestRejected */
$vacationRequestRejected = VacationRequest::factory([
"type" => VacationType::Vacation->value,
"state" => VacationRequestState::Created,
"from" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(),
"to" => Carbon::create($currentYearPeriod->year, 2, 7)->toDateString(),
"comment" => "",
])
->for($employee1)
->for($employee1, "creator")
->for($currentYearPeriod)
->afterCreating(function (VacationRequest $vacationRequest): void {
$days = app(VacationDaysCalculator::class)->calculateDays(
$vacationRequest->yearPeriod,
$vacationRequest->from,
$vacationRequest->to,
);
foreach ($days as $day) {
$vacationRequest->vacations()->create([
"date" => $day,
"user_id" => $vacationRequest->user->id,
"year_period_id" => $vacationRequest->yearPeriod->id,
]);
}
})
->create();
VacationRequestActivity::factory([
"from" => null,
"to" => VacationRequestState::Created,
])->for($vacationRequestRejected)
->for($employee1)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::Created,
"to" => VacationRequestState::WaitingForTechnical,
])->for($vacationRequestRejected)
->create();
VacationRequestActivity::factory([
"from" => VacationRequestState::WaitingForTechnical,
"to" => VacationRequestState::Rejected,
])->for($vacationRequestRejected)
->for($technicalApprover)
->create();
$vacationRequestRejected->changeStateTo(VacationRequestState::Rejected);
}
protected function generateAvatarsForUsers(Collection $users): void
{
foreach ($users as $user) {
$user->saveAvatar($this->avatarGenerator->generateFor($user));
}
}
}

1992
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,31 +12,32 @@
"lintf": "./node_modules/.bin/eslint resources/js --ext .js,.vue --fix"
},
"dependencies": {
"@headlessui/vue": "^1.4.2",
"@headlessui/vue": "^1.5.0",
"@heroicons/vue": "^1.0.5",
"@inertiajs/inertia": "^0.11.0",
"@inertiajs/inertia-vue3": "^0.6.0",
"@inertiajs/progress": "^0.2.7",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/line-clamp": "^0.3.1",
"@tailwindcss/typography": "^0.5.0",
"@vue/compiler-sfc": "^3.2.26",
"@tailwindcss/typography": "^0.5.2",
"@vue/compiler-sfc": "^3.2.31",
"autoprefixer": "^10.4.2",
"axios": "^0.25.0",
"echarts": "^5.2.2",
"axios": "^0.26.0",
"echarts": "^5.3.0",
"eslit": "^6.0.0",
"flatpickr": "^4.6.9",
"laravel-mix": "^6.0.6",
"laravel-mix": "^6.0.43",
"lodash": "^4.17.21",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.13",
"vue": "^3.2.26",
"postcss": "^8.4.7",
"tailwindcss": "^3.0.23",
"vue": "^3.2.31",
"vue-echarts": "^6.0.2",
"vue-flatpickr-component": "^9.0.5",
"vue-loader": "^17.0.0",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"eslint": "^8.6.0",
"eslint-plugin-vue": "^8.2.0"
"eslint": "^8.10.0",
"eslint-plugin-vue": "^8.5.0"
}
}

View File

@ -12,7 +12,7 @@
:href="`/timesheet/${selectedMonth.value}`"
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"
>
Pobierz plik excel
Pobierz plik Excel
</a>
</div>
</div>

View File

@ -1,17 +1,9 @@
<template>
<InertiaHead title="Strona główna" />
<div class="grid grid-cols-1 gap-4 items-start lg:grid-cols-3 lg:gap-8">
<!-- Left column -->
<div class="grid grid-cols-1 gap-4 lg:col-span-2">
<!-- Welcome panel -->
<section aria-labelledby="profile-overview-title">
<div class="rounded-lg bg-white overflow-hidden shadow">
<h2
id="profile-overview-title"
class="sr-only"
>
Profile Overview
</h2>
<section>
<div class=" bg-white overflow-hidden shadow">
<div class="bg-white p-6">
<div class="sm:flex sm:items-center sm:justify-between">
<div class="sm:flex sm:space-x-5">
@ -24,7 +16,7 @@
</div>
<div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left">
<p class="text-sm font-medium text-gray-600">
Welcome back,
Cześć,
</p>
<p class="text-xl font-bold text-gray-900 sm:text-2xl">
{{ user.name }}
@ -34,136 +26,109 @@
</p>
</div>
</div>
<div class="mt-5 flex justify-center sm:mt-0">
<InertiaLink
href="#"
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"
>
View profile
</InertiaLink>
</div>
</div>
</div>
<div
class="border-t border-gray-200 bg-gray-50 grid grid-cols-1 divide-y divide-gray-200 sm:grid-cols-3 sm:divide-y-0 sm:divide-x"
>
<div
v-for="stat in stats"
:key="stat.label"
class="px-6 py-5 text-sm font-medium text-center"
>
<span class="text-gray-900">{{ stat.value }}</span>
{{ ' ' }}
<span class="text-gray-600">{{ stat.label }}</span>
</div>
</div>
</div>
</section>
<!-- Actions panel -->
<section aria-labelledby="quick-links-title">
<div
class="rounded-lg bg-gray-200 overflow-hidden shadow divide-y divide-gray-200 sm:divide-y-0 sm:grid sm:grid-cols-2 sm:gap-px"
>
<h2
id="quick-links-title"
class="sr-only"
>
Quick links
</h2>
<div
v-for="(action, actionIdx) in actions"
:key="action.name"
:class="[actionIdx === 0 ? 'rounded-tl-lg rounded-tr-lg sm:rounded-tr-none' : '', actionIdx === 1 ? 'sm:rounded-tr-lg' : '', actionIdx === actions.length - 2 ? 'sm:rounded-bl-lg' : '', actionIdx === actions.length - 1 ? 'rounded-bl-lg rounded-br-lg sm:rounded-bl-none' : '', 'relative group bg-white p-6 focus-within:ring-2 focus-within:ring-inset focus-within:ring-cyan-500']"
>
<div>
<span
:class="[action.iconBackground, action.iconForeground, 'rounded-lg inline-flex p-3 ring-4 ring-white']"
>
<component
:is="action.icon"
class="h-6 w-6"
aria-hidden="true"
/>
</span>
<section>
<div class="grid grid-cols-2 gap-4">
<div class="bg-white shadow-md p-4">
<VacationChart :stats="stats" />
</div>
<div class="h-full">
<div class="grid grid-cols-2 gap-4 h-full">
<div class="px-4 py-5 bg-white shadow-md sm:p-6">
<dd class="mt-1 text-4xl font-semibold text-blumilk-500">
{{ stats.remaining }}
</dd>
<dt class="text-md font-medium text-gray-700 truncate">
Pozostało
</dt>
<dt class="text-sm font-medium text-gray-500 mt-2">
Dni do wykorzystania teraz.
</dt>
</div>
<div class="px-4 py-5 bg-white shadow-md sm:p-6">
<dd class="mt-1 text-4xl font-semibold text-blumilk-700">
{{ stats.used }}
</dd>
<dt class="text-md font-medium text-gray-700 truncate">
Dni wykorzystane
</dt>
<dt class="text-sm font-medium text-gray-500 mt-2">
Dni, które zostały już wykorzystane na urlop wypoczynkowy.
</dt>
</div>
<div class="px-4 py-5 bg-white shadow-md sm:p-6">
<dt class="mt-1 text-4xl font-semibold text-blumilk-200">
{{ stats.pending }}
</dt>
<dd class="text-md font-medium text-gray-500 truncate">
Rozpatrywane
</dd>
<dt class="text-sm font-medium text-gray-500 mt-2">
Dni czekające na akceptację przełożonych.
</dt>
</div>
<div class="px-4 py-5 bg-white shadow-md sm:p-6">
<dt class="mt-1 text-4xl font-semibold text-gray-900">
{{ stats.limit }}
</dt>
<dd class="text-md font-medium text-gray-500 truncate">
Limit urlopu
</dd>
<dt class="text-sm font-medium text-gray-500 mt-2">
Twój roczny limit urlopu wypoczynkowego.
</dt>
</div>
<div class="px-4 py-5 bg-white shadow-md sm:p-6 col-span-2">
<dt class="mt-1 text-4xl font-semibold text-gray-900">
{{ stats.other }}
</dt>
<dd class="text-md font-medium text-gray-500 truncate">
Inne urlopy
</dd>
<dt class="text-sm font-medium text-gray-500 mt-2">
Urlopy bezpłatne, okolicznościowe, zwolnienia lekarskie, itd., które zostały już zatwierdzone.
</dt>
</div>
</div>
<div class="mt-8">
<h3 class="text-lg font-medium">
<InertiaLink
:href="action.href"
class="focus:outline-none"
>
<!-- Extend touch target to entire panel -->
<span
class="absolute inset-0"
aria-hidden="true"
/>
{{ action.name }}
</InertiaLink>
</h3>
<p class="mt-2 text-sm text-gray-500">
Doloribus dolores nostrum quia qui natus officia quod et dolorem. Sit
repellendus qui ut at blanditiis et quo et molestiae.
</p>
</div>
<span
class="pointer-events-none absolute top-6 right-6 text-gray-300 group-hover:text-gray-400"
aria-hidden="true"
>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M20 4h1a1 1 0 00-1-1v1zm-1 12a1 1 0 102 0h-2zM8 3a1 1 0 000 2V3zM3.293 19.293a1 1 0 101.414 1.414l-1.414-1.414zM19 4v12h2V4h-2zm1-1H8v2h12V3zm-.707.293l-16 16 1.414 1.414 16-16-1.414-1.414z"
/>
</svg>
</span>
</div>
</div>
</section>
</div>
<!-- Right column -->
<div class="grid grid-cols-1 gap-4">
<!-- Announcements -->
<section aria-labelledby="announcements-title">
<div class="rounded-lg bg-white overflow-hidden shadow">
<div class="p-6">
<h2
id="announcements-title"
class="text-base font-medium text-gray-900"
>
Announcements
<section>
<div class="bg-white shadow-md">
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Twoje wnioski
</h2>
</div>
<div class="border-t border-gray-200 pb-5 px-4 sm:px-6">
<div class="flow-root mt-6">
<ul
role="list"
class="-my-5 divide-y divide-gray-200"
>
<ul class="-my-5 divide-y divide-gray-200">
<li
v-for="announcement in announcements"
:key="announcement.id"
v-for="request in vacationRequests.data"
:key="request.id"
class="py-5"
>
<div class="relative focus-within:ring-2 focus-within:ring-cyan-500">
<h3 class="text-sm font-semibold text-gray-800">
<h3 class="text-sm font-semibold text-blumilk-600 hover:text-blumilk-500">
<InertiaLink
:href="announcement.href"
:href="`/vacation-requests/${request.id}`"
class="hover:underline focus:outline-none"
>
<!-- Extend touch target to entire panel -->
<span
class="absolute inset-0"
aria-hidden="true"
/>
{{ announcement.title }}
<span class="absolute inset-0" />
Wniosek o {{ request.type.toLowerCase() }}
[{{ request.name }}]
</InertiaLink>
</h3>
<p class="mt-1 text-sm text-gray-600 line-clamp-2">
{{ announcement.preview }}
<p class="mt-1 text-sm text-gray-600">
{{ request.from }} - {{ request.to }}
</p>
<p class="mt-2 text-sm text-gray-600">
<Status :status="request.state" />
</p>
</div>
</li>
@ -171,71 +136,84 @@
</div>
<div class="mt-6">
<InertiaLink
href="#"
href="/vacation-requests"
class="w-full flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
View all
Zobacz wszystkie
</InertiaLink>
</div>
</div>
</div>
</section>
<!-- Recent Hires -->
<section aria-labelledby="recent-hires-title">
<div class="rounded-lg bg-white overflow-hidden shadow">
<div class="p-6">
<h2
id="recent-hires-title"
class="text-base font-medium text-gray-900"
>
Recent Hires
<section>
<div class="bg-white shadow-md">
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Dzisiejsze nieobecności
</h2>
<div class="flow-root mt-6">
<ul
role="list"
class="-my-5 divide-y divide-gray-200"
</div>
<div class="border-t border-gray-200 px-4 sm:px-6">
<ul class="divide-y divide-gray-200">
<li
v-for="absence in absences.data"
:key="absence.user.id"
class="py-4 flex"
>
<li
v-for="person in recentHires"
:key="person.handle"
class="py-4"
<img
class="h-10 w-10 rounded-full"
:src="absence.user.avatar"
>
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<img
class="h-8 w-8 rounded-full"
:src="person.imageUrl"
alt=""
>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">
{{ person.name }}
</p>
<p class="text-sm text-gray-500 truncate">
{{ '@' + person.handle }}
</p>
</div>
<div>
<InertiaLink
:href="person.href"
class="inline-flex items-center shadow-sm px-2.5 py-0.5 border border-gray-300 text-sm leading-5 font-medium rounded-full text-gray-700 bg-white hover:bg-gray-50"
>
View
</InertiaLink>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">
{{ absence.user.name }}
</p>
<p class="text-sm text-gray-500">
{{ absence.user.email }}
</p>
</div>
</li>
<li v-if="! absences.data.length">
<p class="py-2">
Brak danych
</p>
</li>
</ul>
</div>
</div>
</section>
<section>
<div class="bg-white shadow-md">
<div>
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Najbliższe dni wolne
</h2>
</div>
<div class="border-t border-gray-200 px-4 pb-5 sm:px-6">
<ul class="divide-y divide-gray-200">
<li
v-for="holiday in holidays.data"
:key="holiday.id.id"
class="py-4 flex"
>
<div>
<p class="text-sm font-medium text-gray-900">
{{ holiday.name }}
</p>
<p class="text-sm text-gray-500">
{{ holiday.displayDate }}
</p>
</div>
</li>
</ul>
</div>
<div class="mt-6">
<InertiaLink
href="#"
class="w-full flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
View all
</InertiaLink>
<div>
<InertiaLink
href="/holidays"
class="w-full flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Zobacz wszystkie
</InertiaLink>
</div>
</div>
</div>
</div>
@ -245,129 +223,41 @@
</template>
<script>
import {
AcademicCapIcon,
BadgeCheckIcon,
CashIcon,
ClockIcon,
ReceiptRefundIcon,
UsersIcon,
} from '@heroicons/vue/outline'
import {computed} from 'vue'
import {usePage} from '@inertiajs/inertia-vue3'
import Status from '@/Shared/Status'
import VacationChart from '@/Shared/VacationChart'
export default {
name: 'DashboardPage',
components: {Status, VacationChart},
props: {
absences: {
type: Object,
default: null,
},
vacationRequests: {
type: Object,
default: null,
},
holidays: {
type: Object,
default: null,
},
stats: {
type: Object,
default: () => ({
used: 0,
pending: 0,
remaining: 0,
}),
},
},
setup() {
const user = computed(() => usePage().props.value.auth.user)
const stats = [
{label: 'Vacation days left', value: 12},
{label: 'Sick days left', value: 4},
{label: 'Personal days left', value: 2},
]
const actions = [
{
icon: ClockIcon,
name: 'Request time off',
href: '#',
iconForeground: 'text-teal-700',
iconBackground: 'bg-teal-50',
},
{
icon: BadgeCheckIcon,
name: 'Benefits',
href: '#',
iconForeground: 'text-purple-700',
iconBackground: 'bg-purple-50',
},
{
icon: UsersIcon,
name: 'Schedule a one-on-one',
href: '#',
iconForeground: 'text-sky-700',
iconBackground: 'bg-sky-50',
},
{
icon: CashIcon,
name: 'Payroll',
href: '#',
iconForeground: 'text-yellow-700',
iconBackground: 'bg-yellow-50',
},
{
icon: ReceiptRefundIcon,
name: 'Submit an expense',
href: '#',
iconForeground: 'text-rose-700',
iconBackground: 'bg-rose-50',
},
{
icon: AcademicCapIcon,
name: 'Training',
href: '#',
iconForeground: 'text-indigo-700',
iconBackground: 'bg-indigo-50',
},
]
const recentHires = [
{
name: 'Leonard Krasner',
handle: 'leonardkrasner',
imageUrl:
'https://images.unsplash.com/photo-1519345182560-3f2917c472ef?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
href: '#',
},
{
name: 'Floyd Miles',
handle: 'floydmiles',
imageUrl:
'https://images.unsplash.com/photo-1463453091185-61582044d556?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
href: '#',
},
{
name: 'Emily Selman',
handle: 'emilyselman',
imageUrl:
'https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
href: '#',
},
{
name: 'Kristin Watson',
handle: 'kristinwatson',
imageUrl:
'https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
href: '#',
},
]
const announcements = [
{
id: 1,
title: 'Office closed on July 2nd',
href: '#',
preview:
'Cum qui rem deleniti. Suscipit in dolor veritatis sequi aut. Vero ut earum quis deleniti. Ut a sunt eum cum ut repudiandae possimus. Nihil ex tempora neque cum consectetur dolores.',
},
{
id: 2,
title: 'New password policy',
href: '#',
preview:
'Alias inventore ut autem optio voluptas et repellendus. Facere totam quaerat quam quo laudantium cumque eaque excepturi vel. Accusamus maxime ipsam reprehenderit rerum id repellendus rerum. Culpa cum vel natus. Est sit autem mollitia.',
},
{
id: 3,
title: 'Office closed on July 2nd',
href: '#',
preview:
'Tenetur libero voluptatem rerum occaecati qui est molestiae exercitationem. Voluptate quisquam iure assumenda consequatur ex et recusandae. Alias consectetur voluptatibus. Accusamus a ab dicta et. Consequatur quis dignissimos voluptatem nisi.',
},
]
return {
user,
stats,
actions,
recentHires,
announcements,
}
},
}

View File

@ -11,119 +11,128 @@
</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"
<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"
>
<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']"
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"
>
Pozostałe dni z poprzedniego roku
</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"
>
<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`] }"
<img
class="h-10 w-10 rounded-full"
:src="item.user.avatar"
alt=""
>
<p
v-if="form.errors[`items.${index}.days`]"
class="mt-2 text-sm text-red-600"
>
{{ form.errors[`items.${index}.days`] }}
</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>
</td>
</tr>
<tr
v-if="!form.items.length"
>
<td
colspan="100%"
class="text-center py-4 text-xl leading-5 text-gray-700"
</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']"
>
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"
<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 w-full max-w-lg bg-gray-50 border border-gray-300 rounded-md px-4 py-2 inline-flex items-center text-gray-500 sm:text-sm">
{{ item.remainingLastYear }}
</div>
</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"
>
Zapisz
</button>
</div>
</form>
</div>
<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>
</template>
@ -149,7 +158,7 @@ export default {
},
setup(props) {
const form = useForm({
items: props.limits.data,
items: props.limits,
})
return {

View File

@ -1,277 +1,324 @@
<template>
<InertiaHead title="Złóż wniosek urlopowy" />
<div class="bg-white shadow-md">
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Złóż wniosek urlopowy
</h2>
</div>
<form
class="border-t border-gray-200 px-6"
@submit.prevent="createForm"
>
<div
v-if="form.errors.vacationRequest"
class="rounded-md bg-red-50 p-4 mt-2"
<div class="grid grid-cols-1 gap-4 items-start lg:grid-cols-3 lg:gap-8">
<div class="lg:col-span-2 h-full bg-white shadow-md flex flex-col">
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Złóż wniosek urlopowy
</h2>
</div>
<form
class="border-t border-gray-200 h-full px-6"
@submit.prevent="createForm"
>
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-400" />
<div class="h-full flex flex-col justify-around">
<div>
<div
v-if="form.errors.vacationRequest"
class="rounded-md bg-red-50 p-4 mt-2"
>
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
Wniosek nie mógł zostać utworzony
</h3>
<div class="mt-2 text-sm text-red-700">
<span>{{ form.errors.vacationRequest }}</span>
</div>
</div>
</div>
</div>
<Listbox
v-if="can.createOnBehalfOfEmployee"
v-model="form.user"
as="div"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<ListboxLabel class="block text-sm font-medium text-gray-700">
Osoba składająca wniosek
</ListboxLabel>
<div class="mt-1 relative sm:mt-0 sm:col-span-2">
<ListboxButton
class="bg-white relative w-full max-w-lg border rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default sm:text-sm focus:ring-1"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.type, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.type }"
>
<span class="flex items-center">
<img
:src="form.user.avatar"
class="flex-shrink-0 h-6 w-6 rounded-full"
>
<span class="ml-3 block truncate">{{ form.user.name }}</span>
</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon class="h-5 w-5 text-gray-400" />
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 w-full max-w-lg bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="user in users.data"
:key="user.id"
v-slot="{ active, selected }"
as="template"
:value="user"
>
<li :class="[active ? 'text-white bg-blumilk-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']">
<div class="flex items-center">
<img
:src="user.avatar"
alt=""
class="flex-shrink-0 h-6 w-6 rounded-full"
>
<span :class="[selected ? 'font-semibold' : 'font-normal', 'ml-3 block truncate']">
{{ user.name }}
</span>
</div>
<span
v-if="selected"
:class="[active ? 'text-white' : 'text-blumilk-600', 'absolute inset-y-0 right-0 flex items-center pr-4']"
>
<CheckIcon class="h-5 w-5" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
<p
v-if="form.errors.type"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.type }}
</p>
</div>
</Listbox>
<div
v-else
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<label
for="date_from"
class="block text-sm font-medium text-gray-700 sm:mt-px"
>
Osoba składająca wniosek
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<div class="flex justify-start items-center">
<span class="inline-flex items-center justify-center h-10 w-10 rounded-full">
<img
class="h-10 w-10 rounded-full"
:src="auth.user.avatar"
>
</span>
<div class="ml-3">
<div class="text-sm font-medium text-gray-900">
{{ auth.user.name }}
</div>
</div>
</div>
</div>
</div>
<Listbox
v-model="form.type"
as="div"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<ListboxLabel class="block text-sm font-medium text-gray-700">
Rodzaj wniosku
</ListboxLabel>
<div class="mt-1 relative sm:mt-0 sm:col-span-2">
<ListboxButton
class="bg-white relative w-full max-w-lg border rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default sm:text-sm focus:ring-1"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.type, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.type }"
>
<span class="block truncate">{{ form.type.label }}</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon class="h-5 w-5 text-gray-400" />
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 w-full max-w-lg bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="type in vacationTypes"
:key="type.value"
v-slot="{ active, selected }"
as="template"
:value="type"
>
<li
:class="[active ? 'text-white bg-blumilk-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']"
>
<span :class="[selected ? 'font-semibold' : 'font-normal', 'block truncate']">
{{ type.label }}
</span>
<span
v-if="selected"
:class="[active ? 'text-white' : 'text-blumilk-600', 'absolute inset-y-0 right-0 flex items-center pr-4']"
>
<CheckIcon class="h-5 w-5" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
<p
v-if="form.errors.type"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.type }}
</p>
</div>
</Listbox>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<label
for="date_from"
class="block text-sm font-medium text-gray-700 sm:mt-px"
>
Planowany urlop od
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<FlatPickr
id="date_from"
v-model="form.from"
:config="fromInputConfig"
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.from, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.from }"
@on-change="onFromChange"
/>
<p
v-if="form.errors.from"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.from }}
</p>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<label
for="date_from"
class="block text-sm font-medium text-gray-700 sm:mt-px"
>
Planowany urlop do
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<FlatPickr
id="date_to"
v-model="form.to"
:config="toInputConfig"
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.to, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.to }"
@on-change="onToChange"
/>
<p
v-if="form.errors.to"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.to }}
</p>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<span class="block text-sm font-medium text-gray-700 sm:mt-px">Liczba dni urlopu</span>
<div
class="mt-1 sm:mt-0 sm:col-span-2 w-full max-w-lg bg-gray-50 border border-gray-300 rounded-md px-4 py-2 inline-flex items-center text-gray-500 sm:text-sm"
>
{{ estimatedDays.length }}
</div>
</div>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<label
for="comment"
class="block text-sm font-medium text-gray-700"
>
Komentarz
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<textarea
id="comment"
v-model="form.comment"
rows="4"
class="shadow-sm focus:ring-blumilk-500 focus:border-blumilk-500 block w-full max-w-lg sm:text-sm border-gray-300 rounded-md"
/>
</div>
</div>
<div
v-if="can.skipFlow"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<label
for="flowSkipped"
class="block text-sm font-medium text-gray-700"
>
Natychmiastowo zatwierdź wniosek
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<Switch
id="flowSkipped"
v-model="form.flowSkipped"
:class="[form.flowSkipped ? '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="[form.flowSkipped ? '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>
</div>
</div>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
Wniosek nie mógł zostać utworzony
</h3>
<div class="mt-2 text-sm text-red-700">
<span>{{ form.errors.vacationRequest }}</span>
<div class="flex justify-end py-3">
<div class="space-x-3">
<InertiaLink
href="/vacation-requests"
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>
</div>
</form>
</div>
<div class="bg-white shadow-md h-full">
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
<span v-if="auth.user.id !== form.user.id">
Urlop wypoczynkowy, dane dla: {{ form.user.name }}
</span>
<span v-else>
Twoje dane o urlopie wypoczynkowym
</span>
</h2>
</div>
<Listbox
v-if="can.createOnBehalfOfEmployee"
v-model="form.user"
as="div"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<ListboxLabel class="block text-sm font-medium text-gray-700">
Osoba składająca wniosek
</ListboxLabel>
<div class="mt-1 relative sm:mt-0 sm:col-span-2">
<ListboxButton
class="bg-white relative w-full max-w-lg border rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default sm:text-sm focus:ring-1"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.type, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.type }"
>
<span class="flex items-center">
<img
:src="form.user.avatar"
class="flex-shrink-0 h-6 w-6 rounded-full"
>
<span class="ml-3 block truncate">{{ form.user.name }}</span>
</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon class="h-5 w-5 text-gray-400" />
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 w-full max-w-lg bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="user in users.data"
:key="user.id"
v-slot="{ active, selected }"
as="template"
:value="user"
>
<li :class="[active ? 'text-white bg-blumilk-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']">
<div class="flex items-center">
<img
:src="user.avatar"
alt=""
class="flex-shrink-0 h-6 w-6 rounded-full"
>
<span :class="[selected ? 'font-semibold' : 'font-normal', 'ml-3 block truncate']">
{{ user.name }}
</span>
</div>
<span
v-if="selected"
:class="[active ? 'text-white' : 'text-blumilk-600', 'absolute inset-y-0 right-0 flex items-center pr-4']"
>
<CheckIcon class="h-5 w-5" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
<p
v-if="form.errors.type"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.type }}
</p>
</div>
</Listbox>
<Listbox
v-model="form.type"
as="div"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<ListboxLabel class="block text-sm font-medium text-gray-700">
Rodzaj wniosku
</ListboxLabel>
<div class="mt-1 relative sm:mt-0 sm:col-span-2">
<ListboxButton
class="bg-white relative w-full max-w-lg border rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default sm:text-sm focus:ring-1"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.type, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.type }"
>
<span class="block truncate">{{ form.type.label }}</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon class="h-5 w-5 text-gray-400" />
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 w-full max-w-lg bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="type in vacationTypes"
:key="type.value"
v-slot="{ active, selected }"
as="template"
:value="type"
>
<li
:class="[active ? 'text-white bg-blumilk-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']"
>
<span :class="[selected ? 'font-semibold' : 'font-normal', 'block truncate']">
{{ type.label }}
</span>
<span
v-if="selected"
:class="[active ? 'text-white' : 'text-blumilk-600', 'absolute inset-y-0 right-0 flex items-center pr-4']"
>
<CheckIcon class="h-5 w-5" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
<p
v-if="form.errors.type"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.type }}
</p>
</div>
</Listbox>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<label
for="date_from"
class="block text-sm font-medium text-gray-700 sm:mt-px"
>
Planowany urlop od
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<FlatPickr
id="date_from"
v-model="form.from"
:config="fromInputConfig"
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.from, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.from }"
@on-change="onFromChange"
/>
<p
v-if="form.errors.from"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.from }}
</p>
</div>
<div class="border-t border-gray-200 px-6 pt-8">
<VacationChart :stats="stats" />
</div>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<label
for="date_from"
class="block text-sm font-medium text-gray-700 sm:mt-px"
>
Planowany urlop do
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<FlatPickr
id="date_to"
v-model="form.to"
:config="toInputConfig"
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.to, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.to }"
@on-change="onToChange"
/>
<p
v-if="form.errors.to"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.to }}
</p>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<span class="block text-sm font-medium text-gray-700 sm:mt-px">Liczba dni urlopu</span>
<div
class="mt-1 sm:mt-0 sm:col-span-2 w-full max-w-lg bg-gray-50 border border-gray-300 rounded-md px-4 py-2 inline-flex items-center text-gray-500 sm:text-sm"
>
{{ estimatedDays.length }}
</div>
</div>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<label
for="comment"
class="block text-sm font-medium text-gray-700"
>
Komentarz
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<textarea
id="comment"
v-model="form.comment"
rows="4"
class="shadow-sm focus:ring-blumilk-500 focus:border-blumilk-500 block w-full max-w-lg sm:text-sm border-gray-300 rounded-md"
/>
</div>
</div>
<div
v-if="can.skipFlow"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<label
for="flowSkipped"
class="block text-sm font-medium text-gray-700"
>
Natychmiastowo zatwierdź wniosek
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<Switch
id="flowSkipped"
v-model="form.flowSkipped"
:class="[form.flowSkipped ? '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="[form.flowSkipped ? '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>
</div>
</div>
<div class="flex justify-end py-3">
<div class="space-x-3">
<InertiaLink
href="/vacation-requests"
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>
</div>
</template>
@ -280,13 +327,15 @@ import {useForm} from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component'
import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions, Switch} from '@headlessui/vue'
import {CheckIcon, SelectorIcon, XCircleIcon} from '@heroicons/vue/solid'
import {reactive, ref} from 'vue'
import {reactive, ref, watch} from 'vue'
import axios from 'axios'
import useCurrentYearPeriodInfo from '@/Composables/yearPeriodInfo'
import VacationChart from '@/Shared/VacationChart'
export default {
name: 'VacationRequestCreate',
components: {
VacationChart,
Switch,
FlatPickr,
Listbox,
@ -333,6 +382,13 @@ export default {
})
const estimatedDays = ref([])
const stats = ref({
used: 0,
pending: 0,
remaining: 0,
})
const {minDate, maxDate} = useCurrentYearPeriodInfo()
const disableDates = [
@ -351,9 +407,15 @@ export default {
disable: disableDates,
})
watch(() => form.user, user => {
axios.post('/api/calculate-vacations-stats', {user: user.id})
.then(res => stats.value = res.data)
}, {immediate: true})
return {
form,
estimatedDays,
stats,
fromInputConfig,
toInputConfig,
}
@ -383,6 +445,5 @@ export default {
}
},
},
}
</script>

View File

@ -2,7 +2,7 @@
<div class="min-h-full">
<MainMenu />
<main class="lg:ml-64 flex flex-col flex-1 py-8">
<div>
<div class="px-4">
<slot />
</div>
</main>

View File

@ -110,7 +110,7 @@
:class="[$page.url === '/' ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md']"
>
<HomeIcon class="mr-4 flex-shrink-0 h-6 w-6 text-blumilk-200" />
Dashboard
Strona główna
</InertiaLink>
</div>
<div class="mt-4 pt-4">
@ -337,8 +337,6 @@ export default {
].filter(item => item.can))
const userNavigation = [
{name: 'Your Profile', href: '#'},
{name: 'Settings', href: '#'},
{name: 'Wyloguj się', href: '/logout', method: 'post', as: 'button'},
]

View File

@ -0,0 +1,93 @@
<template>
<v-chart
style="height: 600px;"
:autoresize="true"
:option="option"
/>
</template>
<script>
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
} from 'echarts/components'
import VChart from 'vue-echarts'
import {computed} from 'vue'
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent,
])
export default {
name: 'VacationChart',
components: {
VChart,
},
props: {
stats: {
type: Object,
default: () => ({
used: 0,
pending: 0,
remaining: 0,
}),
},
},
setup(props) {
const option = computed(() => ({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
color: [
'#2C466F',
'#AABDDD',
'#527ABA',
],
legend: {
orient: 'vertical',
left: 'left',
data: ['Wykorzystane', 'Rozpatrywane', 'Pozostałe'],
},
series: [
{
name: 'Urlop wypoczynkowy',
type: 'pie',
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
position: 'inner',
formatter: param => param.value !== 0 ? param.value : '' ,
fontWeight: 'bold',
fontSize: 16,
color: '#FFFFFF',
labelLine: {
show: false,
},
},
data: [
{ value: props.stats.used, name: 'Wykorzystane' },
{ value: props.stats.pending, name: 'Rozpatrywane' },
{ value: props.stats.remaining, name: 'Pozostałe' },
],
radius: ['30%', '70%'],
},
],
}))
return { option }
},
}
</script>

View File

@ -3,8 +3,10 @@
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Toby\Infrastructure\Http\Controllers\Api\CalculateUserVacationStatsController;
use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController;
Route::middleware("auth:sanctum")->group(function (): void {
Route::post("calculate-vacation-days", CalculateVacationDaysController::class);
Route::post("calculate-vacations-stats", CalculateUserVacationStatsController::class);
});

View File

@ -3,6 +3,7 @@
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Toby\Infrastructure\Http\Controllers\DashboardController;
use Toby\Infrastructure\Http\Controllers\GoogleController;
use Toby\Infrastructure\Http\Controllers\HolidayController;
use Toby\Infrastructure\Http\Controllers\LogoutController;
@ -14,7 +15,7 @@ use Toby\Infrastructure\Http\Controllers\VacationLimitController;
use Toby\Infrastructure\Http\Controllers\VacationRequestController;
Route::middleware("auth")->group(function (): void {
Route::get("/", fn() => inertia("Dashboard"))
Route::get("/", DashboardController::class)
->name("dashboard");
Route::post("/logout", LogoutController::class);

View File

@ -26,7 +26,7 @@ class VacationLimitTest extends FeatureTestCase
->assertInertia(
fn(Assert $page) => $page
->component("VacationLimits")
->has("limits.data", 10),
->has("limits", 10),
);
}