This commit is contained in:
Adrian Hopek 2022-03-07 13:05:49 +01:00
parent 5d64ad9d2a
commit b2f08f61b7
10 changed files with 137 additions and 59 deletions

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Toby\Domain; namespace Toby\Domain;
use Toby\Domain\Enums\Role;
use Toby\Domain\States\VacationRequest\AcceptedByAdministrative; use Toby\Domain\States\VacationRequest\AcceptedByAdministrative;
use Toby\Domain\States\VacationRequest\AcceptedByTechnical; use Toby\Domain\States\VacationRequest\AcceptedByTechnical;
use Toby\Domain\States\VacationRequest\Approved; use Toby\Domain\States\VacationRequest\Approved;
@ -12,6 +13,7 @@ use Toby\Domain\States\VacationRequest\Created;
use Toby\Domain\States\VacationRequest\Rejected; use Toby\Domain\States\VacationRequest\Rejected;
use Toby\Domain\States\VacationRequest\WaitingForAdministrative; use Toby\Domain\States\VacationRequest\WaitingForAdministrative;
use Toby\Domain\States\VacationRequest\WaitingForTechnical; use Toby\Domain\States\VacationRequest\WaitingForTechnical;
use Toby\Eloquent\Models\User;
class VacationRequestStatesRetriever class VacationRequestStatesRetriever
{ {
@ -39,6 +41,16 @@ class VacationRequestStatesRetriever
]; ];
} }
public static function waitingForUserActionStates(User $user): array
{
return match ($user->role) {
Role::AdministrativeApprover => [WaitingForAdministrative::class],
Role::TechnicalApprover => [WaitingForTechnical::class],
Role::Administrator => [WaitingForAdministrative::class, WaitingForTechnical::class],
default => [],
};
}
public static function all(): array public static function all(): array
{ {
return [ return [
@ -48,12 +60,13 @@ class VacationRequestStatesRetriever
]; ];
} }
public static function filterByStatusGroup(string $filter): array public static function filterByStatusGroup(string $filter, ?User $user = null): array
{ {
return match ($filter) { return match ($filter) {
"pending" => self::pendingStates(), "pending" => self::pendingStates(),
"success" => self::successStates(), "success" => self::successStates(),
"failed" => self::failedStates(), "failed" => self::failedStates(),
"waiting_for_action" => self::waitingForUserActionStates($user),
default => self::all(), default => self::all(),
}; };
} }

View File

@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Inertia\Response; use Inertia\Response;
use Toby\Domain\Enums\Role;
use Toby\Domain\UserVacationStatsRetriever; use Toby\Domain\UserVacationStatsRetriever;
use Toby\Domain\VacationRequestStatesRetriever; use Toby\Domain\VacationRequestStatesRetriever;
use Toby\Eloquent\Models\Holiday; use Toby\Eloquent\Models\Holiday;
@ -35,10 +36,18 @@ class DashboardController extends Controller
) )
->get(); ->get();
if ($user->role !== Role::Employee) {
$vacationRequests = VacationRequest::query() $vacationRequests = VacationRequest::query()
->states(VacationRequestStatesRetriever::waitingForUserActionStates($user))
->latest("updated_at") ->latest("updated_at")
->limit(3) ->limit(3)
->get(); ->get();
} else {
$vacationRequests = $user->vacationRequests()
->latest("updated_at")
->limit(3)
->get();
}
$holidays = Holiday::query() $holidays = Holiday::query()
->whereDate("date", ">=", $now) ->whereDate("date", ">=", $now)

View File

@ -40,7 +40,7 @@ class VacationRequestController extends Controller
->with("vacations") ->with("vacations")
->where("year_period_id", $yearPeriodRetriever->selected()->id) ->where("year_period_id", $yearPeriodRetriever->selected()->id)
->latest() ->latest()
->states(VacationRequestStatesRetriever::filterByStatusGroup($status)) ->states(VacationRequestStatesRetriever::filterByStatusGroup($status, $request->user()))
->paginate(); ->paginate();
return inertia("VacationRequest/Index", [ return inertia("VacationRequest/Index", [
@ -67,7 +67,7 @@ class VacationRequestController extends Controller
->with(["user", "vacations"]) ->with(["user", "vacations"])
->where("year_period_id", $yearPeriod->id) ->where("year_period_id", $yearPeriod->id)
->when($user !== null, fn(Builder $query) => $query->where("user_id", $user)) ->when($user !== null, fn(Builder $query) => $query->where("user_id", $user))
->when($status !== null, fn(Builder $query) => $query->states([$status])) ->when($status !== null, fn(Builder $query) => $query->states(VacationRequestStatesRetriever::filterByStatusGroup($status, $request->user())))
->latest() ->latest()
->paginate(); ->paginate();

View File

@ -16,7 +16,8 @@ class UserResource extends JsonResource
"id" => $this->id, "id" => $this->id,
"name" => $this->fullName, "name" => $this->fullName,
"email" => $this->email, "email" => $this->email,
"role" => $this->role->label(), "displayRole" => $this->role->label(),
"role" => $this->role,
"position" => $this->position, "position" => $this->position,
"avatar" => $this->getAvatar(), "avatar" => $this->getAvatar(),
"deleted" => $this->trashed(), "deleted" => $this->trashed(),

View File

@ -11,7 +11,6 @@
<img <img
class="mx-auto h-20 w-20 rounded-full" class="mx-auto h-20 w-20 rounded-full"
:src="user.avatar" :src="user.avatar"
alt=""
> >
</div> </div>
<div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left"> <div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left">
@ -22,7 +21,7 @@
{{ user.name }} {{ user.name }}
</p> </p>
<p class="text-sm font-medium text-gray-600"> <p class="text-sm font-medium text-gray-600">
{{ user.role }} {{ user.displayRole }}
</p> </p>
</div> </div>
</div> </div>
@ -98,7 +97,7 @@
</section> </section>
</div> </div>
<div class="grid grid-cols-1 gap-4"> <div class="grid grid-cols-1 gap-4">
<section> <section v-if="user.role === 'employee'">
<div class="bg-white shadow-md"> <div class="bg-white shadow-md">
<div class="p-4 sm:px-6"> <div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900"> <h2 class="text-lg leading-6 font-medium text-gray-900">
@ -134,9 +133,70 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="mt-6">
<InertiaLink
href="/vacation-requests/me"
class="w-full flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Zobacz wszystkie
</InertiaLink>
</div>
</div>
</div>
</section>
<section v-else>
<div class="bg-white shadow-md">
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Wnioski oczekujące na akcje
</h2>
</div>
<div class="border-t border-gray-200 pb-5 px-4 sm:px-6">
<div class="flow-root mt-6">
<ul class="-my-5 divide-y divide-gray-200">
<li
v-for="request in vacationRequests.data"
:key="request.id"
class="py-5"
>
<div class="relative focus-within:ring-2 focus-within:ring-cyan-500">
<h3 class="text-sm font-semibold text-blumilk-600 hover:text-blumilk-500">
<InertiaLink
:href="`/vacation-requests/${request.id}`"
class="hover:underline focus:outline-none"
>
<span class="absolute inset-0" />
Wniosek o {{ request.type.toLowerCase() }}
[{{ request.name }}]
</InertiaLink>
</h3>
<p class="mt-1 text-sm text-gray-600">
{{ request.from }} - {{ request.to }}
</p>
<div class="mt-3 text-sm text-gray-600">
<div class="flex">
<img
class="h-10 w-10 rounded-full"
:src="request.user.avatar"
>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900">
{{ request.user.name }}
</p>
<p class="text-sm text-gray-500">
{{ request.user.email }}
</p>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="mt-6"> <div class="mt-6">
<InertiaLink <InertiaLink
href="/vacation-requests" href="/vacation-requests"
:data="{status: 'waiting_for_action'}"
class="w-full flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50" class="w-full flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
> >
Zobacz wszystkie Zobacz wszystkie

View File

@ -101,7 +101,7 @@
</div> </div>
</td> </td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.role }} {{ user.displayRole }}
</td> </td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.position }} {{ user.position }}

View File

@ -9,7 +9,7 @@
</div> </div>
<div> <div>
<InertiaLink <InertiaLink
href="vacation-requests/create" 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" 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 Dodaj wniosek

View File

@ -123,14 +123,10 @@
<ListboxButton <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 focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300" 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 focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300"
> >
<span v-if="form.status === null">
Wszystkie
</span>
<span <span
v-else
class="flex items-center" class="flex items-center"
> >
{{ form.status.text }} {{ form.status.name }}
</span> </span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <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" /> <SelectorIcon class="h-5 w-5 text-gray-400" />
@ -145,40 +141,20 @@
<ListboxOptions <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" 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-slot="{ active }"
as="template"
:value="null"
>
<li
:class="[active ? 'text-white bg-blumilk-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']"
>
<div class="flex items-center">
Wszystkie statusy
</div>
<span
v-if="form.status === null"
: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>
<ListboxOption <ListboxOption
v-for="status in statuses" v-for="status in statuses"
:key="status.value" :key="status.value"
v-slot="{ active }" v-slot="{ active, selected }"
as="template" as="template"
:value="status" :value="status"
> >
<li <li
:class="[active ? 'text-white bg-blumilk-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']" :class="[active ? 'text-white bg-blumilk-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']"
> >
{{ status.text }} {{ status.name }}
<span <span
v-if="form.status?.value === status.value" v-if="selected"
:class="[active ? 'text-white' : 'text-blumilk-600', 'absolute inset-y-0 right-0 flex items-center pr-4']" :class="[active ? 'text-white' : 'text-blumilk-600', 'absolute inset-y-0 right-0 flex items-center pr-4']"
> >
<CheckIcon class="h-5 w-5" /> <CheckIcon class="h-5 w-5" />
@ -255,17 +231,18 @@
</InertiaLink> </InertiaLink>
</td> </td>
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-500"> <td class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-500">
<div class="flex justify-start items-center"> <div class="flex justify-center items-center">
<span class="inline-flex items-center justify-center h-10 w-10 rounded-full">
<img <img
class="h-10 w-10 rounded-full" class="h-10 w-10 rounded-full"
:src="request.user.avatar" :src="request.user.avatar"
> >
</span>
<div class="ml-3"> <div class="ml-3">
<div class="text-sm font-medium text-gray-500"> <p class="text-sm font-medium text-gray-900">
{{ request.user.name }} {{ request.user.name }}
</div> </p>
<p class="text-sm text-gray-500">
{{ request.user.email }}
</p>
</div> </div>
</div> </div>
</td> </td>
@ -376,7 +353,6 @@ import {watch, reactive} from 'vue'
import {debounce} from 'lodash' import {debounce} from 'lodash'
import {Inertia} from '@inertiajs/inertia' import {Inertia} from '@inertiajs/inertia'
import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions} from '@headlessui/vue' import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions} from '@headlessui/vue'
import {useStatusInfo} from '@/Composables/statusInfo'
export default { export default {
name: 'VacationRequestIndex', name: 'VacationRequestIndex',
@ -415,17 +391,36 @@ export default {
}, },
}, },
setup(props) { setup(props) {
const {getStatues, findStatus} = useStatusInfo() const statuses = [
{
name: 'Wszystkie',
value: 'all',
},
{
name: 'Oczekujące na akcje',
value: 'waiting_for_action',
},
{
name: 'W trakcie',
value: 'pending',
},
{
name: 'Zatwierdzone',
value: 'success',
},
{
name: 'Odrzucone/anulowane',
value: 'failed',
},
]
const form = reactive({ const form = reactive({
user: props.users.data.find(user => user.id === props.filters.user) ?? null, user: props.users.data.find(user => user.id === props.filters.user) ?? null,
status: findStatus(props.filters.status) ?? null, status: statuses.find(status => status.value === props.filters.status) ?? statuses[0],
}) })
const statuses = getStatues()
watch(form, debounce(() => { watch(form, debounce(() => {
Inertia.get('/vacation-requests', {user: form.user?.id, status: form.status?.value}, { Inertia.get('/vacation-requests', {user: form.user?.id, status: form.status.value}, {
preserveState: true, preserveState: true,
replace: true, replace: true,
}) })

View File

@ -74,7 +74,7 @@
v-for="item in navigation" v-for="item in navigation"
:key="item.name" :key="item.name"
:href="item.href" :href="item.href"
:class="[$page.url.startsWith(item.href) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']" :class="[$page.url === item.href ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']"
> >
<component <component
:is="item.icon" :is="item.icon"
@ -119,7 +119,7 @@
v-for="item in navigation" v-for="item in navigation"
:key="item.name" :key="item.name"
:href="item.href" :href="item.href"
:class="[$page.url.startsWith(item.href) ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md']" :class="[$page.url === item.href ? 'bg-blumilk-800 text-white' : 'text-blumilk-100 hover:text-white hover:bg-blumilk-600', 'group flex items-center px-2 py-2 text-sm leading-6 font-medium rounded-md']"
> >
<component <component
:is="item.icon" :is="item.icon"

View File

@ -49,7 +49,7 @@ class VacationRequestTest extends FeatureTestCase
->create(); ->create();
$this->actingAs($user) $this->actingAs($user)
->get("/vacation-requests") ->get("/vacation-requests/me")
->assertOk() ->assertOk()
->assertInertia( ->assertInertia(
fn(Assert $page) => $page fn(Assert $page) => $page