#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
parent 98a93cb45f
commit 692ccc6ba8
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\Carbon;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Toby\Eloquent\Models\Holiday;
use Toby\Eloquent\Models\VacationLimit;
use Toby\Eloquent\Scopes\SelectedYearPeriodScope;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -16,10 +13,5 @@ class AppServiceProvider extends ServiceProvider
{ {
Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y")); Carbon::macro("toDisplayString", fn() => $this->translatedFormat("j F Y"));
Carbon::macro("toDisplayDate", fn() => $this->translatedFormat("d.m.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( public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever, protected YearPeriodRetriever $yearPeriodRetriever,
) { ) {}
}
public function generate(Carbon $month): array public function generate(Carbon $month): array
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,8 +41,7 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
public function __construct( public function __construct(
protected User $user, protected User $user,
protected Carbon $month, protected Carbon $month,
) { ) {}
}
public function title(): string 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( public function __construct(
protected Auth $auth, protected Auth $auth,
protected Dispatcher $dispatcher, protected Dispatcher $dispatcher,
) { ) {}
}
public function markAsCreated(VacationRequest $vacationRequest, ?User $user = null): void public function markAsCreated(VacationRequest $vacationRequest, ?User $user = null): void
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,7 @@ class SelectedYearPeriodScope implements Scope
{ {
public function __construct( public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever, protected YearPeriodRetriever $yearPeriodRetriever,
) { ) {}
}
public function apply(Builder $builder, Model $model): Builder 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\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Response; use Inertia\Response;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\Holiday; use Toby\Eloquent\Models\Holiday;
use Toby\Infrastructure\Http\Requests\HolidayRequest; use Toby\Infrastructure\Http\Requests\HolidayRequest;
use Toby\Infrastructure\Http\Resources\HolidayFormDataResource; use Toby\Infrastructure\Http\Resources\HolidayFormDataResource;
@ -15,9 +16,12 @@ use Toby\Infrastructure\Http\Resources\HolidayResource;
class HolidayController extends Controller 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") ->orderBy("date")
->get(); ->get();

View File

@ -6,24 +6,41 @@ namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Inertia\Response; use Inertia\Response;
use Toby\Domain\UserVacationStatsRetriever;
use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\VacationLimit; use Toby\Eloquent\Models\VacationLimit;
use Toby\Eloquent\Models\YearPeriod;
use Toby\Infrastructure\Http\Requests\VacationLimitRequest; use Toby\Infrastructure\Http\Requests\VacationLimitRequest;
use Toby\Infrastructure\Http\Resources\VacationLimitResource; use Toby\Infrastructure\Http\Resources\UserResource;
class VacationLimitController extends Controller class VacationLimitController extends Controller
{ {
public function edit(): Response public function edit(YearPeriodRetriever $yearPeriodRetriever, UserVacationStatsRetriever $statsRetriever): Response
{ {
$this->authorize("manageVacationLimits"); $this->authorize("manageVacationLimits");
$limits = VacationLimit::query() $yearPeriod = $yearPeriodRetriever->selected();
$previousYearPeriod = YearPeriod::findByYear($yearPeriod->year - 1);
$limits = $yearPeriod
->vacationLimits()
->with("user") ->with("user")
->orderByUserField("last_name") ->orderByUserField("last_name")
->orderByUserField("first_name") ->orderByUserField("first_name")
->get(); ->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", [ 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 Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response as LaravelResponse; use Illuminate\Http\Response as LaravelResponse;
@ -88,9 +89,15 @@ class VacationRequestController extends Controller
return $pdf->stream(); return $pdf->stream();
} }
public function create(Request $request): Response public function create(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response
{ {
$yearPeriod = $yearPeriodRetriever->selected();
$users = User::query() $users = User::query()
->whereRelation(
"vacationlimits",
fn(Builder $query) => $query->where("year_period_id", $yearPeriod->id)->whereNotNull("days"),
)
->orderBy("last_name") ->orderBy("last_name")
->orderBy("first_name") ->orderBy("first_name")
->get(); ->get();

View File

@ -13,8 +13,7 @@ class HandleInertiaRequests extends Middleware
{ {
public function __construct( public function __construct(
protected YearPeriodRetriever $yearPeriodRetriever, protected YearPeriodRetriever $yearPeriodRetriever,
) { ) {}
}
public function share(Request $request): array 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; use Illuminate\Http\Resources\Json\JsonResource;
class VacationLimitResource extends JsonResource class AbsenceResource extends JsonResource
{ {
public static $wrap = null; public static $wrap = null;
@ -15,8 +15,7 @@ class VacationLimitResource extends JsonResource
return [ return [
"id" => $this->id, "id" => $this->id,
"user" => new UserResource($this->user), "user" => new UserResource($this->user),
"hasVacation" => $this->hasVacation(), "date" => $this->date->toDisplayString(),
"days" => $this->days,
]; ];
} }
} }

View File

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

View File

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

View File

@ -23,7 +23,7 @@
"maatwebsite/excel": "^3.1" "maatwebsite/excel": "^3.1"
}, },
"require-dev": { "require-dev": {
"blumilksoftware/codestyle": "^0.9.0", "blumilksoftware/codestyle": "^0.10.0",
"spatie/laravel-ignition": "^1.0", "spatie/laravel-ignition": "^1.0",
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"laravel/dusk": "^6.21", "laravel/dusk": "^6.21",
@ -47,6 +47,11 @@
"ecs": "./vendor/bin/ecs check --clear-cache", "ecs": "./vendor/bin/ecs check --clear-cache",
"ecsf": "./vendor/bin/ecs check --clear-cache --fix", "ecsf": "./vendor/bin/ecs check --clear-cache --fix",
"test": "@php artisan test", "test": "@php artisan test",
"fresh": "@php artisan migrate:fresh",
"fresh:demo": [
"composer fresh",
"@php artisan db:seed --class=DemoSeeder"
],
"post-autoload-dump": [ "post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi" "@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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c090431972dc8bfbe198ce9fc9f816f3", "content-hash": "d3f019c6e743a3249af78baefe7acb01",
"packages": [ "packages": [
{ {
"name": "asm89/stack-cors", "name": "asm89/stack-cors",
@ -7468,21 +7468,22 @@
"packages-dev": [ "packages-dev": [
{ {
"name": "blumilksoftware/codestyle", "name": "blumilksoftware/codestyle",
"version": "v0.9.0", "version": "v0.10.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/blumilksoftware/codestyle.git", "url": "https://github.com/blumilksoftware/codestyle.git",
"reference": "c1e06d9df22adaf1f0ae646dfbc0620b28e9a79a" "reference": "39da9ec4922e633e8f0bd065f244365217e9d00a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/blumilksoftware/codestyle/zipball/c1e06d9df22adaf1f0ae646dfbc0620b28e9a79a", "url": "https://api.github.com/repos/blumilksoftware/codestyle/zipball/39da9ec4922e633e8f0bd065f244365217e9d00a",
"reference": "c1e06d9df22adaf1f0ae646dfbc0620b28e9a79a", "reference": "39da9ec4922e633e8f0bd065f244365217e9d00a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"kubawerlos/php-cs-fixer-custom-fixers": "^3.7",
"php": "^8.0", "php": "^8.0",
"symplify/easy-coding-standard": "10.0.8" "symplify/easy-coding-standard": "10.1.0"
}, },
"require-dev": { "require-dev": {
"composer/composer": "2.*", "composer/composer": "2.*",
@ -7509,9 +7510,299 @@
"description": "Blumilk codestyle configurator", "description": "Blumilk codestyle configurator",
"support": { "support": {
"issues": "https://github.com/blumilksoftware/codestyle/issues", "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", "name": "doctrine/instantiator",
@ -7720,6 +8011,95 @@
], ],
"time": "2022-01-07T12:00:00+00:00" "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", "name": "hamcrest/hamcrest-php",
"version": "v2.0.1", "version": "v2.0.1",
@ -7771,6 +8151,53 @@
}, },
"time": "2020-07-09T08:09:16+00:00" "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", "name": "laravel/dusk",
"version": "v6.22.1", "version": "v6.22.1",
@ -8172,6 +8599,58 @@
}, },
"time": "2022-02-21T01:04:05+00:00" "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", "name": "php-webdriver/webdriver",
"version": "1.12.0", "version": "1.12.0",
@ -9977,16 +10456,16 @@
}, },
{ {
"name": "spatie/ignition", "name": "spatie/ignition",
"version": "1.0.5", "version": "1.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/spatie/ignition.git", "url": "https://github.com/spatie/ignition.git",
"reference": "6b7bb804f4834b080f5ac941f6ac6800a485011e" "reference": "8ecde033600064e3ffdbf804deec0dcb05004387"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/spatie/ignition/zipball/6b7bb804f4834b080f5ac941f6ac6800a485011e", "url": "https://api.github.com/repos/spatie/ignition/zipball/8ecde033600064e3ffdbf804deec0dcb05004387",
"reference": "6b7bb804f4834b080f5ac941f6ac6800a485011e", "reference": "8ecde033600064e3ffdbf804deec0dcb05004387",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -10044,7 +10523,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-02-17T21:40:47+00:00" "time": "2022-03-01T17:01:33+00:00"
}, },
{ {
"name": "spatie/laravel-ignition", "name": "spatie/laravel-ignition",
@ -10134,17 +10613,209 @@
"time": "2022-02-15T11:02:15+00:00" "time": "2022-02-15T11:02:15+00:00"
}, },
{ {
"name": "symplify/easy-coding-standard", "name": "symfony/filesystem",
"version": "10.0.8", "version": "v6.0.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symplify/easy-coding-standard.git", "url": "https://github.com/symfony/filesystem.git",
"reference": "12ef3b0f500d485e6313926e5f2900a74c578394" "reference": "6646c13f787057d64701a3a0235cf9567c6ccbbd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symplify/easy-coding-standard/zipball/12ef3b0f500d485e6313926e5f2900a74c578394", "url": "https://api.github.com/repos/symfony/filesystem/zipball/6646c13f787057d64701a3a0235cf9567c6ccbbd",
"reference": "12ef3b0f500d485e6313926e5f2900a74c578394", "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": "" "shasum": ""
}, },
"require": { "require": {
@ -10174,7 +10845,7 @@
], ],
"description": "Prefixed scoped version of ECS package", "description": "Prefixed scoped version of ECS package",
"support": { "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": [ "funding": [
{ {
@ -10186,7 +10857,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2021-12-31T12:19:54+00:00" "time": "2022-02-21T11:18:44+00:00"
}, },
{ {
"name": "theseer/tokenizer", "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( public function __construct(
protected UserAvatarGenerator $avatarGenerator, protected UserAvatarGenerator $avatarGenerator,
) { ) {}
}
public function run(): void 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" "lintf": "./node_modules/.bin/eslint resources/js --ext .js,.vue --fix"
}, },
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.4.2", "@headlessui/vue": "^1.5.0",
"@heroicons/vue": "^1.0.5", "@heroicons/vue": "^1.0.5",
"@inertiajs/inertia": "^0.11.0", "@inertiajs/inertia": "^0.11.0",
"@inertiajs/inertia-vue3": "^0.6.0", "@inertiajs/inertia-vue3": "^0.6.0",
"@inertiajs/progress": "^0.2.7", "@inertiajs/progress": "^0.2.7",
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
"@tailwindcss/line-clamp": "^0.3.1", "@tailwindcss/line-clamp": "^0.3.1",
"@tailwindcss/typography": "^0.5.0", "@tailwindcss/typography": "^0.5.2",
"@vue/compiler-sfc": "^3.2.26", "@vue/compiler-sfc": "^3.2.31",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"axios": "^0.25.0", "axios": "^0.26.0",
"echarts": "^5.2.2", "echarts": "^5.3.0",
"eslit": "^6.0.0",
"flatpickr": "^4.6.9", "flatpickr": "^4.6.9",
"laravel-mix": "^6.0.6", "laravel-mix": "^6.0.43",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"postcss": "^8.4.5", "postcss": "^8.4.7",
"tailwindcss": "^3.0.13", "tailwindcss": "^3.0.23",
"vue": "^3.2.26", "vue": "^3.2.31",
"vue-echarts": "^6.0.2", "vue-echarts": "^6.0.2",
"vue-flatpickr-component": "^9.0.5", "vue-flatpickr-component": "^9.0.5",
"vue-loader": "^17.0.0", "vue-loader": "^17.0.0",
"vue-toastification": "^2.0.0-rc.5" "vue-toastification": "^2.0.0-rc.5"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.6.0", "eslint": "^8.10.0",
"eslint-plugin-vue": "^8.2.0" "eslint-plugin-vue": "^8.5.0"
} }
} }

View File

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

View File

@ -1,17 +1,9 @@
<template> <template>
<InertiaHead title="Strona główna" /> <InertiaHead title="Strona główna" />
<div class="grid grid-cols-1 gap-4 items-start lg:grid-cols-3 lg:gap-8"> <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"> <div class="grid grid-cols-1 gap-4 lg:col-span-2">
<!-- Welcome panel --> <section>
<section aria-labelledby="profile-overview-title"> <div class=" bg-white overflow-hidden shadow">
<div class="rounded-lg bg-white overflow-hidden shadow">
<h2
id="profile-overview-title"
class="sr-only"
>
Profile Overview
</h2>
<div class="bg-white p-6"> <div class="bg-white p-6">
<div class="sm:flex sm:items-center sm:justify-between"> <div class="sm:flex sm:items-center sm:justify-between">
<div class="sm:flex sm:space-x-5"> <div class="sm:flex sm:space-x-5">
@ -24,7 +16,7 @@
</div> </div>
<div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left"> <div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left">
<p class="text-sm font-medium text-gray-600"> <p class="text-sm font-medium text-gray-600">
Welcome back, Cześć,
</p> </p>
<p class="text-xl font-bold text-gray-900 sm:text-2xl"> <p class="text-xl font-bold text-gray-900 sm:text-2xl">
{{ user.name }} {{ user.name }}
@ -34,136 +26,109 @@
</p> </p>
</div> </div>
</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> </div>
</div> </div>
</section> </section>
<section>
<!-- Actions panel --> <div class="grid grid-cols-2 gap-4">
<section aria-labelledby="quick-links-title"> <div class="bg-white shadow-md p-4">
<div <VacationChart :stats="stats" />
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" </div>
> <div class="h-full">
<h2 <div class="grid grid-cols-2 gap-4 h-full">
id="quick-links-title" <div class="px-4 py-5 bg-white shadow-md sm:p-6">
class="sr-only" <dd class="mt-1 text-4xl font-semibold text-blumilk-500">
> {{ stats.remaining }}
Quick links </dd>
</h2> <dt class="text-md font-medium text-gray-700 truncate">
<div Pozostało
v-for="(action, actionIdx) in actions" </dt>
:key="action.name" <dt class="text-sm font-medium text-gray-500 mt-2">
: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']" Dni do wykorzystania teraz.
> </dt>
<div> </div>
<span <div class="px-4 py-5 bg-white shadow-md sm:p-6">
:class="[action.iconBackground, action.iconForeground, 'rounded-lg inline-flex p-3 ring-4 ring-white']" <dd class="mt-1 text-4xl font-semibold text-blumilk-700">
> {{ stats.used }}
<component </dd>
:is="action.icon" <dt class="text-md font-medium text-gray-700 truncate">
class="h-6 w-6" Dni wykorzystane
aria-hidden="true" </dt>
/> <dt class="text-sm font-medium text-gray-500 mt-2">
</span> 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> </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>
</div> </div>
</section> </section>
</div> </div>
<!-- Right column -->
<div class="grid grid-cols-1 gap-4"> <div class="grid grid-cols-1 gap-4">
<!-- Announcements --> <section>
<section aria-labelledby="announcements-title"> <div class="bg-white shadow-md">
<div class="rounded-lg bg-white overflow-hidden shadow"> <div class="p-4 sm:px-6">
<div class="p-6"> <h2 class="text-lg leading-6 font-medium text-gray-900">
<h2 Twoje wnioski
id="announcements-title"
class="text-base font-medium text-gray-900"
>
Announcements
</h2> </h2>
</div>
<div class="border-t border-gray-200 pb-5 px-4 sm:px-6">
<div class="flow-root mt-6"> <div class="flow-root mt-6">
<ul <ul class="-my-5 divide-y divide-gray-200">
role="list"
class="-my-5 divide-y divide-gray-200"
>
<li <li
v-for="announcement in announcements" v-for="request in vacationRequests.data"
:key="announcement.id" :key="request.id"
class="py-5" class="py-5"
> >
<div class="relative focus-within:ring-2 focus-within:ring-cyan-500"> <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 <InertiaLink
:href="announcement.href" :href="`/vacation-requests/${request.id}`"
class="hover:underline focus:outline-none" class="hover:underline focus:outline-none"
> >
<!-- Extend touch target to entire panel --> <span class="absolute inset-0" />
<span Wniosek o {{ request.type.toLowerCase() }}
class="absolute inset-0" [{{ request.name }}]
aria-hidden="true"
/>
{{ announcement.title }}
</InertiaLink> </InertiaLink>
</h3> </h3>
<p class="mt-1 text-sm text-gray-600 line-clamp-2"> <p class="mt-1 text-sm text-gray-600">
{{ announcement.preview }} {{ request.from }} - {{ request.to }}
</p>
<p class="mt-2 text-sm text-gray-600">
<Status :status="request.state" />
</p> </p>
</div> </div>
</li> </li>
@ -171,203 +136,128 @@
</div> </div>
<div class="mt-6"> <div class="mt-6">
<InertiaLink <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" 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> </InertiaLink>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section>
<!-- Recent Hires --> <div class="bg-white shadow-md">
<section aria-labelledby="recent-hires-title"> <div class="p-4 sm:px-6">
<div class="rounded-lg bg-white overflow-hidden shadow"> <h2 class="text-lg leading-6 font-medium text-gray-900">
<div class="p-6"> Dzisiejsze nieobecności
<h2
id="recent-hires-title"
class="text-base font-medium text-gray-900"
>
Recent Hires
</h2> </h2>
<div class="flow-root mt-6"> </div>
<ul <div class="border-t border-gray-200 px-4 sm:px-6">
role="list" <ul class="divide-y divide-gray-200">
class="-my-5 divide-y divide-gray-200"
>
<li <li
v-for="person in recentHires" v-for="absence in absences.data"
:key="person.handle" :key="absence.user.id"
class="py-4" class="py-4 flex"
> >
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<img <img
class="h-8 w-8 rounded-full" class="h-10 w-10 rounded-full"
:src="person.imageUrl" :src="absence.user.avatar"
alt=""
> >
</div> <div class="ml-3">
<div class="flex-1 min-w-0"> <p class="text-sm font-medium text-gray-900">
<p class="text-sm font-medium text-gray-900 truncate"> {{ absence.user.name }}
{{ person.name }}
</p> </p>
<p class="text-sm text-gray-500 truncate"> <p class="text-sm text-gray-500">
{{ '@' + person.handle }} {{ absence.user.email }}
</p> </p>
</div> </div>
<div> </li>
<InertiaLink <li v-if="! absences.data.length">
:href="person.href" <p class="py-2">
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" Brak danych
> </p>
View
</InertiaLink>
</div>
</div>
</li> </li>
</ul> </ul>
</div> </div>
<div class="mt-6"> </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>
<InertiaLink <InertiaLink
href="#" 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" 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> </InertiaLink>
</div> </div>
</div> </div>
</div> </div>
</div>
</section> </section>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {
AcademicCapIcon,
BadgeCheckIcon,
CashIcon,
ClockIcon,
ReceiptRefundIcon,
UsersIcon,
} from '@heroicons/vue/outline'
import {computed} from 'vue' import {computed} from 'vue'
import {usePage} from '@inertiajs/inertia-vue3' import {usePage} from '@inertiajs/inertia-vue3'
import Status from '@/Shared/Status'
import VacationChart from '@/Shared/VacationChart'
export default { export default {
name: 'DashboardPage', 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() { setup() {
const user = computed(() => usePage().props.value.auth.user) 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 { return {
user, user,
stats,
actions,
recentHires,
announcements,
} }
}, },
} }

View File

@ -11,7 +11,6 @@
</p> </p>
</div> </div>
</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"> <div class="overflow-x-auto xl:overflow-x-visible overflow-y-auto xl:overflow-y-visible">
<form @submit.prevent="submitVacationDays"> <form @submit.prevent="submitVacationDays">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
@ -35,6 +34,12 @@
> >
Posiada urlop? Posiada urlop?
</th> </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 <th
scope="col" scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider" class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
@ -83,6 +88,11 @@
/> />
</Switch> </Switch>
</td> </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"> <td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="mt-1 sm:mt-0 sm:col-span-2"> <div class="mt-1 sm:mt-0 sm:col-span-2">
<input <input
@ -125,7 +135,6 @@
</form> </form>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -149,7 +158,7 @@ export default {
}, },
setup(props) { setup(props) {
const form = useForm({ const form = useForm({
items: props.limits.data, items: props.limits,
}) })
return { return {

View File

@ -1,15 +1,18 @@
<template> <template>
<InertiaHead title="Złóż wniosek urlopowy" /> <InertiaHead title="Złóż wniosek urlopowy" />
<div class="bg-white shadow-md"> <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"> <div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900"> <h2 class="text-lg leading-6 font-medium text-gray-900">
Złóż wniosek urlopowy Złóż wniosek urlopowy
</h2> </h2>
</div> </div>
<form <form
class="border-t border-gray-200 px-6" class="border-t border-gray-200 h-full px-6"
@submit.prevent="createForm" @submit.prevent="createForm"
> >
<div class="h-full flex flex-col justify-around">
<div>
<div <div
v-if="form.errors.vacationRequest" v-if="form.errors.vacationRequest"
class="rounded-md bg-red-50 p-4 mt-2" class="rounded-md bg-red-50 p-4 mt-2"
@ -99,6 +102,32 @@
</p> </p>
</div> </div>
</Listbox> </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 <Listbox
v-model="form.type" v-model="form.type"
as="div" as="div"
@ -254,6 +283,7 @@
</Switch> </Switch>
</div> </div>
</div> </div>
</div>
<div class="flex justify-end py-3"> <div class="flex justify-end py-3">
<div class="space-x-3"> <div class="space-x-3">
<InertiaLink <InertiaLink
@ -271,8 +301,25 @@
</button> </button>
</div> </div>
</div> </div>
</div>
</form> </form>
</div> </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>
<div class="border-t border-gray-200 px-6 pt-8">
<VacationChart :stats="stats" />
</div>
</div>
</div>
</template> </template>
<script> <script>
@ -280,13 +327,15 @@ import {useForm} from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component' import FlatPickr from 'vue-flatpickr-component'
import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions, Switch} from '@headlessui/vue' import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions, Switch} from '@headlessui/vue'
import {CheckIcon, SelectorIcon, XCircleIcon} from '@heroicons/vue/solid' import {CheckIcon, SelectorIcon, XCircleIcon} from '@heroicons/vue/solid'
import {reactive, ref} from 'vue' import {reactive, ref, watch} from 'vue'
import axios from 'axios' import axios from 'axios'
import useCurrentYearPeriodInfo from '@/Composables/yearPeriodInfo' import useCurrentYearPeriodInfo from '@/Composables/yearPeriodInfo'
import VacationChart from '@/Shared/VacationChart'
export default { export default {
name: 'VacationRequestCreate', name: 'VacationRequestCreate',
components: { components: {
VacationChart,
Switch, Switch,
FlatPickr, FlatPickr,
Listbox, Listbox,
@ -333,6 +382,13 @@ export default {
}) })
const estimatedDays = ref([]) const estimatedDays = ref([])
const stats = ref({
used: 0,
pending: 0,
remaining: 0,
})
const {minDate, maxDate} = useCurrentYearPeriodInfo() const {minDate, maxDate} = useCurrentYearPeriodInfo()
const disableDates = [ const disableDates = [
@ -351,9 +407,15 @@ export default {
disable: disableDates, disable: disableDates,
}) })
watch(() => form.user, user => {
axios.post('/api/calculate-vacations-stats', {user: user.id})
.then(res => stats.value = res.data)
}, {immediate: true})
return { return {
form, form,
estimatedDays, estimatedDays,
stats,
fromInputConfig, fromInputConfig,
toInputConfig, toInputConfig,
} }
@ -383,6 +445,5 @@ export default {
} }
}, },
}, },
} }
</script> </script>

View File

@ -2,7 +2,7 @@
<div class="min-h-full"> <div class="min-h-full">
<MainMenu /> <MainMenu />
<main class="lg:ml-64 flex flex-col flex-1 py-8"> <main class="lg:ml-64 flex flex-col flex-1 py-8">
<div> <div class="px-4">
<slot /> <slot />
</div> </div>
</main> </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']" :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" /> <HomeIcon class="mr-4 flex-shrink-0 h-6 w-6 text-blumilk-200" />
Dashboard Strona główna
</InertiaLink> </InertiaLink>
</div> </div>
<div class="mt-4 pt-4"> <div class="mt-4 pt-4">
@ -337,8 +337,6 @@ export default {
].filter(item => item.can)) ].filter(item => item.can))
const userNavigation = [ const userNavigation = [
{name: 'Your Profile', href: '#'},
{name: 'Settings', href: '#'},
{name: 'Wyloguj się', href: '/logout', method: 'post', as: 'button'}, {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); declare(strict_types=1);
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Toby\Infrastructure\Http\Controllers\Api\CalculateUserVacationStatsController;
use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController; use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController;
Route::middleware("auth:sanctum")->group(function (): void { Route::middleware("auth:sanctum")->group(function (): void {
Route::post("calculate-vacation-days", CalculateVacationDaysController::class); Route::post("calculate-vacation-days", CalculateVacationDaysController::class);
Route::post("calculate-vacations-stats", CalculateUserVacationStatsController::class);
}); });

View File

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

View File

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