#108 - types of vacation request (#110)

* #108 - wip

* #108 - add icon to absence

* #108 - wip

* #108 - fix

* #108 - fix title

Co-authored-by: EwelinaLasowy <ewelina.lasowy@blumilk.pl>
This commit is contained in:
Adrian Hopek 2022-04-06 15:13:54 +02:00 committed by GitHub
parent fa244b96cd
commit 6af4380fe6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 208 additions and 48 deletions

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Toby\Domain\Enums; namespace Toby\Domain\Enums;
use Illuminate\Support\Collection;
enum VacationType: string enum VacationType: string
{ {
case Vacation = "vacation"; case Vacation = "vacation";
@ -15,6 +17,7 @@ enum VacationType: string
case Volunteering = "volunteering_vacation"; case Volunteering = "volunteering_vacation";
case TimeInLieu = "time_in_lieu"; case TimeInLieu = "time_in_lieu";
case Sick = "sick_vacation"; case Sick = "sick_vacation";
case Absence = "absence";
public function label(): string public function label(): string
{ {
@ -23,7 +26,7 @@ enum VacationType: string
public static function casesToSelect(): array public static function casesToSelect(): array
{ {
$cases = collect(VacationType::cases()); $cases = VacationType::all();
return $cases->map( return $cases->map(
fn(VacationType $enum) => [ fn(VacationType $enum) => [
@ -32,4 +35,9 @@ enum VacationType: string
], ],
)->toArray(); )->toArray();
} }
public static function all(): Collection
{
return new Collection(VacationType::cases());
}
} }

View File

@ -4,20 +4,21 @@ declare(strict_types=1);
namespace Toby\Domain; namespace Toby\Domain;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\WithMultipleSheets; use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\User;
class TimesheetExport implements WithMultipleSheets class TimesheetExport implements WithMultipleSheets
{ {
protected Collection $users; protected Collection $users;
protected Collection $types;
protected Carbon $month; protected Carbon $month;
public function sheets(): array public function sheets(): array
{ {
return $this->users return $this->users
->map(fn(User $user) => new TimesheetPerUserSheet($user, $this->month)) ->map(fn(User $user) => new TimesheetPerUserSheet($user, $this->month, $this->types))
->toArray(); ->toArray();
} }
@ -34,4 +35,11 @@ class TimesheetExport implements WithMultipleSheets
return $this; return $this;
} }
public function forVacationTypes(Collection $types): static
{
$this->types = $types;
return $this;
}
} }

View File

@ -7,6 +7,7 @@ namespace Toby\Domain;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Carbon\CarbonPeriod; use Carbon\CarbonPeriod;
use Generator; use Generator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromGenerator; use Maatwebsite\Excel\Concerns\FromGenerator;
@ -40,6 +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,
protected Collection $types,
) {} ) {}
public function title(): string public function title(): string
@ -49,8 +51,6 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
public function headings(): array public function headings(): array
{ {
$types = VacationType::cases();
$headings = [ $headings = [
__("Date"), __("Date"),
__("Day of week"), __("Day of week"),
@ -59,7 +59,7 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
__("Worked hours"), __("Worked hours"),
]; ];
foreach ($types as $type) { foreach ($this->types as $type) {
$headings[] = $type->label(); $headings[] = $type->label();
} }
@ -187,6 +187,7 @@ class TimesheetPerUserSheet implements WithTitle, WithHeadings, WithEvents, With
{ {
return $user->vacations() return $user->vacations()
->with("vacationRequest") ->with("vacationRequest")
->whereRelation("vacationRequest", fn(Builder $query) => $query->whereIn("type", $this->types))
->whereBetween("date", [$period->start, $period->end]) ->whereBetween("date", [$period->start, $period->end])
->approved() ->approved()
->get() ->get()

View File

@ -95,14 +95,14 @@ class UserVacationStatsRetriever
protected function getLimitableVacationTypes(): Collection protected function getLimitableVacationTypes(): Collection
{ {
$types = new Collection(VacationType::cases()); $types = VacationType::all();
return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type)); return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type));
} }
protected function getNotLimitableVacationTypes(): Collection protected function getNotLimitableVacationTypes(): Collection
{ {
$types = new Collection(VacationType::cases()); $types = VacationType::all();
return $types->filter(fn(VacationType $type) => !$this->configRetriever->hasLimit($type)); return $types->filter(fn(VacationType $type) => !$this->configRetriever->hasLimit($type));
} }

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Toby\Domain; namespace Toby\Domain;
use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Config\Repository;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\VacationType; use Toby\Domain\Enums\VacationType;
class VacationTypeConfigRetriever class VacationTypeConfigRetriever
@ -13,6 +14,7 @@ class VacationTypeConfigRetriever
public const KEY_ADMINISTRATIVE_APPROVAL = "administrative_approval"; public const KEY_ADMINISTRATIVE_APPROVAL = "administrative_approval";
public const KEY_BILLABLE = "billable"; public const KEY_BILLABLE = "billable";
public const KEY_HAS_LIMIT = "has_limit"; public const KEY_HAS_LIMIT = "has_limit";
public const KEY_AVAILABLE_FOR = "available_for";
public function __construct( public function __construct(
protected Repository $config, protected Repository $config,
@ -38,6 +40,11 @@ class VacationTypeConfigRetriever
return $this->getConfigFor($type)[static::KEY_HAS_LIMIT]; return $this->getConfigFor($type)[static::KEY_HAS_LIMIT];
} }
public function isAvailableFor(VacationType $type, EmploymentForm $employmentForm): bool
{
return in_array($employmentForm, $this->getConfigFor($type)[static::KEY_AVAILABLE_FOR], true);
}
protected function getConfigFor(VacationType $type): array protected function getConfigFor(VacationType $type): array
{ {
return $this->config->get("vacation_types.{$type->value}"); return $this->config->get("vacation_types.{$type->value}");

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Toby\Domain\Validation\Rules; namespace Toby\Domain\Validation\Rules;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Collection;
use Toby\Domain\Enums\VacationType; use Toby\Domain\Enums\VacationType;
use Toby\Domain\VacationDaysCalculator; use Toby\Domain\VacationDaysCalculator;
use Toby\Domain\VacationRequestStatesRetriever; use Toby\Domain\VacationRequestStatesRetriever;
@ -59,7 +59,7 @@ class DoesNotExceedLimitRule implements VacationRequestRule
protected function getLimitableVacationTypes(): Collection protected function getLimitableVacationTypes(): Collection
{ {
$types = new Collection(VacationType::cases()); $types = VacationType::all();
return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type)); return $types->filter(fn(VacationType $type) => $this->configRetriever->hasLimit($type));
} }

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Toby\Infrastructure\Http\Controllers\Api;
use Illuminate\Http\JsonResponse;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Models\User;
use Toby\Infrastructure\Http\Controllers\Controller;
use Toby\Infrastructure\Http\Requests\Api\GetAvailableVacationTypesRequest;
class GetAvailableVacationTypesController extends Controller
{
public function __invoke(
GetAvailableVacationTypesRequest $request,
VacationTypeConfigRetriever $configRetriever,
): JsonResponse {
/** @var User $user */
$user = User::query()->find($request->get("user"));
$types = VacationType::all()
->filter(fn(VacationType $type) => $configRetriever->isAvailableFor($type, $user->employment_form))
->map(fn(VacationType $type) => [
"label" => $type->label(),
"value" => $type->value,
])
->values();
return new JsonResponse($types);
}
}

View File

@ -7,30 +7,43 @@ namespace Toby\Infrastructure\Http\Controllers;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\Month; use Toby\Domain\Enums\Month;
use Toby\Domain\Enums\VacationType;
use Toby\Domain\TimesheetExport; use Toby\Domain\TimesheetExport;
use Toby\Domain\VacationTypeConfigRetriever;
use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Helpers\YearPeriodRetriever;
use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\User;
class TimesheetController extends Controller class TimesheetController extends Controller
{ {
public function __invoke(Month $month, YearPeriodRetriever $yearPeriodRetriever): BinaryFileResponse public function __invoke(
{ Month $month,
YearPeriodRetriever $yearPeriodRetriever,
VacationTypeConfigRetriever $configRetriever,
): BinaryFileResponse {
$this->authorize("generateTimesheet"); $this->authorize("generateTimesheet");
$yearPeriod = $yearPeriodRetriever->selected(); $yearPeriod = $yearPeriodRetriever->selected();
$carbonMonth = Carbon::create($yearPeriod->year, $month->toCarbonNumber()); $carbonMonth = Carbon::create($yearPeriod->year, $month->toCarbonNumber());
$users = User::query() $users = User::query()
->where("employment_form", EmploymentForm::EmploymentContract)
->orderBy("last_name") ->orderBy("last_name")
->orderBy("first_name") ->orderBy("first_name")
->get(); ->get();
$types = VacationType::all()
->filter(
fn(VacationType $type) => $configRetriever->isAvailableFor($type, EmploymentForm::EmploymentContract),
);
$filename = "{$carbonMonth->translatedFormat("F Y")}.xlsx"; $filename = "{$carbonMonth->translatedFormat("F Y")}.xlsx";
$timesheet = (new TimesheetExport()) $timesheet = (new TimesheetExport())
->forMonth($carbonMonth) ->forMonth($carbonMonth)
->forUsers($users); ->forUsers($users)
->forVacationTypes($types);
return Excel::download($timesheet, $filename); return Excel::download($timesheet, $filename);
} }

View File

@ -158,10 +158,7 @@ class VacationRequestController extends Controller
public function create(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response public function create(Request $request, YearPeriodRetriever $yearPeriodRetriever): Response
{ {
$yearPeriod = $yearPeriodRetriever->selected();
$users = User::query() $users = User::query()
->withVacationLimitIn($yearPeriod)
->orderBy("last_name") ->orderBy("last_name")
->orderBy("first_name") ->orderBy("first_name")
->get(); ->get();

View File

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

View File

@ -2,6 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Toby\Domain\Enums\EmploymentForm;
use Toby\Domain\Enums\VacationType; use Toby\Domain\Enums\VacationType;
use Toby\Domain\VacationTypeConfigRetriever; use Toby\Domain\VacationTypeConfigRetriever;
@ -11,53 +12,100 @@ return [
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true, VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => true, VacationTypeConfigRetriever::KEY_HAS_LIMIT => true,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
], ],
VacationType::OnRequest->value => [ VacationType::OnRequest->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true, VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true, VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => true, VacationTypeConfigRetriever::KEY_HAS_LIMIT => true,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
], ],
VacationType::TimeInLieu->value => [ VacationType::TimeInLieu->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => false, VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => false,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => false, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => false,
VacationTypeConfigRetriever::KEY_BILLABLE => true, VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false, VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
], ],
VacationType::Sick->value => [ VacationType::Sick->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => false, VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => false,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true, VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false, VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
], ],
VacationType::Unpaid->value => [ VacationType::Unpaid->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true, VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => false, VacationTypeConfigRetriever::KEY_BILLABLE => false,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false, VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
], ],
VacationType::Special->value => [ VacationType::Special->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true, VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => false, VacationTypeConfigRetriever::KEY_BILLABLE => false,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false, VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
], ],
VacationType::Childcare->value => [ VacationType::Childcare->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true, VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => false, VacationTypeConfigRetriever::KEY_BILLABLE => false,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false, VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
], ],
VacationType::Training->value => [ VacationType::Training->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true, VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true, VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false, VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
], ],
VacationType::Volunteering->value => [ VacationType::Volunteering->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true, VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true, VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true, VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false, VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
],
VacationType::Volunteering->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => true,
VacationTypeConfigRetriever::KEY_BILLABLE => true,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::EmploymentContract,
],
],
VacationType::Absence->value => [
VacationTypeConfigRetriever::KEY_TECHNICAL_APPROVAL => true,
VacationTypeConfigRetriever::KEY_ADMINISTRATIVE_APPROVAL => false,
VacationTypeConfigRetriever::KEY_BILLABLE => false,
VacationTypeConfigRetriever::KEY_HAS_LIMIT => false,
VacationTypeConfigRetriever::KEY_AVAILABLE_FOR => [
EmploymentForm::CommissionContract,
EmploymentForm::B2bContract,
EmploymentForm::BoardMemberContract,
],
], ],
]; ];

View File

@ -7,6 +7,7 @@ import CurrencyUsdOffIcon from 'vue-material-design-icons/CurrencyUsdOff.vue'
import HandHeartOutlineIcon from 'vue-material-design-icons/HandHeartOutline.vue' import HandHeartOutlineIcon from 'vue-material-design-icons/HandHeartOutline.vue'
import CalendarCheckIcon from 'vue-material-design-icons/CalendarCheck.vue' import CalendarCheckIcon from 'vue-material-design-icons/CalendarCheck.vue'
import MedicalBagIcon from 'vue-material-design-icons/MedicalBag.vue' import MedicalBagIcon from 'vue-material-design-icons/MedicalBag.vue'
import CalendarRemoveIcon from 'vue-material-design-icons/CalendarRemove.vue'
const types = [ const types = [
{ {
@ -63,6 +64,12 @@ const types = [
icon: MedicalBagIcon, icon: MedicalBagIcon,
color: 'text-rose-500', color: 'text-rose-500',
}, },
{
text: 'Nieobecność',
value: 'absence',
icon: CalendarRemoveIcon,
color: 'text-cyan-500',
},
] ]
export default function useVacationTypeInfo() { export default function useVacationTypeInfo() {

View File

@ -4,7 +4,7 @@
<div class="flex justify-between items-center p-4 sm:px-6"> <div class="flex justify-between items-center p-4 sm:px-6">
<div class="flex items-center"> <div class="flex items-center">
<h2 class="text-lg font-medium leading-6 text-gray-900"> <h2 class="text-lg font-medium leading-6 text-gray-900">
Wykorzystanie miesięczne urlopu wypoczynkowego Wykorzystanie miesięczne urlopu
</h2> </h2>
</div> </div>
</div> </div>

View File

@ -30,7 +30,7 @@
scope="col" scope="col"
class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap"
> >
Posiada urlop? Posiada limit?
</th> </th>
<th <th
scope="col" scope="col"

View File

@ -1,10 +1,10 @@
<template> <template>
<InertiaHead title="Złóż wniosek urlopowy" /> <InertiaHead title="Złóż wniosek" />
<div class="grid grid-cols-1 gap-4 items-start xl:grid-cols-3 xl:gap-8"> <div class="grid grid-cols-1 gap-4 items-start xl:grid-cols-3 xl:gap-8">
<div class="flex flex-col h-full bg-white shadow-md xl:col-span-2"> <div class="flex flex-col h-full bg-white shadow-md xl:col-span-2">
<div class="p-4 sm:px-6"> <div class="p-4 sm:px-6">
<h2 class="text-lg font-medium leading-6 text-gray-900"> <h2 class="text-lg font-medium leading-6 text-gray-900">
Złóż wniosek urlopowy Złóż wniosek
</h2> </h2>
</div> </div>
<form <form
@ -42,8 +42,8 @@
</ListboxLabel> </ListboxLabel>
<div class="relative mt-1 sm:col-span-2 sm:mt-0"> <div class="relative mt-1 sm:col-span-2 sm:mt-0">
<ListboxButton <ListboxButton
class="relative py-2 pr-10 pl-3 w-full max-w-lg text-left bg-white rounded-md border focus:ring-1 shadow-sm cursor-default sm:text-sm" class="relative py-2 pr-10 pl-3 w-full max-w-lg text-left bg-white rounded-md border focus:outline-none focus:ring-1 shadow-sm cursor-default sm:text-sm"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.user, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.user }" :class="{ 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500': form.errors.user, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.user }"
> >
<span class="flex items-center"> <span class="flex items-center">
<img <img
@ -63,7 +63,7 @@
leave-to-class="opacity-0" leave-to-class="opacity-0"
> >
<ListboxOptions <ListboxOptions
class="overflow-auto absolute z-10 py-1 mt-1 w-full max-w-lg max-h-60 text-base bg-white rounded-md focus:outline-none ring-1 ring-black ring-opacity-5 shadow-lg sm:text-sm" class="overflow-auto absolute z-10 py-1 mt-1 w-full max-w-lg max-h-60 text-base bg-white rounded-md focus:outline-none ring-1 focus:ring-blumilk-500 ring-opacity-5 shadow-lg sm:text-sm"
> >
<ListboxOption <ListboxOption
v-for="user in users.data" v-for="user in users.data"
@ -134,24 +134,29 @@
class="items-center py-4 sm:grid sm:grid-cols-3" class="items-center py-4 sm:grid sm:grid-cols-3"
> >
<ListboxLabel class="block text-sm font-medium text-gray-700"> <ListboxLabel class="block text-sm font-medium text-gray-700">
Rodzaj urlopu Typ wniosku
</ListboxLabel> </ListboxLabel>
<div class="relative mt-1 sm:col-span-2 sm:mt-0"> <div class="relative mt-1 sm:col-span-2 sm:mt-0">
<ListboxButton <ListboxButton
class="relative py-2 pr-10 pl-3 w-full max-w-lg text-left bg-white rounded-md border focus:ring-1 shadow-sm cursor-default sm:text-sm" class="relative py-2 pr-10 pl-3 w-full max-w-lg text-left bg-white rounded-md border focus:outline-none focus:ring-1 shadow-sm cursor-default sm:text-sm"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.type, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.type }" :class="{ 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500': form.errors.type, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.type }"
> >
<span class="block truncate">{{ form.vacationType.label }}</span> <template v-if="form.vacationType">
<span class="flex absolute inset-y-0 right-0 items-center pr-2 pointer-events-none"> <span class="block truncate">{{ form.vacationType.label }}</span>
<SelectorIcon class="w-5 h-5 text-gray-400" /> <span class="flex absolute inset-y-0 right-0 items-center pr-2 pointer-events-none">
</span> <SelectorIcon class="w-5 h-5 text-gray-400" />
</span>
</template>
<template v-else>
Ładowanie...
</template>
</ListboxButton> </ListboxButton>
<transition <transition
leave-active-class="transition ease-in duration-100" leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100" leave-from-class="opacity-100"
leave-to-class="opacity-0" leave-to-class="opacity-0"
> >
<ListboxOptions class="overflow-auto absolute z-10 py-1 mt-1 w-full max-w-lg max-h-60 text-base bg-white rounded-md focus:outline-none ring-1 ring-black ring-opacity-5 shadow-lg sm:text-sm"> <ListboxOptions class="overflow-auto absolute z-10 py-1 mt-1 w-full max-w-lg max-h-60 text-base bg-white rounded-md focus:outline-none ring-1 focus:ring-blumilk-500 ring-opacity-5 shadow-lg sm:text-sm">
<ListboxOption <ListboxOption
v-for="vacationType in vacationTypes" v-for="vacationType in vacationTypes"
:key="vacationType.value" :key="vacationType.value"
@ -187,7 +192,7 @@
for="date_from" for="date_from"
class="block text-sm font-medium text-gray-700 sm:mt-px" class="block text-sm font-medium text-gray-700 sm:mt-px"
> >
Planowany urlop od Data od
</label> </label>
<div class="mt-1 sm:col-span-2 sm:mt-0"> <div class="mt-1 sm:col-span-2 sm:mt-0">
<FlatPickr <FlatPickr
@ -212,7 +217,7 @@
for="date_from" for="date_from"
class="block text-sm font-medium text-gray-700 sm:mt-px" class="block text-sm font-medium text-gray-700 sm:mt-px"
> >
Planowany urlop do Data do
</label> </label>
<div class="mt-1 sm:col-span-2 sm:mt-0"> <div class="mt-1 sm:col-span-2 sm:mt-0">
<FlatPickr <FlatPickr
@ -233,7 +238,7 @@
</div> </div>
</div> </div>
<div class="items-center py-4 sm:grid sm:grid-cols-3"> <div class="items-center py-4 sm:grid sm:grid-cols-3">
<span class="block text-sm font-medium text-gray-700 sm:mt-px">Liczba dni urlopu</span> <span class="block text-sm font-medium text-gray-700 sm:mt-px">Liczba dni</span>
<div <div
class="inline-flex items-center py-2 px-4 mt-1 w-full max-w-lg text-gray-500 bg-gray-50 rounded-md border border-gray-300 sm:col-span-2 sm:mt-0 sm:text-sm" class="inline-flex items-center py-2 px-4 mt-1 w-full max-w-lg text-gray-500 bg-gray-50 rounded-md border border-gray-300 sm:col-span-2 sm:mt-0 sm:text-sm"
> >
@ -303,10 +308,10 @@
<div class="p-4 sm:px-6"> <div class="p-4 sm:px-6">
<h2 class="text-lg font-medium leading-6 text-gray-900"> <h2 class="text-lg font-medium leading-6 text-gray-900">
<span v-if="auth.user.id !== form.user.id"> <span v-if="auth.user.id !== form.user.id">
Urlop wypoczynkowy, dane dla: {{ form.user.name }} Dane urlopowe dla: {{ form.user.name }}
</span> </span>
<span v-else> <span v-else>
Twoje dane o urlopie wypoczynkowym Twoje dane o urlopie
</span> </span>
</h2> </h2>
</div> </div>
@ -330,7 +335,6 @@ import VacationChart from '@/Shared/VacationChart'
const props = defineProps({ const props = defineProps({
auth: Object, auth: Object,
users: Object, users: Object,
vacationTypes: Object,
holidays: Object, holidays: Object,
can: Object, can: Object,
}) })
@ -341,12 +345,13 @@ const form = useForm({
: props.auth.user, : props.auth.user,
from: null, from: null,
to: null, to: null,
vacationType: props.vacationTypes[0], vacationType: null,
comment: null, comment: null,
flowSkipped: false, flowSkipped: false,
}) })
const estimatedDays = ref([]) const estimatedDays = ref([])
const vacationTypes = ref([])
const stats = ref({ const stats = ref({
used: 0, used: 0,
@ -372,8 +377,9 @@ const toInputConfig = reactive({
watch(() => form.user, user => { watch(() => form.user, user => {
resetForm() resetForm()
refreshVacationStats(user) refreshAvailableTypes(user)
refreshUnavailableDays(user) refreshUnavailableDays(user)
refreshVacationStats(user)
}, { immediate: true }) }, { immediate: true })
function createForm() { function createForm() {
@ -406,7 +412,7 @@ function onToChange(selectedDates, dateStr) {
} }
function resetForm() { function resetForm() {
form.reset('type', 'to', 'from', 'comment', 'flowSkipped') form.reset('to', 'from', 'comment', 'flowSkipped')
form.clearErrors() form.clearErrors()
estimatedDays.value = [] estimatedDays.value = []
} }
@ -440,4 +446,11 @@ async function refreshUnavailableDays(user) {
] ]
} }
async function refreshAvailableTypes(user) {
const res = await axios.post('/api/vacation/get-available-vacation-types', { user: user.id })
vacationTypes.value = res.data
form.vacationType = vacationTypes.value[0]
}
</script> </script>

View File

@ -1,9 +1,9 @@
<template> <template>
<InertiaHead title="Moje wnioski urlopowe" /> <InertiaHead title="Moje wnioski" />
<div class="bg-white shadow-md"> <div class="bg-white shadow-md">
<div class="flex justify-between items-center p-4 sm:px-6"> <div class="flex justify-between items-center p-4 sm:px-6">
<h2 class="text-lg font-medium leading-6 text-gray-900"> <h2 class="text-lg font-medium leading-6 text-gray-900">
Moje wnioski urlopowe Moje wnioski
</h2> </h2>
<div> <div>
<InertiaLink <InertiaLink
@ -102,7 +102,7 @@
scope="col" scope="col"
class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap"
> >
Rodzaj urlopu Rodzaj wniosku
</th> </th>
<th <th
scope="col" scope="col"

View File

@ -1,10 +1,10 @@
<template> <template>
<InertiaHead title="Wnioski urlopowe" /> <InertiaHead title="Lista wniosków" />
<div class="bg-white shadow-md"> <div class="bg-white shadow-md">
<div class="flex justify-between items-center p-4 sm:px-6"> <div class="flex justify-between items-center p-4 sm:px-6">
<div> <div>
<h2 class="text-lg font-medium leading-6 text-gray-900"> <h2 class="text-lg font-medium leading-6 text-gray-900">
Wnioski urlopowe Lista wniosków
</h2> </h2>
</div> </div>
<div> <div>
@ -181,7 +181,7 @@
scope="col" scope="col"
class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap" class="py-3 px-4 text-xs font-semibold tracking-wider text-left text-gray-500 uppercase whitespace-nowrap"
> >
Rodzaj urlopu Rodzaj wniosku
</th> </th>
<th <th
scope="col" scope="col"

View File

@ -41,7 +41,7 @@
</div> </div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6"> <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
<dt class="text-sm font-medium text-gray-500"> <dt class="text-sm font-medium text-gray-500">
Rodzaj urlopu Rodzaj wniosku
</dt> </dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0"> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<VacationType :type="request.type" /> <VacationType :type="request.type" />

View File

@ -301,7 +301,7 @@ const navigation = computed(() =>
can: true, can: true,
}, },
{ {
name: 'Wnioski urlopowe', name: 'Lista wniosków',
href: '/vacation/requests', href: '/vacation/requests',
component: 'VacationRequest/IndexForApprovers', component: 'VacationRequest/IndexForApprovers',
icon: CollectionIcon, icon: CollectionIcon,

View File

@ -14,6 +14,7 @@
"look_for_work_vacation": "Urlop na poszukiwanie pracy", "look_for_work_vacation": "Urlop na poszukiwanie pracy",
"time_in_lieu": "Odbiór za święto", "time_in_lieu": "Odbiór za święto",
"sick_vacation": "Zwolnienie lekarskie", "sick_vacation": "Zwolnienie lekarskie",
"absence": "Nieobecność",
"employee": "Pracownik", "employee": "Pracownik",
"administrator": "Administrator", "administrator": "Administrator",
"technical_approver": "Techniczny akceptujący", "technical_approver": "Techniczny akceptujący",

View File

@ -6,9 +6,11 @@ use Illuminate\Support\Facades\Route;
use Toby\Infrastructure\Http\Controllers\Api\CalculateUserUnavailableDaysController; use Toby\Infrastructure\Http\Controllers\Api\CalculateUserUnavailableDaysController;
use Toby\Infrastructure\Http\Controllers\Api\CalculateUserVacationStatsController; use Toby\Infrastructure\Http\Controllers\Api\CalculateUserVacationStatsController;
use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController; use Toby\Infrastructure\Http\Controllers\Api\CalculateVacationDaysController;
use Toby\Infrastructure\Http\Controllers\Api\GetAvailableVacationTypesController;
Route::middleware("auth:sanctum")->group(function (): void { Route::middleware("auth:sanctum")->group(function (): void {
Route::post("vacation/calculate-days", CalculateVacationDaysController::class); Route::post("vacation/calculate-days", CalculateVacationDaysController::class);
Route::post("vacation/calculate-stats", CalculateUserVacationStatsController::class); Route::post("vacation/calculate-stats", CalculateUserVacationStatsController::class);
Route::post("vacation/calculate-unavailable-days", CalculateUserUnavailableDaysController::class); Route::post("vacation/calculate-unavailable-days", CalculateUserUnavailableDaysController::class);
Route::post("vacation/get-available-vacation-types", GetAvailableVacationTypesController::class);
}); });

View File

@ -6,6 +6,7 @@ namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\FeatureTestCase; use Tests\FeatureTestCase;
use Toby\Domain\Enums\EmploymentForm;
use Toby\Eloquent\Models\User; use Toby\Eloquent\Models\User;
class VacationCalendarTest extends FeatureTestCase class VacationCalendarTest extends FeatureTestCase
@ -16,6 +17,10 @@ class VacationCalendarTest extends FeatureTestCase
{ {
$administrativeApprover = User::factory()->administrativeApprover()->create(); $administrativeApprover = User::factory()->administrativeApprover()->create();
User::factory(["employment_form" => EmploymentForm::EmploymentContract])
->count(10)
->create();
$this->actingAs($administrativeApprover) $this->actingAs($administrativeApprover)
->get("/vacation/timesheet/january") ->get("/vacation/timesheet/january")
->assertOk(); ->assertOk();