Merge branch 'main' into #22-vacation-calendar

# Conflicts:
#	resources/js/Pages/Dashboard.vue
#	resources/js/Shared/Layout/AppLayout.vue
#	resources/js/Shared/MainMenu.vue
#	routes/web.php
This commit is contained in:
Adrian Hopek 2022-02-03 10:31:17 +01:00
commit 47288917a2
69 changed files with 5057 additions and 2864 deletions

View File

@ -1,17 +1,17 @@
module.exports = {
env: {
node: true,
'vue/setup-compiler-macros': true,
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
],
rules: {
semi: [2, 'always'],
semi: [2, 'never'],
quotes: ['error', 'single'],
indent: ['error', 4],
'vue/html-indent': ['error', 4],
'vue/multi-word-component-names': 'off',
indent: ['error', 2],
'vue/html-indent': ['error', 2],
'comma-dangle': ['error', 'always-multiline'],
},
};
}

View File

@ -5,8 +5,24 @@ declare(strict_types=1);
namespace Toby\Architecture\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Domain\Events\VacationRequestStateChanged;
use Toby\Domain\Listeners\CreateVacationRequestActivity;
use Toby\Domain\Listeners\HandleAcceptedByAdministrativeVacationRequest;
use Toby\Domain\Listeners\HandleAcceptedByTechnicalVacationRequest;
use Toby\Domain\Listeners\HandleApprovedVacationRequest;
use Toby\Domain\Listeners\HandleCreatedVacationRequest;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [];
protected $listen = [
VacationRequestStateChanged::class => [CreateVacationRequestActivity::class],
VacationRequestCreated::class => [HandleCreatedVacationRequest::class],
VacationRequestAcceptedByTechnical::class => [HandleAcceptedByTechnicalVacationRequest::class],
VacationRequestAcceptedByAdministrative::class => [HandleAcceptedByAdministrativeVacationRequest::class],
VacationRequestApproved::class => [HandleApprovedVacationRequest::class],
];
}

View File

@ -6,8 +6,10 @@ namespace Toby\Architecture\Providers;
use Illuminate\Support\ServiceProvider;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Eloquent\Models\YearPeriod;
use Toby\Eloquent\Observers\UserObserver;
use Toby\Eloquent\Observers\VacationRequestObserver;
use Toby\Eloquent\Observers\YearPeriodObserver;
class ObserverServiceProvider extends ServiceProvider
@ -16,5 +18,6 @@ class ObserverServiceProvider extends ServiceProvider
{
User::observe(UserObserver::class);
YearPeriod::observe(YearPeriodObserver::class);
VacationRequest::observe(VacationRequestObserver::class);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Toby\Domain;
namespace Toby\Domain\Enums;
enum EmploymentForm: string
{

30
app/Domain/Enums/Role.php Normal file
View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Enums;
enum Role: string
{
case EMPLOYEE = "employee";
case ADMINISTRATOR = "administrator";
case TECHNICAL_APPROVER = "technical_approver";
case ADMINISTRATIVE_APPROVER = "administrative_approver";
public function label(): string
{
return __($this->value);
}
public static function casesToSelect(): array
{
$cases = collect(Role::cases());
return $cases->map(
fn(Role $enum) => [
"label" => $enum->label(),
"value" => $enum->value,
],
)->toArray();
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Enums;
enum VacationRequestState: string
{
case CREATED = "created";
case CANCELED = "canceled";
case REJECTED = "rejected";
case APPROVED = "approved";
case WAITING_FOR_TECHNICAL = "waiting_for_technical";
case WAITING_FOR_ADMINISTRATIVE = "waiting_for_administrative";
case ACCEPTED_BY_TECHNICAL = "accepted_by_technical";
case ACCEPTED_BY_ADMINSTRATIVE = "accepted_by_administrative";
public function label(): string
{
return __($this->value);
}
public static function pendingStates(): array
{
return [
self::CREATED,
self::WAITING_FOR_TECHNICAL,
self::WAITING_FOR_ADMINISTRATIVE,
self::ACCEPTED_BY_TECHNICAL,
self::ACCEPTED_BY_ADMINSTRATIVE,
];
}
public static function successStates(): array
{
return [self::APPROVED];
}
public static function failedStates(): array
{
return [
self::REJECTED,
self::CANCELED,
];
}
public static function filterByStatus(string $filter): array
{
return match ($filter) {
"pending" => VacationRequestState::pendingStates(),
"success" => VacationRequestState::successStates(),
"failed" => VacationRequestState::failedStates(),
default => VacationRequestState::cases(),
};
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Enums;
enum VacationType: string
{
case VACATION = "vacation";
case VACATION_ON_REQUEST = "vacation_on_request";
case SPECIAL_VACATION = "special_vacation";
case CHILDCARE_VACATION = "childcare_vacation";
case TRAINING_VACATION = "training_vacation";
case UNPAID_VACATION = "unpaid_vacation";
case VOLUNTEERING_VACATION = "volunteering_vacation";
case TIME_IN_LIEU = "time_in_lieu";
case SICK_VACATION = "sick_vacation";
public function label(): string
{
return __($this->value);
}
public static function casesToSelect(): array
{
$cases = collect(VacationType::cases());
return $cases->map(
fn(VacationType $enum) => [
"label" => $enum->label(),
"value" => $enum->value,
],
)->toArray();
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestAcceptedByAdministrative
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestAcceptedByTechnical
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestApproved
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestCreated
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
) {
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestStateChanged
{
use Dispatchable;
use SerializesModels;
public function __construct(
public VacationRequest $vacationRequest,
public ?VacationRequestState $from,
public VacationRequestState $to,
public ?User $user = null,
) {
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestStateChanged;
class CreateVacationRequestActivity
{
public function handle(VacationRequestStateChanged $event): void
{
$event->vacationRequest->activities()->create([
"from" => $event->from,
"to" => $event->to,
"user_id" => $event->user?->id,
]);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
use Toby\Domain\VacationRequestStateManager;
class HandleAcceptedByAdministrativeVacationRequest
{
public function __construct(
protected VacationRequestStateManager $stateManager,
) {
}
public function handle(VacationRequestAcceptedByAdministrative $event): void
{
$this->stateManager->approve($event->vacationRequest);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
class HandleAcceptedByTechnicalVacationRequest
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationRequestStateManager $stateManager,
) {
}
public function handle(VacationRequestAcceptedByTechnical $event): void
{
$vacationRequest = $event->vacationRequest;
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
$this->stateManager->waitForAdministrative($vacationRequest);
return;
}
$this->stateManager->approve($vacationRequest);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Illuminate\Support\Facades\Log;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Domain\VacationTypeConfigRetriever;
class HandleApprovedVacationRequest
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
) {
}
public function handle(VacationRequestApproved $event): void
{
$vacationRequest = $event->vacationRequest;
Log::info("approved! {$vacationRequest->id}");
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Listeners;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\VacationTypeConfigRetriever;
class HandleCreatedVacationRequest
{
public function __construct(
protected VacationTypeConfigRetriever $configRetriever,
protected VacationRequestStateManager $stateManager,
) {
}
public function handle(VacationRequestCreated $event): void
{
$vacationRequest = $event->vacationRequest;
if ($this->configRetriever->needsTechnicalApproval($vacationRequest->type)) {
$this->stateManager->waitForTechnical($vacationRequest);
return;
}
if ($this->configRetriever->needsAdministrativeApproval($vacationRequest->type)) {
$this->stateManager->waitForAdministrative($vacationRequest);
return;
}
$this->stateManager->approve($vacationRequest);
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Contracts\Events\Dispatcher;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Domain\Events\VacationRequestAcceptedByAdministrative;
use Toby\Domain\Events\VacationRequestAcceptedByTechnical;
use Toby\Domain\Events\VacationRequestApproved;
use Toby\Domain\Events\VacationRequestCreated;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestStateManager
{
public function __construct(
protected Auth $auth,
protected Dispatcher $dispatcher,
) {
}
public function markAsCreated(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::CREATED);
$this->dispatcher->dispatch(new VacationRequestCreated($vacationRequest));
}
public function approve(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::APPROVED);
$this->dispatcher->dispatch(new VacationRequestApproved($vacationRequest));
}
public function reject(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::REJECTED);
}
public function cancel(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::CANCELED);
}
public function acceptAsTechnical(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::ACCEPTED_BY_TECHNICAL);
$this->dispatcher->dispatch(new VacationRequestAcceptedByTechnical($vacationRequest));
}
public function acceptAsAdministrative(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::ACCEPTED_BY_ADMINSTRATIVE);
$this->dispatcher->dispatch(new VacationRequestAcceptedByAdministrative($vacationRequest));
}
public function waitForTechnical(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::WAITING_FOR_TECHNICAL);
}
public function waitForAdministrative(VacationRequest $vacationRequest): void
{
$this->changeState($vacationRequest, VacationRequestState::WAITING_FOR_ADMINISTRATIVE);
}
protected function changeState(VacationRequest $vacationRequest, VacationRequestState $state): void
{
$vacationRequest->changeStateTo($state);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Toby\Domain;
use Illuminate\Contracts\Config\Repository;
use Toby\Domain\Enums\VacationType;
class VacationTypeConfigRetriever
{
public const KEY_TECHNICAL_APPROVAL = "technical_approval";
public const KEY_ADMINISTRATIVE_APPROVAL = "administrative_approval";
public const KEY_BILLABLE = "billable";
public const KEY_HAS_LIMIT = "has_limit";
public function __construct(
protected Repository $config,
) {
}
public function needsTechnicalApproval(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_TECHNICAL_APPROVAL];
}
public function needsAdministrativeApproval(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_ADMINISTRATIVE_APPROVAL];
}
public function isBillable(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_BILLABLE];
}
public function hasLimit(VacationType $type): bool
{
return $this->getConfigFor($type)[static::KEY_HAS_LIMIT];
}
protected function getConfigFor(VacationType $type): array
{
return $this->config->get("vacation_types.{$type->value}");
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class ApprovedVacationDaysInSameRange implements VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class DoesNotExceedLimitRule implements VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class MinimumOneVacationDayRule implements VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class PendingVacationRequestInSameRange implements VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
class UsedVacationDaysInSameRange
{
public function check(VacationRequest $vacationRequest, Closure $next)
{
return $next($vacationRequest);
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation\Rules;
use Closure;
use Toby\Eloquent\Models\VacationRequest;
interface VacationRequestRule
{
public function check(VacationRequest $vacationRequest, Closure $next);
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Toby\Domain\Validation;
use Illuminate\Pipeline\Pipeline;
use Toby\Domain\Validation\Rules\ApprovedVacationDaysInSameRange;
use Toby\Domain\Validation\Rules\DoesNotExceedLimitRule;
use Toby\Domain\Validation\Rules\MinimumOneVacationDayRule;
use Toby\Domain\Validation\Rules\PendingVacationRequestInSameRange;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestValidator
{
protected array $rules = [
MinimumOneVacationDayRule::class,
DoesNotExceedLimitRule::class,
PendingVacationRequestInSameRange::class,
ApprovedVacationDaysInSameRange::class,
];
public function __construct(
protected Pipeline $pipeline,
) {
}
public function validate(VacationRequest $vacationRequest): void
{
$this->pipeline
->send($vacationRequest)
->through($this->rules)
->via("check");
}
}

View File

@ -13,7 +13,8 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Domain\EmploymentForm;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\Role;
/**
* @property int $id
@ -21,9 +22,11 @@ use Toby\Domain\EmploymentForm;
* @property string $last_name
* @property string $email
* @property string $avatar
* @property Role $role
* @property EmploymentForm $employment_form
* @property Carbon $employment_date
* @property Collection $vacationLimits
* @property Collection $vacationRequests
*/
class User extends Authenticatable
{
@ -34,6 +37,7 @@ class User extends Authenticatable
protected $guarded = [];
protected $casts = [
"role" => Role::class,
"employment_form" => EmploymentForm::class,
"employment_date" => "date",
];
@ -47,6 +51,11 @@ class User extends Authenticatable
return $this->hasMany(VacationLimit::class);
}
public function vacationRequests(): HasMany
{
return $this->hasMany(VacationRequest::class);
}
public function scopeSearch(Builder $query, ?string $text): Builder
{
if ($text === null) {
@ -71,6 +80,11 @@ class User extends Authenticatable
return "{$this->first_name} {$this->last_name}";
}
public function hasRole(Role $role): bool
{
return $this->role === $role;
}
protected static function newFactory(): UserFactory
{
return UserFactory::new();

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Database\Factories\VacationRequestFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Domain\Enums\VacationType;
/**
* @property int $id
* @property VacationType $type
* @property VacationRequestState $state
* @property Carbon $from
* @property Carbon $to
* @property string $comment
* @property User $user
* @property Collection $activities
*/
class VacationRequest extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
"type" => VacationType::class,
"state" => VacationRequestState::class,
"from" => "date",
"to" => "date",
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function activities(): HasMany
{
return $this->hasMany(VacationRequestActivity::class);
}
public function changeStateTo(VacationRequestState $state): void
{
$this->state = $state;
$this->save();
}
public function scopeStates(Builder $query, array $states): Builder
{
return $query->whereIn("state", $states);
}
protected static function newFactory(): VacationRequestFactory
{
return VacationRequestFactory::new();
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Toby\Domain\Enums\VacationRequestState;
/**
* @property int $id
* @property VacationRequest $vacationRequest
* @property ?User $user
* @property ?VacationRequestState $from
* @property VacationRequestState $to
*/
class VacationRequestActivity extends Model
{
protected $guarded = [];
protected $casts = [
"from" => VacationRequestState::class,
"to" => VacationRequestState::class,
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function vacationRequest(): BelongsTo
{
return $this->belongsTo(VacationRequest::class);
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Toby\Eloquent\Observers;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Events\Dispatcher;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Domain\Events\VacationRequestStateChanged;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestObserver
{
public function __construct(
protected Auth $auth,
protected Dispatcher $dispatcher,
) {
}
public function creating(VacationRequest $vacationRequest): void
{
$year = $vacationRequest->from->year;
$vacationRequestNumber = $vacationRequest->user->vacationRequests()
->whereYear("from", $year)
->count() + 1;
$vacationRequest->name = "{$vacationRequestNumber}/${year}";
}
public function saved(VacationRequest $vacationRequest): void
{
if ($vacationRequest->isDirty("state")) {
$previousState = $vacationRequest->getOriginal("state");
$this->fireStateChangedEvent($vacationRequest, $previousState, $vacationRequest->state);
}
}
protected function fireStateChangedEvent(
VacationRequest $vacationRequest,
?VacationRequestState $from,
VacationRequestState $to,
): void {
$event = new VacationRequestStateChanged($vacationRequest, $from, $to, $this->getAuthUser());
$this->dispatcher->dispatch($event);
}
protected function getAuthUser(): ?User
{
/** @var User $user */
$user = $this->auth->guard()->user();
return $user;
}
}

View File

@ -7,7 +7,8 @@ namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Response;
use Toby\Domain\EmploymentForm;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\Role;
use Toby\Eloquent\Models\User;
use Toby\Infrastructure\Http\Requests\UserRequest;
use Toby\Infrastructure\Http\Resources\UserFormDataResource;
@ -35,6 +36,7 @@ class UserController extends Controller
{
return inertia("Users/Create", [
"employmentForms" => EmploymentForm::casesToSelect(),
"roles" => Role::casesToSelect(),
]);
}
@ -52,6 +54,7 @@ class UserController extends Controller
return inertia("Users/Edit", [
"user" => new UserFormDataResource($user),
"employmentForms" => EmploymentForm::casesToSelect(),
"roles" => Role::casesToSelect(),
]);
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Response;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\VacationRequestStateManager;
use Toby\Domain\Validation\VacationRequestValidator;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Infrastructure\Http\Requests\VacationRequestRequest;
use Toby\Infrastructure\Http\Resources\VacationRequestActivityResource;
use Toby\Infrastructure\Http\Resources\VacationRequestResource;
class VacationRequestController extends Controller
{
public function index(Request $request): Response
{
$vacationRequests = $request->user()
->vacationRequests()
->latest()
->states(VacationRequestState::filterByStatus($request->query("status", "all")))
->paginate();
return inertia("VacationRequest/Index", [
"requests" => VacationRequestResource::collection($vacationRequests),
"filters" => $request->only("status"),
]);
}
public function show(VacationRequest $vacationRequest): Response
{
return inertia("VacationRequest/Show", [
"request" => new VacationRequestResource($vacationRequest),
"activities" => VacationRequestActivityResource::collection($vacationRequest->activities),
]);
}
public function create(): Response
{
return inertia("VacationRequest/Create", [
"vacationTypes" => VacationType::casesToSelect(),
]);
}
public function store(
VacationRequestRequest $request,
VacationRequestValidator $vacationRequestValidator,
VacationRequestStateManager $stateManager,
): RedirectResponse {
/** @var VacationRequest $vacationRequest */
$vacationRequest = $request->user()->vacationRequests()->make($request->data());
$vacationRequestValidator->validate($vacationRequest);
$vacationRequest->save();
$stateManager->markAsCreated($vacationRequest);
return redirect()
->route("vacation.requests.index");
}
public function reject(
VacationRequest $vacationRequest,
VacationRequestStateManager $stateManager,
): RedirectResponse {
$stateManager->reject($vacationRequest);
return redirect()->back();
}
public function cancel(
VacationRequest $vacationRequest,
VacationRequestStateManager $stateManager,
): RedirectResponse {
$stateManager->cancel($vacationRequest);
return redirect()->back();
}
public function acceptAsTechnical(
VacationRequest $vacationRequest,
VacationRequestStateManager $stateManager,
): RedirectResponse {
$stateManager->acceptAsTechnical($vacationRequest);
return redirect()->back();
}
public function acceptAsAdministrative(
VacationRequest $vacationRequest,
VacationRequestStateManager $stateManager,
): RedirectResponse {
$stateManager->acceptAsAdministrative($vacationRequest);
return redirect()->back();
}
}

View File

@ -23,6 +23,7 @@ use Illuminate\Routing\Middleware\ValidateSignature;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
use Toby\Infrastructure\Http\Middleware\Authenticate;
use Toby\Infrastructure\Http\Middleware\HandleInertiaRequests;
use Toby\Infrastructure\Http\Middleware\RedirectIfAuthenticated;
@ -52,6 +53,7 @@ class Kernel extends HttpKernel
HandleInertiaRequests::class,
],
"api" => [
EnsureFrontendRequestsAreStateful::class,
"throttle:api",
SubstituteBindings::class,
],

View File

@ -7,7 +7,8 @@ namespace Toby\Infrastructure\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Toby\Domain\EmploymentForm;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\Role;
class UserRequest extends FormRequest
{
@ -17,6 +18,7 @@ class UserRequest extends FormRequest
"firstName" => ["required", "min:3", "max:80"],
"lastName" => ["required", "min:3", "max:80"],
"email" => ["required", "email", Rule::unique("users", "email")->ignore($this->user)],
"role" => ["required", new Enum(Role::class)],
"employmentForm" => ["required", new Enum(EmploymentForm::class)],
"employmentDate" => ["required", "date_format:Y-m-d"],
];
@ -28,6 +30,7 @@ class UserRequest extends FormRequest
"first_name" => $this->get("firstName"),
"last_name" => $this->get("lastName"),
"email" => $this->get("email"),
"role" => $this->get("role"),
"employment_form" => $this->get("employmentForm"),
"employment_date" => $this->get("employmentDate"),
];

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
use Toby\Domain\Enums\VacationType;
use Toby\Infrastructure\Http\Rules\YearPeriodExists;
class VacationRequestRequest extends FormRequest
{
public function rules(): array
{
return [
"type" => ["required", new Enum(VacationType::class)],
"from" => ["required", "date_format:Y-m-d", new YearPeriodExists()],
"to" => ["required", "date_format:Y-m-d", new YearPeriodExists()],
"comment" => ["nullable"],
];
}
public function data(): array
{
return [
"type" => $this->get("type"),
"from" => $this->get("from"),
"to" => $this->get("to"),
"comment" => $this->get("comment"),
];
}
}

View File

@ -15,6 +15,7 @@ class HolidayResource extends JsonResource
return [
"id" => $this->id,
"name" => $this->name,
"date" => $this->date->toDateString(),
"displayDate" => $this->date->toDisplayString(),
"dayOfWeek" => $this->date->dayName,
];

View File

@ -17,8 +17,9 @@ class UserFormDataResource extends JsonResource
"firstName" => $this->first_name,
"lastName" => $this->last_name,
"email" => $this->email,
"role" => $this->role,
"employmentForm" => $this->employment_form,
"employmentDate" => $this->employment_date,
"employmentDate" => $this->employment_date->toDateString(),
];
}
}

View File

@ -16,7 +16,7 @@ class UserResource extends JsonResource
"id" => $this->id,
"name" => $this->fullName,
"email" => $this->email,
"role" => "Human Resources Manager",
"role" => $this->role->label(),
"avatar" => asset($this->avatar),
"deleted" => $this->trashed(),
"employmentForm" => $this->employment_form->label(),

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class VacationRequestActivityResource extends JsonResource
{
public static $wrap = null;
public function toArray($request): array
{
return [
"date" => $this->created_at->toDisplayString(),
"who" => $this->user ? $this->user->fullName : __("System"),
"to" => $this->to->label(),
];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class VacationRequestResource extends JsonResource
{
public static $wrap = null;
public function toArray($request): array
{
return [
"id" => $this->id,
"name" => $this->name,
"user" => new UserResource($this->user),
"type" => $this->type->label(),
"state" => $this->state->label(),
"from" => $this->from->toDisplayString(),
"to" => $this->to->toDisplayString(),
"comment" => $this->comment,
];
}
}

View File

@ -12,6 +12,7 @@
"guzzlehttp/guzzle": "^7.0.1",
"inertiajs/inertia-laravel": "^0.5.1",
"laravel/framework": "^8.75",
"laravel/sanctum": "^2.14",
"laravel/socialite": "^5.2",
"laravel/telescope": "^4.6",
"laravel/tinker": "^2.5",

429
composer.lock generated

File diff suppressed because it is too large Load Diff

20
config/sanctum.php Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
return [
"stateful" => explode(",", env("SANCTUM_STATEFUL_DOMAINS", sprintf(
"%s%s",
"localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1",
env("APP_URL") ? "," . parse_url(env("APP_URL"), PHP_URL_HOST) : "",
))),
"guard" => ["web"],
"expiration" => null,
"middleware" => [
"verify_csrf_token" => VerifyCsrfToken::class,
"encrypt_cookies" => EncryptCookies::class,
],
];

63
config/vacation_types.php Normal file
View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
use Toby\Domain\Enums\VacationType;
use Toby\Domain\VacationTypeConfigRetriever;
return [
VacationType::VACATION->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => true,
],
VacationType::VACATION_ON_REQUEST->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => true,
],
VacationType::TIME_IN_LIEU->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => false,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => false,
VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
],
VacationType::SICK_VACATION->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => false,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
],
VacationType::UNPAID_VACATION->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => false,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
],
VacationType::SPECIAL_VACATION->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => false,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
],
VacationType::CHILDCARE_VACATION->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => false,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
],
VacationType::TRAINING_VACATION->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
],
VacationType::VOLUNTEERING_VACATION->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
],
];

View File

@ -7,7 +7,8 @@ namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Toby\Domain\EmploymentForm;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\Role;
use Toby\Eloquent\Models\User;
class UserFactory extends Factory
@ -21,6 +22,7 @@ class UserFactory extends Factory
"last_name" => $this->faker->lastName(),
"email" => $this->faker->unique()->safeEmail(),
"employment_form" => $this->faker->randomElement(EmploymentForm::cases()),
"role" => Role::EMPLOYEE,
"employment_date" => Carbon::createFromInterface($this->faker->dateTimeBetween("2020-10-27"))->toDateString(),
"remember_token" => Str::random(10),
];

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Toby\Domain\Enums\VacationRequestState;
use Toby\Domain\Enums\VacationType;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
class VacationRequestFactory extends Factory
{
protected $model = VacationRequest::class;
public function definition(): array
{
$number = $this->faker->numberBetween(1, 20);
$year = $this->faker->year;
return [
"user_id" => User::factory(),
"name" => "{$number}/{$year}",
"type" => $this->faker->randomElement(VacationType::cases()),
"state" => $this->faker->randomElement(VacationRequestState::cases()),
"from" => $this->faker->date,
"to" => $this->faker->date,
"comment" => $this->faker->boolean ? $this->faker->paragraph() : null,
];
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Toby\Domain\Enums\Role;
return new class() extends Migration {
public function up(): void
@ -15,6 +16,7 @@ return new class() extends Migration {
$table->string("last_name");
$table->string("email")->unique();
$table->string("avatar")->nullable();
$table->string("role")->default(Role::EMPLOYEE->value);
$table->string("employment_form");
$table->date("employment_date");
$table->rememberToken();

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Toby\Eloquent\Models\User;
return new class() extends Migration {
public function up(): void
{
Schema::create("vacation_requests", function (Blueprint $table): void {
$table->id();
$table->string("name");
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
$table->string("type");
$table->string("state")->nullable();
$table->date("from");
$table->date("to");
$table->text("comment")->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists("vacation_requests");
}
};

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationRequest;
return new class() extends Migration {
public function up(): void
{
Schema::create("vacation_request_activities", function (Blueprint $table): void {
$table->id();
$table->foreignIdFor(VacationRequest::class)->constrained()->cascadeOnDelete();
$table->foreignIdFor(User::class)->nullable()->constrained()->cascadeOnDelete();
$table->string("from")->nullable();
$table->string("to");
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists("vacation_request_activities");
}
};

View File

@ -11,6 +11,7 @@ use Toby\Domain\PolishHolidaysRetriever;
use Toby\Eloquent\Helpers\UserAvatarGenerator;
use Toby\Eloquent\Models\User;
use Toby\Eloquent\Models\VacationLimit;
use Toby\Eloquent\Models\VacationRequest;
use Toby\Eloquent\Models\YearPeriod;
class DatabaseSeeder extends Seeder
@ -24,11 +25,14 @@ class DatabaseSeeder extends Seeder
{
User::unsetEventDispatcher();
YearPeriod::unsetEventDispatcher();
VacationRequest::unsetEventDispatcher();
User::factory(9)->create();
User::factory([
"email" => env("LOCAL_EMAIL_FOR_LOGIN_VIA_GOOGLE"),
])->create();
])
->hasVacationRequests(5)
->create();
$users = User::all();

2677
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,12 +22,14 @@
"@tailwindcss/typography": "^0.5.0",
"@vue/compiler-sfc": "^3.2.26",
"autoprefixer": "^10.4.2",
"echarts": "^5.2.2",
"flatpickr": "^4.6.9",
"laravel-mix": "^6.0.6",
"lodash": "^4.17.21",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.13",
"vue": "^3.2.26",
"vue-echarts": "^6.0.2",
"vue-flatpickr-component": "^9.0.5",
"vue-loader": "^17.0.0"
},

View File

@ -47,3 +47,24 @@
-webkit-box-shadow: -5px 0 0 #527ABA, 5px 0 0 #527ABA;
box-shadow: -5px 0 0 #527ABA, 5px 0 0 #527ABA;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
border-radius: 100vh;
background: transparent;
}
::-webkit-scrollbar-thumb {
border-radius: 8px;
background: #dadce0;
border: 4px solid transparent;
}
::-webkit-scrollbar-thumb:hover {
background: #dadce0;
}

View File

@ -81,8 +81,8 @@
</template>
<script>
import { useForm } from '@inertiajs/inertia-vue3';
import FlatPickr from 'vue-flatpickr-component';
import { useForm } from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component'
export default {
name: 'HolidayCreate',
@ -93,14 +93,14 @@ export default {
const form = useForm({
name: null,
date: null,
});
})
return { form };
return { form }
},
methods: {
createHoliday() {
this.form.post('/holidays');
this.form.post('/holidays')
},
},
};
}
</script>

View File

@ -81,8 +81,8 @@
</template>
<script>
import { useForm } from '@inertiajs/inertia-vue3';
import FlatPickr from 'vue-flatpickr-component';
import { useForm } from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component'
export default {
name: 'HolidayEdit',
@ -99,15 +99,15 @@ export default {
const form = useForm({
name: props.holiday.name,
date: props.holiday.date,
});
})
return { form };
return { form }
},
methods: {
editHoliday() {
this.form
.put(`/holidays/${this.holiday.id}`);
.put(`/holidays/${this.holiday.id}`)
},
},
};
}
</script>

View File

@ -140,8 +140,8 @@
</template>
<script>
import { DotsVerticalIcon, PencilIcon, TrashIcon } from '@heroicons/vue/solid';
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
import { DotsVerticalIcon, PencilIcon, TrashIcon } from '@heroicons/vue/solid'
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
export default {
name: 'HolidayINdex',
@ -161,7 +161,7 @@ export default {
},
},
setup() {
return {};
return {}
},
};
}
</script>

View File

@ -65,12 +65,12 @@
</template>
<script>
import GuestLayout from '@/Shared/Layout/GuestLayout';
import {XIcon} from '@heroicons/vue/solid';
import {ExclamationIcon} from '@heroicons/vue/solid';
import GuestLayout from '@/Shared/Layout/GuestLayout'
import {XIcon} from '@heroicons/vue/solid'
import {ExclamationIcon} from '@heroicons/vue/solid'
export default {
name: 'Login',
name: 'LoginPage',
components: {
XIcon,
ExclamationIcon,
@ -82,5 +82,5 @@ export default {
default: () => ({oauth: null}),
},
},
};
}
</script>

View File

@ -82,6 +82,60 @@
</p>
</div>
</div>
<Listbox
v-model="form.role"
as="div"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<ListboxLabel class="block text-sm font-medium text-gray-700">
Rola
</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.employmentForm, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.employmentForm }"
>
<span class="block truncate">{{ form.role.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="role in roles"
:key="role.value"
v-slot="{ active, selected }"
as="template"
:value="role"
>
<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']">
{{ role.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.role"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.role }}
</p>
</div>
</Listbox>
<Listbox
v-model="form.employmentForm"
as="div"
@ -182,13 +236,13 @@
</template>
<script>
import { useForm } from '@inertiajs/inertia-vue3';
import FlatPickr from 'vue-flatpickr-component';
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '@headlessui/vue';
import { CheckIcon, SelectorIcon } from '@heroicons/vue/solid';
import { useForm } from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component'
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '@headlessui/vue'
import { CheckIcon, SelectorIcon } from '@heroicons/vue/solid'
export default {
employmentDate: 'UserCreate',
name: 'UserCreate',
components: {
FlatPickr,
Listbox,
@ -204,6 +258,10 @@ export default {
type: Object,
default: () => null,
},
roles: {
type: Object,
default: () => null,
},
},
setup(props) {
const form = useForm({
@ -211,10 +269,11 @@ export default {
lastName: null,
email: null,
employmentForm: props.employmentForms[0],
role: props.roles[0],
employmentDate: null,
});
})
return { form };
return { form }
},
methods: {
createUser() {
@ -222,9 +281,10 @@ export default {
.transform(data => ({
...data,
employmentForm: data.employmentForm.value,
role: data.role.value,
}))
.post('/users');
.post('/users')
},
},
};
}
</script>

View File

@ -82,6 +82,60 @@
</p>
</div>
</div>
<Listbox
v-model="form.role"
as="div"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<ListboxLabel class="block text-sm font-medium text-gray-700">
Rola
</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.employmentForm, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.employmentForm }"
>
<span class="block truncate">{{ form.role.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="role in roles"
:key="role.value"
v-slot="{ active, selected }"
as="template"
:value="role"
>
<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']">
{{ role.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.role"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.role }}
</p>
</div>
</Listbox>
<Listbox
v-model="form.employmentForm"
as="div"
@ -100,7 +154,6 @@
<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"
@ -182,13 +235,13 @@
</template>
<script>
import {useForm} from '@inertiajs/inertia-vue3';
import FlatPickr from 'vue-flatpickr-component';
import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions} from '@headlessui/vue';
import {CheckIcon, SelectorIcon} from '@heroicons/vue/solid';
import {useForm} from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component'
import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions} from '@headlessui/vue'
import {CheckIcon, SelectorIcon} from '@heroicons/vue/solid'
export default {
employmentDate: 'UserEdit',
name: 'UserEdit',
components: {
FlatPickr,
Listbox,
@ -204,6 +257,10 @@ export default {
type: Object,
default: () => null,
},
roles: {
type: Object,
default: () => null,
},
user: {
type: Object,
default: () => null,
@ -214,11 +271,12 @@ export default {
firstName: props.user.firstName,
lastName: props.user.lastName,
email: props.user.email,
role: props.roles.find(role => role.value === props.user.role),
employmentForm: props.employmentForms.find(form => form.value === props.user.employmentForm),
employmentDate: props.user.employmentDate,
});
})
return { form };
return { form }
},
methods: {
editUser() {
@ -226,9 +284,10 @@ export default {
.transform(data => ({
...data,
employmentForm: data.employmentForm.value,
role: data.role.value,
}))
.put(`/users/${this.user.id}`);
.put(`/users/${this.user.id}`)
},
},
};
}
</script>

View File

@ -252,12 +252,12 @@
</template>
<script>
import { ref, watch } from 'vue';
import { Inertia } from '@inertiajs/inertia';
import { debounce } from 'lodash';
import { SearchIcon } from '@heroicons/vue/outline';
import { DotsVerticalIcon, PencilIcon, TrashIcon, RefreshIcon } from '@heroicons/vue/solid';
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
import { ref, watch } from 'vue'
import { Inertia } from '@inertiajs/inertia'
import { debounce } from 'lodash'
import { SearchIcon } from '@heroicons/vue/outline'
import { DotsVerticalIcon, PencilIcon, TrashIcon, RefreshIcon } from '@heroicons/vue/solid'
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
export default {
name: 'UserIndex',
@ -283,18 +283,18 @@ export default {
},
},
setup(props) {
let search = ref(props.filters.search);
let search = ref(props.filters.search)
watch(search, debounce(value => {
Inertia.get('/users', value ? { search: value} : {}, {
preserveState: true,
replace: true,
});
}, 300));
})
}, 300))
return {
search,
};
}
},
};
}
</script>

View File

@ -129,8 +129,8 @@
</template>
<script>
import {Switch} from '@headlessui/vue';
import {useForm} from '@inertiajs/inertia-vue3';
import {Switch} from '@headlessui/vue'
import {useForm} from '@inertiajs/inertia-vue3'
export default {
name: 'VacationLimits',
@ -150,11 +150,11 @@ export default {
setup(props) {
const form = useForm({
items: props.limits.data,
});
})
return {
form,
};
}
},
methods: {
submitVacationDays() {
@ -168,8 +168,8 @@ export default {
.put('/vacation-limits', {
preserveState: (page) => Object.keys(page.props.errors).length,
preserveScroll: true,
});
})
},
},
};
}
</script>

View File

@ -0,0 +1,240 @@
<template>
<InertiaHead title="Złóż wniosek urlopowy" />
<div class="bg-white sm:rounded-lg shadow-md">
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Złóż wniosek urlopowy
</h2>
</div>
<form
class="border-t border-gray-200 px-6"
@submit.prevent="createForm"
>
<Listbox
v-model="form.vacationType"
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.vacationType, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.vacationType }"
>
<span class="block truncate">{{ form.vacationType.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="vacationType in vacationTypes"
:key="vacationType.value"
v-slot="{ active, selected }"
as="template"
:value="vacationType"
>
<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']">
{{ vacationType.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.vacationType"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.vacationType }}
</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.dateFrom"
: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.dateFrom, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.dateFrom }"
@on-change="onFromChange"
/>
<p
v-if="form.errors.dateFrom"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.dateFrom }}
</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.dateTo"
: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.dateTo, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.dateTo }"
@on-change="onToChange"
/>
<p
v-if="form.errors.dateTo"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.dateTo }}
</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">
1
</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-indigo-500 focus:border-indigo-500 block w-full max-w-lg sm:text-sm border-gray-300 rounded-md"
/>
</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>
</template>
<script>
import {useForm} from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component'
import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions} from '@headlessui/vue'
import {CheckIcon, SelectorIcon} from '@heroicons/vue/solid'
import {reactive} from 'vue'
export default {
name: 'VacationRequestCreate',
components: {
FlatPickr,
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
CheckIcon,
SelectorIcon,
},
props: {
vacationTypes: {
type: Object,
default: () => null,
},
holidays: {
type: Object,
default: () => null,
},
},
setup(props) {
const form = useForm({
dateFrom: null,
dateTo: null,
vacationType: props.vacationTypes[0],
comment: null,
})
const disableDates = [
date => (date.getDay() === 0 || date.getDay() === 6),
]
const fromInputConfig = reactive({
maxDate: null,
disable: disableDates,
})
const toInputConfig = reactive({
minDate: null,
disable: disableDates,
})
return {
form,
fromInputConfig,
toInputConfig,
}
},
methods: {
createForm() {
this.form
.transform(data => ({
from: data.dateFrom,
to: data.dateTo,
type: data.vacationType.value,
comment: data.comment,
}))
.post('/vacation-requests')
},
onFromChange(selectedDates, dateStr) {
this.toInputConfig.minDate = dateStr
},
onToChange(selectedDates, dateStr) {
this.fromInputConfig.maxDate = dateStr
},
},
}
</script>

View File

@ -0,0 +1,217 @@
<template>
<InertiaHead title="Twoje wnioski urlopowe" />
<div class="bg-white sm:rounded-lg shadow-md">
<div class="flex justify-between items-center p-4 sm:px-6">
<div>
<h2 class="text-lg leading-6 font-medium text-gray-900">
Twoje wnioski urlopowe
</h2>
</div>
<div>
<InertiaLink
href="vacation-requests/create"
class="inline-flex items-center px-4 py-3 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blumilk-600 hover:bg-blumilk-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500"
>
Dodaj wniosek
</InertiaLink>
</div>
</div>
<div class="overflow-x-auto xl:overflow-x-visible overflow-y-auto xl:overflow-y-visible">
<nav class="relative shadow flex divide-x divide-gray-200 border-t border-gray-200">
<InertiaLink
v-for="(status, index) in statuses"
:key="index"
:data="{ status: status.value }"
:class="[status.value === filters.status ? 'text-gray-900' : '', 'text-gray-500 hover:text-gray-700 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-sm font-medium text-center hover:bg-gray-50 focus:z-10']"
>
<span>{{ status.name }}</span>
<span :class="[status.value === filters.status ? 'bg-blumilk-500' : 'bg-transparent', 'absolute inset-x-0 bottom-0 h-0.5']" />
</InertiaLink>
</nav>
<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"
>
Numer
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Rodzaj urlopu
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Status
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Od
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Do
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider"
>
Dni urlopu
</th>
<th scope="col" />
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
<tr
v-for="request in requests.data"
:key="request.id"
class="hover:bg-blumilk-25"
>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<InertiaLink
:href="`/vacation-requests/${request.id}`"
class="font-semibold text-blumilk-600 hover:text-blumilk-500 hover:underline"
>
{{ request.name }}
</InertiaLink>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{{ request.type }}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{{ request.state }}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{{ request.from }}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{{ request.to }}
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
X
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
<InertiaLink :href="`/vacation-requests/${request.id}`">
<ChevronRightIcon class="block w-6 h-6 fill-gray-400" />
</InertiaLink>
</td>
</tr>
<tr
v-if="! requests.data.length"
>
<td
colspan="100%"
class="text-center py-4 text-xl leading-5 text-gray-700"
>
Brak danych
</td>
</tr>
</tbody>
</table>
<div
v-if="requests.data.length && requests.meta.last_page !== 1"
class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6 rounded-b-lg"
>
<div class="flex-1 flex justify-between sm:hidden">
<InertiaLink
:is="requests.links.prev ? 'InertiaLink': 'span'"
:href="requests.links.prev"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Poprzednia
</InertiaLink>
<Component
:is="requests.links.next ? 'InertiaLink': 'span'"
:href="requests.links.next"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Następna
</Component>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div class="text-sm text-gray-700">
Wyświetlanie
<span class="font-medium">{{ requests.meta.from }}</span>
od
<span class="font-medium">{{ requests.meta.to }}</span>
do
<span class="font-medium">{{ requests.meta.total }}</span>
wyników
</div>
<nav class="relative z-0 inline-flex space-x-1">
<template
v-for="(link, index) in requests.meta.links"
:key="index"
>
<Component
:is="link.url ? 'InertiaLink' : 'span'"
:href="link.url"
:preserve-scroll="true"
class="relative inline-flex items-center px-4 py-2 border rounded-md text-sm font-medium"
:class="{ 'z-10 bg-blumilk-25 border-blumilk-500 text-blumilk-600': link.active, 'bg-white border-gray-300 text-gray-500': !link.active, 'hover:bg-blumilk-25': link.url, 'border-none': !link.url}"
v-text="link.label"
/>
</template>
</nav>
</div>
</div>
</div>
</div>
</template>
<script>
import {ChevronRightIcon, DotsVerticalIcon, PencilIcon, TrashIcon} from '@heroicons/vue/solid'
export default {
name: 'VacationRequestIndex',
components: {
DotsVerticalIcon,
PencilIcon,
TrashIcon,
ChevronRightIcon,
},
props: {
requests: {
type: Object,
default: () => null,
},
filters: {
type: Object,
default: () => null,
},
},
setup() {
const statuses = [
{
name: 'Wszystkie',
value: 'all',
},
{
name: 'W trakcie',
value: 'pending',
},
{
name: 'Zatwierdzone',
value: 'success',
},
{
name: 'Odrzucone/anulowane',
value: 'failed',
},
]
return {
statuses,
}
},
}
</script>

View File

@ -0,0 +1,216 @@
<template>
<InertiaHead :title="`Wniosek ${request.name}`" />
<div class="grid grid-cols-1 gap-6 xl:grid-flow-col-dense xl:grid-cols-3">
<div class="space-y-6 xl:col-start-1 xl:col-span-2">
<div class="bg-white sm:rounded-lg shadow-md">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Informacje na temat wniosku
</h3>
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:p-0">
<dl class="sm:divide-y sm:divide-gray-200">
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Nr wniosku
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ request.name }}
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Rodzaj urlopu
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ request.type }}
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Urlop od
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ request.from }}
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Urlop do
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ request.to }}
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Dni
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
x
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">
Komentarz
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{{ request.comment }}
</dd>
</div>
</dl>
</div>
</div>
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Zaakceptuj wniosek jako osoba techniczna
</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<p>
W zależności od typu wniosku, zostanie on zatwierdzony lub osoba administracyjna będzie musiała go zaakceptować.
</p>
</div>
<div class="mt-5">
<InertiaLink
:href="`/vacation-requests/${request.id}/accept-as-technical`"
method="post"
as="button"
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"
>
Zaakceptuj wniosek
</InertiaLink>
</div>
</div>
</div>
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Zaakceptuj wniosek jako osoba administracyjna
</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<p>
Po akceptacji przez osobę administracyjną, wniosek zostanie zatwierdzony.
</p>
</div>
<div class="mt-5">
<InertiaLink
:href="`/vacation-requests/${request.id}/accept-as-administrative`"
method="post"
as="button"
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"
>
Zaakceptuj wniosek
</InertiaLink>
</div>
</div>
</div>
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Odrzuć wniosek
</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<p>
Odrzuconego wniosku nie można przywracać - należy zrobić nowy.
</p>
</div>
<div class="mt-5">
<InertiaLink
:href="`/vacation-requests/${request.id}/reject`"
method="post"
as="button"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
>
Odrzuć wniosek
</InertiaLink>
</div>
</div>
</div>
<div class="bg-white shadow sm:rounded-lg border border-red-500">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Anuluj wniosek
</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<p>
Wniosek można anulować w każdej chwili - nawet jeśli był już zatwierdzony.
</p>
</div>
<div class="mt-5">
<InertiaLink
:href="`/vacation-requests/${request.id}/cancel`"
method="post"
as="button"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
>
Anuluj wniosek
</InertiaLink>
</div>
</div>
</div>
</div>
<div class="xl:col-start-3 xl:col-span-1 space-y-6">
<div class="bg-white sm:rounded-lg shadow-md">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Historia wniosku
</h3>
</div>
<div class="border-t border-gray-200 px-4 py-4">
<ul>
<li
v-for="(activity, index) in activities.data"
:key="activity.id"
>
<div :class="{'relative pb-8': index !== activities.data.length - 1}">
<span
v-if="(index !== activities.data.length - 1)"
class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
/>
<div class="relative flex space-x-3">
<div>
<span class="bg-blumilk-500 h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white">
<ThumbUpIcon class="w-5 h-5 text-white" />
</span>
</div>
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
<div>
<p class="text-sm text-gray-500">
{{ activity.to }}
</p>
</div>
<div class="text-right text-sm whitespace-nowrap text-gray-500">
<time>{{ activity.date }}</time>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { ThumbUpIcon } from '@heroicons/vue/outline'
export default {
name: 'VacationRequestShow',
components: {
ThumbUpIcon,
},
props: {
request: {
type: Object,
default: () => null,
},
activities: {
type: Object,
default: () => null,
},
},
}
</script>

View File

@ -8,5 +8,5 @@
<script>
export default {
name: 'GuestLayout',
};
}
</script>

View File

@ -1,38 +1,38 @@
import {createApp, h} from 'vue';
import {createInertiaApp, Head, Link} from '@inertiajs/inertia-vue3';
import {InertiaProgress} from '@inertiajs/progress';
import AppLayout from '@/Shared/Layout/AppLayout';
import Flatpickr from 'flatpickr';
import { Polish } from 'flatpickr/dist/l10n/pl.js';
import {createApp, h} from 'vue'
import {createInertiaApp, Head, Link} from '@inertiajs/inertia-vue3'
import {InertiaProgress} from '@inertiajs/progress'
import AppLayout from '@/Shared/Layout/AppLayout'
import Flatpickr from 'flatpickr'
import { Polish } from 'flatpickr/dist/l10n/pl.js'
createInertiaApp({
resolve: name => {
const page = require(`./Pages/${name}`).default;
const page = require(`./Pages/${name}`).default
page.layout = page.layout || AppLayout;
page.layout = page.layout || AppLayout
return page;
return page
},
setup({el, App, props, plugin}) {
createApp({render: () => h(App, props)})
.use(plugin)
.component('InertiaLink', Link)
.component('InertiaHead', Head)
.mount(el);
.mount(el)
},
title: title => `${title} - Toby`,
});
})
InertiaProgress.init({
delay: 0,
color: 'red',
});
})
Flatpickr.localize(Polish);
Flatpickr.localize(Polish)
Flatpickr.setDefaults({
dateFormat: 'Y-m-d',
enableTime: false,
altFormat: 'j F Y',
altInput: true,
});
})

View File

@ -3,5 +3,27 @@
"employment_contract": "Umowa o pracę",
"commission_contract": "Umowa zlecenie",
"b2b_contract": "Kontrakt B2B",
"board_member_contract": "Członek zarządu"
"board_member_contract": "Członek zarządu",
"vacation": "Urlop wypoczynkowy",
"vacation_on_request": "Urlop na żądanie",
"special_vacation": "Urlop okolicznościowy",
"childcare_vacation": "Opieka nad dzieckiem art 188 kp",
"training_vacation": "Urlop szkoleniowy",
"unpaid_vacation": "Urlop bezpłatny",
"volunteering_vacation": "Wolontariat",
"look_for_work_vacation": "Urlop na poszukiwanie pracy",
"time_in_lieu": "Odbiór za święto",
"sick_vacation": "Zwolnienie lekarskie",
"employee": "Pracownik",
"administrator": "Administrator",
"technical_approver": "Techniczny klepacz",
"administrative_approver": "Administracyjny klepacz",
"created": "Utworzony",
"canceled": "Anulowany",
"rejected": "Odrzucony",
"approved": "Zatwierdzony",
"waiting_for_technical": "Czeka na akceptację od technicznego",
"waiting_for_administrative": "Czeka na akceptację od administracyjnego",
"accepted_by_technical": "Zaakceptowany przez technicznego",
"accepted_by_administrative": "Zaakceptowany przez administracyjnego"
}

View File

@ -3,16 +3,17 @@
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Toby\Infrastructure\Http\Controllers\GoogleController;
use Toby\Infrastructure\Http\Controllers\HolidayController;
use Toby\Infrastructure\Http\Controllers\LogoutController;
use Toby\Infrastructure\Http\Controllers\SelectYearPeriodController;
use Toby\Infrastructure\Http\Controllers\UserController;
use Toby\Infrastructure\Http\Controllers\VacationLimitController;
use Toby\Infrastructure\Http\Controllers\VacationRequestController;
Route::middleware("auth")->group(function (): void {
Route::get("/", fn() => inertia("Dashboard"))->name("dashboard");
Route::get("/", fn() => inertia("Dashboard"))
->name("dashboard");
Route::post("/logout", LogoutController::class);
Route::resource("users", UserController::class);
@ -20,16 +21,37 @@ Route::middleware("auth")->group(function (): void {
Route::resource("holidays", HolidayController::class);
Route::get("/vacation-limits", [VacationLimitController::class, "edit"])
->name("vacation.limits");
Route::get("/calendar", [HolidayController::class,"showCalendar"]);
Route::get("/vacation-limits", [VacationLimitController::class, "edit"])->name("vacation.limits");
Route::put("/vacation-limits", [VacationLimitController::class, "update"]);
Route::post("year-periods/{yearPeriod}/select", SelectYearPeriodController::class)->name("year-periods.select");
Route::get("/vacation-requests", [VacationRequestController::class, "index"])
->name("vacation.requests.index");
Route::get("/vacation-requests/create", [VacationRequestController::class, "create"])
->name("vacation.requests.create");
Route::post("/vacation-requests", [VacationRequestController::class, "store"])
->name("vacation.requests.store");
Route::get("/vacation-requests/{vacationRequest}", [VacationRequestController::class, "show"])
->name("vacation.requests.show");
Route::post("/vacation-requests/{vacationRequest}/reject", [VacationRequestController::class, "reject"])
->name("vacation.requests.reject");
Route::post("/vacation-requests/{vacationRequest}/cancel", [VacationRequestController::class, "cancel"])
->name("vacation.requests.cancel");
Route::post("/vacation-requests/{vacationRequest}/accept-as-technical", [VacationRequestController::class, "acceptAsTechnical"])
->name("vacation.requests.accept-as-technical");
Route::post("/vacation-requests/{vacationRequest}/accept-as-administrative", [VacationRequestController::class, "acceptAsAdministrative"])
->name("vacation.requests.accept-as-administrative");
Route::post("year-periods/{yearPeriod}/select", SelectYearPeriodController::class)
->name("year-periods.select");
});
Route::middleware("guest")->group(function (): void {
Route::get("login", fn() => inertia("Login"))->name("login");
Route::get("login", fn() => inertia("Login"))
->name("login");
Route::get("login/google/start", [GoogleController::class, "redirect"])
->name("login.google.start");
Route::get("login/google/end", [GoogleController::class, "callback"])

View File

@ -8,7 +8,8 @@ use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Carbon;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\FeatureTestCase;
use Toby\Domain\EmploymentForm;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\Role;
use Toby\Eloquent\Models\User;
class UserTest extends FeatureTestCase
@ -87,6 +88,7 @@ class UserTest extends FeatureTestCase
->post("/users", [
"firstName" => "John",
"lastName" => "Doe",
"role" => Role::EMPLOYEE->value,
"email" => "john.doe@example.com",
"employmentForm" => EmploymentForm::B2B_CONTRACT->value,
"employmentDate" => Carbon::now()->toDateString(),
@ -122,6 +124,7 @@ class UserTest extends FeatureTestCase
"firstName" => "John",
"lastName" => "Doe",
"email" => "john.doe@example.com",
"role" => Role::EMPLOYEE->value,
"employmentForm" => EmploymentForm::B2B_CONTRACT->value,
"employmentDate" => Carbon::now()->toDateString(),
])