Skip to content

Commit 1b1264b

Browse files
authored
#377 - calendar months dropdown (#468)
* #377 - refactor: refactored calendar month buttons - added dropdown with months * #377 - feat: added changing year from calendar buttons * #377 - fix: fixed icons * #377 - fix: linter fixes * #377 - fix: js linter node version update * #377 - fix: js linter node version update 2 * #377 - fix: js linter node version update 3 * #377 - feat: added tests
1 parent 54ad119 commit 1b1264b

File tree

8 files changed

+326
-182
lines changed

8 files changed

+326
-182
lines changed

.github/workflows/test-and-lint-js.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- name: Set up node
3636
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
3737
with:
38-
node-version: 22
38+
node-version: 22.4.1
3939

4040
- name: Instal npm dependencies
4141
run: npm clean-install

app/Http/Controllers/VacationCalendarController.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44

55
namespace Toby\Http\Controllers;
66

7+
use Illuminate\Http\RedirectResponse;
78
use Illuminate\Http\Request;
89
use Illuminate\Support\Carbon;
10+
use Illuminate\Support\Facades\Cache;
911
use Inertia\Response;
1012
use Toby\Domain\CalendarGenerator;
1113
use Toby\Enums\Month;
1214
use Toby\Helpers\YearPeriodRetriever;
1315
use Toby\Http\Resources\SimpleUserResource;
1416
use Toby\Models\User;
17+
use Toby\Models\YearPeriod;
1518

1619
class VacationCalendarController extends Controller
1720
{
@@ -20,12 +23,25 @@ public function index(
2023
YearPeriodRetriever $yearPeriodRetriever,
2124
CalendarGenerator $calendarGenerator,
2225
?string $month = null,
23-
): Response {
26+
?int $year = null,
27+
): Response|RedirectResponse {
28+
if ($year !== null) {
29+
return $this->changeYearPeriod($request, $month, $year);
30+
}
31+
2432
$month = Month::fromNameOrCurrent((string)$month);
2533
$currentUser = $request->user();
2634
$withTrashedUsers = $currentUser->canSeeInactiveUsers();
2735

2836
$yearPeriod = $yearPeriodRetriever->selected();
37+
$previousYearPeriod = YearPeriod::query()
38+
->where("year", "<", $yearPeriod->year)
39+
->orderBy("year", "desc")
40+
->first();
41+
$nextYearPeriod = YearPeriod::query()
42+
->where("year", ">", $yearPeriod->year)
43+
->orderBy("year")
44+
->first();
2945
$carbonMonth = Carbon::create($yearPeriod->year, $month->toCarbonNumber());
3046

3147
$users = User::query()
@@ -42,9 +58,21 @@ public function index(
4258
return inertia("Calendar", [
4359
"calendar" => $calendar,
4460
"current" => Month::current(),
45-
"selected" => $month->value,
61+
"selectedMonth" => $month->value,
4662
"users" => SimpleUserResource::collection($users),
4763
"withBlockedUsers" => $withTrashedUsers,
64+
"previousYearPeriod" => $previousYearPeriod,
65+
"nextYearPeriod" => $nextYearPeriod,
4866
]);
4967
}
68+
69+
private function changeYearPeriod(Request $request, string $month, int $year): RedirectResponse
70+
{
71+
$yearPeriod = YearPeriod::query()->where("year", $year)->firstOrFail();
72+
$request->session()->put(YearPeriodRetriever::SESSION_KEY, $yearPeriod->id);
73+
Cache::forget("selected_year_period");
74+
75+
return redirect()->route("calendar", ["month" => $month])
76+
->with("info", __("Year period changed."));
77+
}
5078
}

lang/pl.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"Holiday updated.": "Dzień wolny zaktualizowany.",
4242
"Holiday deleted.": "Dzień wolny usunięty.",
4343
"Selected year period changed.": "Wybrany rok zmieniony.",
44+
"Year period changed.": "Zmieniono rok.",
4445
"Vacation limits updated.": "Limity urlopów zaktualizowane.",
4546
"Request created.": "Wniosek utworzony.",
4647
"Request accepted.": "Wniosek zaakceptowany.",

package-lock.json

Lines changed: 145 additions & 157 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"@heroicons/vue": "^2.1.4",
1212
"@inertiajs/inertia": "^0.11.1",
1313
"@inertiajs/inertia-vue3": "^0.6.0",
14-
"@inertiajs/progress": "^0.2.7",
14+
"@inertiajs/progress": "^0.1.2",
1515
"@tailwindcss/forms": "^0.5.7",
1616
"@tailwindcss/line-clamp": "^0.4.4",
1717
"@tailwindcss/typography": "^0.5.13",

resources/js/Pages/Calendar.vue

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
<script setup>
2-
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/vue/24/solid'
3-
import { computed, ref } from 'vue'
2+
import { ChevronLeftIcon, ChevronRightIcon, ChevronDoubleRightIcon, ChevronDoubleLeftIcon, ChevronUpDownIcon } from '@heroicons/vue/24/solid'
3+
import { computed, ref, watch } from 'vue'
44
import { useMonthInfo } from '@/Composables/monthInfo.js'
55
import VacationTypeCalendarIcon from '@/Shared/VacationTypeCalendarIcon.vue'
66
import CalendarDay from '@/Shared/CalendarDay.vue'
77
import UserProfileLink from '@/Shared/UserProfileLink.vue'
8+
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/vue'
9+
import { useForm } from '@inertiajs/inertia-vue3'
10+
import { Inertia } from '@inertiajs/inertia'
811
912
const props = defineProps({
1013
users: Object,
1114
auth: Object,
1215
calendar: Object,
1316
current: String,
14-
selected: String,
17+
selectedMonth: String,
1518
years: Object,
19+
previousYearPeriod: Object,
20+
nextYearPeriod: Object,
1621
})
1722
1823
let activeElement = ref(undefined)
@@ -21,8 +26,17 @@ const { getMonths, findMonth } = useMonthInfo()
2126
2227
const months = getMonths()
2328
24-
const currentMonth = computed(() => findMonth(props.current))
25-
const selectedMonth = computed(() => findMonth(props.selected))
29+
const form = useForm({
30+
selectedMonth: months.find(month => month.value === props.selectedMonth),
31+
})
32+
33+
watch(() => form.selectedMonth, (value) => {
34+
if (value) {
35+
Inertia.visit(`/calendar/${value.value}`)
36+
}
37+
})
38+
39+
const selectedMonth = computed(() => findMonth(props.selectedMonth))
2640
const previousMonth = computed(() => months[months.indexOf(selectedMonth.value) - 1])
2741
const nextMonth = computed(() => months[months.indexOf(selectedMonth.value) + 1])
2842
@@ -52,33 +66,76 @@ function linkVacationRequest(user) {
5266
<InertiaHead title="Kalendarz" />
5367
<div class="bg-white shadow-md">
5468
<div class="flex-row sm:flex justify-between items-center p-4 sm:px-6">
55-
<div class="flex items-center">
56-
<h2 class="text-lg font-medium leading-6 text-center text-gray-900">
69+
<div class="flex-row sm:flex items-center">
70+
<h2 class="text-lg font-medium leading-6 sm:text-center mb-2 sm:mb-0 text-gray-900">
5771
Kalendarz
5872
</h2>
59-
<div class="flex items-center ml-3 rounded-md shadow-sm md:items-stretch">
73+
<div class="flex items-center sm:ml-3 md:items-stretch">
6074
<InertiaLink
6175
v-if="previousMonth"
6276
:href="`/calendar/${previousMonth.value}`"
6377
as="button"
64-
class="flex focus:relative justify-center items-center p-2 text-gray-400 hover:text-gray-500 bg-white rounded-l-md border border-r-0 border-gray-300 focus:outline-blumilk-500 md:px-2 md:w-9 md:hover:bg-gray-50"
78+
class="flex focus:relative justify-center items-center p-2 text-gray-400 hover:text-gray-500 bg-white rounded-l-md border border-r-0 border-gray-300 md:px-2 md:w-9 md:hover:bg-gray-50"
6579
>
6680
<ChevronLeftIcon class="w-5 h-5" />
6781
</InertiaLink>
82+
<InertiaLink
83+
v-else-if="previousYearPeriod"
84+
:href="`/calendar/${months[11].value}/${previousYearPeriod.year}`"
85+
as="button"
86+
class="flex focus:relative justify-center items-center p-2 text-gray-400 hover:text-gray-500 bg-white rounded-l-md border border-r-0 border-gray-300 md:px-2 md:w-9 md:hover:bg-gray-50"
87+
>
88+
<ChevronDoubleLeftIcon class="w-5 h-5" />
89+
</InertiaLink>
6890
<span
6991
v-else
70-
class="flex justify-center items-center p-2 text-gray-400 bg-gray-100 rounded-l-md border border-r-0 border-gray-300 md:px-2 md:w-9"
92+
class="flex justify-center items-center text-gray-400 bg-gray-100 rounded-l-md border border-r-0 border-gray-300 md:px-2 md:w-9"
7193
>
72-
<ChevronLeftIcon class="w-5 h-5" />
94+
<ChevronDoubleLeftIcon class="w-5 h-5" />
7395
</span>
74-
<InertiaLink
75-
v-if="years.current.year === years.selected.year"
76-
:href="`/calendar/${currentMonth.value}`"
77-
as="button"
78-
class="hidden focus:relative items-center p-2 text-sm font-medium text-gray-700 hover:text-gray-900 bg-white hover:bg-gray-50 border-y border-gray-300 focus:outline-blumilk-500 md:flex"
96+
<Listbox
97+
v-model="form.selectedMonth"
98+
as="div"
99+
class="items-center grid-cols-3 w-[135px] h-[] text-sm font-medium text-gray-700 hover:text-gray-900 bg-white hover:bg-gray-50 border-y border-gray-300 focus:outline-blumilk-500"
79100
>
80-
Dzisiaj
81-
</InertiaLink>
101+
<div class="relative sm:col-span-2 sm:mt-0">
102+
<ListboxButton
103+
class="relative pr-10 pl-3 w-full h-[36px] max-w-lg text-left bg-white focus:outline-none shadow-sm sm:text-sm cursor-pointer"
104+
>
105+
<template v-if="form.selectedMonth">
106+
<span class="block truncate text-center">
107+
{{ form.selectedMonth.name }}
108+
</span>
109+
<span class="flex absolute inset-y-0 right-0 items-center pr-2 pointer-events-none">
110+
<ChevronUpDownIcon class="w-5 h-5 text-gray-400" />
111+
</span>
112+
</template>
113+
</ListboxButton>
114+
<transition
115+
leave-active-class="transition ease-in duration-100"
116+
leave-from-class="opacity-100"
117+
leave-to-class="opacity-0"
118+
>
119+
<ListboxOptions
120+
class="overflow-auto absolute z-10 py-1 mt-1 w-auto 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"
121+
>
122+
<ListboxOption
123+
v-for="month in months"
124+
:key="month.value"
125+
v-slot="{ active, selected }"
126+
:value="month"
127+
as="template"
128+
>
129+
<li :class="[active ? 'bg-gray-100' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9 cursor-pointer']">
130+
<span :class="[selected ? 'font-semibold' : 'font-normal', 'block truncate']">
131+
{{ month.name }}
132+
</span>
133+
</li>
134+
</ListboxOption>
135+
</ListboxOptions>
136+
</transition>
137+
</div>
138+
</Listbox>
82139
<InertiaLink
83140
v-if="nextMonth"
84141
:href="`/calendar/${nextMonth.value}`"
@@ -87,11 +144,19 @@ function linkVacationRequest(user) {
87144
>
88145
<ChevronRightIcon class="w-5 h-5" />
89146
</InertiaLink>
147+
<InertiaLink
148+
v-else-if="nextYearPeriod"
149+
:href="`/calendar/${months[0].value}/${nextYearPeriod.year}`"
150+
as="button"
151+
class="flex focus:relative justify-center items-center p-2 text-gray-400 hover:text-gray-500 bg-white rounded-r-md border border-l-0 border-gray-300 focus:outline-blumilk-500 md:px-2 md:w-9 md:hover:bg-gray-50"
152+
>
153+
<ChevronDoubleRightIcon class="w-5 h-5" />
154+
</InertiaLink>
90155
<span
91156
v-else
92-
class="flex justify-center items-center p-2 text-gray-400 bg-gray-100 rounded-r-md border border-l-0 border-gray-300 md:px-2 md:w-9"
157+
class="flex justify-center items-center text-gray-400 bg-gray-100 rounded-r-md border border-l-0 border-gray-300 md:px-2 md:w-9"
93158
>
94-
<ChevronRightIcon class="w-5 h-5" />
159+
<ChevronDoubleRightIcon class="w-5 h-5" />
95160
</span>
96161
</div>
97162
</div>

routes/web.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
->whereNumber("yearPeriod")
130130
->name("year-periods.select");
131131

132-
Route::get("/calendar/{month?}", [VacationCalendarController::class, "index"])
132+
Route::get("/calendar/{month?}/{year?}", [VacationCalendarController::class, "index"])
133133
->name("calendar");
134134

135135
Route::prefix("/vacation")->as("vacation.")->group(function (): void {

tests/Feature/CalendarTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Feature;
6+
7+
use Illuminate\Foundation\Testing\DatabaseMigrations;
8+
use Tests\FeatureTestCase;
9+
use Toby\Models\User;
10+
use Toby\Models\YearPeriod;
11+
12+
class CalendarTest extends FeatureTestCase
13+
{
14+
use DatabaseMigrations;
15+
16+
protected User $user;
17+
18+
protected function setUp(): void
19+
{
20+
parent::setUp();
21+
22+
$this->user = User::factory()->create();
23+
}
24+
25+
public function testUserCanChangeYearPeriodOnTheCalendar(): void
26+
{
27+
$currentYearPeriod = YearPeriod::current();
28+
$nextYearPeriod = YearPeriod::query()
29+
->create([
30+
"year" => $currentYearPeriod->year + 1,
31+
]);
32+
33+
$this->assertNotEquals($currentYearPeriod->year, $nextYearPeriod->year);
34+
35+
$this->actingAs($this->user)
36+
->get("/calendar/january/$nextYearPeriod->year")
37+
->assertRedirect();
38+
39+
$selectedYearPeriod = YearPeriod::query()->where("id", session()->get("selected_year_period"))->first();
40+
$this->assertEquals($selectedYearPeriod->year, $nextYearPeriod->year);
41+
}
42+
43+
public function testUserCannotChangeYearPeriodIfYearPeriodDoesNotExist(): void
44+
{
45+
$currentYearPeriod = YearPeriod::current();
46+
47+
$this->actingAs($this->user)
48+
->get("/calendar/january/" . ($currentYearPeriod->year + 1))
49+
->assertStatus(404);
50+
51+
$this->actingAs($this->user)
52+
->get("/calendar/january/$currentYearPeriod->year")
53+
->assertStatus(302);
54+
}
55+
56+
public function testUserCanSeeCalendar(): void
57+
{
58+
$this->actingAs($this->user)
59+
->get("/calendar")
60+
->assertOk();
61+
}
62+
}

0 commit comments

Comments
 (0)