Files
htwkalender/frontend/src/view/FreeRooms.vue
2024-04-02 16:24:14 +02:00

294 lines
8.3 KiB
Vue

<!--
Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
Copyright (C) 2024 HTWKalender support@htwkalender.de
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<DynamicPage
:hide-content="availableRooms.length === 0"
:headline="$t('freeRooms.freeRooms')"
:sub-title="$t('freeRooms.detail')"
icon="pi pi-search"
:button="{
label: $t('freeRooms.search'),
icon: 'pi pi-search',
disabled: isLater,
onClick: loadFreeRooms,
}"
>
<template #selection="{ flexSpecs }">
<Calendar
v-model="date"
:class="flexSpecs"
:placeholder="$t('freeRooms.pleaseSelectDate')"
:empty-message="$t('roomFinderPage.noRoomsAvailable')"
date-format="dd.mm.yy"
panel-class="min-w-min"
touch-u-i
/>
<div class="break" />
<Calendar
v-if="mobilePage"
v-model="start"
type="time"
placeholder="start"
time-only
hour-format="24"
date-format="HH:mm"
:class="[{ 'p-invalid': isLater }, flexSpecs]"
panel-class="min-w-min"
touch-u-i
@update:model-value="
() => {
timeRange[0] = start.getHours() * 60 + start.getMinutes();
}
"
/>
<Calendar
v-if="mobilePage"
v-model="end"
type="time"
time-only
hour-format="24"
placeholder="end"
date-format="HH:mm"
:class="[{ 'p-invalid': isLater }, flexSpecs]"
panel-class="min-w-min"
touch-u-i
@update:model-value="
() => {
timeRange[1] = end.getHours() * 60 + end.getMinutes();
}
"
/>
<div
v-if="!mobilePage"
class="flex-grow-1 relative mb-2"
:class="flexSpecs"
>
<Tag
:value="formatTime(start)"
class="opacity-0 pointer-events-none text-xl lg:text-base"
/>
<Tag
:value="formatTime(start)"
class="absolute time-tag text-xl lg:text-base"
:style="{ left: timeToPercentString(timeRange[0]) }"
:class="startMoved.value ? 'moved' : ''"
/>
<Tag
:value="formatTime(end)"
class="absolute time-tag text-xl lg:text-base"
:style="{ left: timeToPercentString(timeRange[1]) }"
:class="endMoved.value ? 'moved' : ''"
/>
</div>
<div class="break" />
<Slider
v-if="!mobilePage"
v-model="timeRange"
:class="flexSpecs"
range
:min="0"
:max="24 * 60"
:step="5"
@change="updateTimeRange"
/>
</template>
<template #content>
<DataTable
v-model:filters="filters"
:value="availableRooms"
data-key="id"
filter-display="row"
paginator
:rows="10"
:global-filter-fields="['room']"
>
<Column field="room" sortable :header="$t('freeRooms.room')">
<template #filter="{ filterModel, filterCallback }">
<InputText
v-model="filterModel.value"
type="text"
class="p-column-filter"
:placeholder="$t('freeRooms.searchByRoom')"
@input="filterCallback()"
/>
</template>
<template #body="slotProps">
<div class="flex flex-column sm:flex-row justify-content-between flex-1 column-gap-4 mx-2 md:mx-4">
<p class="flex-1 align-self-stretch sm:align-self-center my-2">{{ slotProps.data.room }}</p>
<Button
:label="$t('freeRooms.viewOccupancy')"
icon="pi pi-hourglass"
class="p-button-rounded p-button-outlined sm:align-self-center align-self-end"
@click="occupationRoute(slotProps.data.room)"
/>
</div>
</template>
</Column>
</DataTable>
</template>
</DynamicPage>
</template>
<script setup lang="ts">
import { computed, inject, Ref, ref } from "vue";
import DynamicPage from "@/view/DynamicPage.vue";
import { requestFreeRooms } from "@/api/requestFreeRooms.ts";
import { FilterMatchMode } from "primevue/api";
import { padStart } from "@fullcalendar/core/internal";
import router from "@/router";
import { formatYearMonthDay } from "@/helpers/dates";
const mobilePage = inject("mobilePage") as Ref<boolean>;
const filters = ref({
room: { value: null, matchMode: FilterMatchMode.CONTAINS, label: "Room" },
});
function occupationRoute(room: string): void {
// date is in format like YYYYMMDD
router.push({
name: "room-schedule",
query: {
room: room,
date: formatYearMonthDay(date.value),
},
});
}
const date: Ref<Date> = ref(new Date(Date.now()));
const start: Ref<Date> = ref(new Date(0));
const end: Ref<Date> = ref(new Date(0));
class Moved {
value: boolean = false;
timeout: NodeJS.Timeout | null = null;
}
const startMoved: Ref<Moved> = ref(new Moved());
const endMoved: Ref<Moved> = ref(new Moved());
start.value.setHours(new Date().getHours());
end.value.setHours(Math.min(new Date().getHours() + 3, 23));
if (end.value.getHours() === 23) {
start.value.setHours(22);
end.value.setMinutes(55);
}
const timeRange: Ref<number[]> = ref([
start.value.getHours() * 60 + start.value.getMinutes(),
end.value.getHours() * 60 + end.value.getMinutes(),
]);
function buttonMovedTimeout(
time: number,
timeDate: Date,
moved: Ref<Moved>,
): void {
if (time !== timeDate.getHours() * 60 + timeDate.getMinutes()) {
if (moved.value.timeout !== null) {
clearTimeout(moved.value.timeout);
}
moved.value.value = true;
moved.value.timeout = setTimeout(() => {
moved.value.value = false;
}, 1000);
}
}
function updateTimeRange(): void {
buttonMovedTimeout(timeRange.value[0], start.value, startMoved);
buttonMovedTimeout(timeRange.value[1], end.value, endMoved);
if (timeRange.value[0] > timeRange.value[1]) {
timeRange.value[1] = timeRange.value[0];
}
start.value.setHours(Math.floor(timeRange.value[0] / 60));
start.value.setMinutes(timeRange.value[0] % 60);
end.value.setHours(Math.floor(timeRange.value[1] / 60));
end.value.setMinutes(timeRange.value[1] % 60);
}
function formatTime(time: Date): string {
return (
padStart(time.getHours().toString(), 2) +
":" +
padStart(time.getMinutes().toString(), 2)
);
}
function timeToPercentString(time: number): string {
return `${(Number(time) * 100) / (24 * 60)}%`;
}
async function loadFreeRooms(): Promise<void> {
availableRooms.value = [];
const startDate = new Date(date.value.getTime());
startDate.setHours(start.value.getHours());
startDate.setMinutes(start.value.getMinutes());
const endDate = new Date(date.value.getTime());
endDate.setHours(end.value.getHours());
endDate.setMinutes(end.value.getMinutes());
await requestFreeRooms(startDate.toISOString(), endDate.toISOString()).then(
(data) => {
availableRooms.value = data.map((room, index) => {
return { id: index, room: room };
});
},
);
}
const isLater = computed(() => {
return start.value > end.value;
});
const availableRooms: Ref<{ id: number; room: string }[]> = ref([]);
</script>
<style scoped>
.break {
flex-basis: 100%;
height: 0;
}
.time-tag {
transform: translateX(-50%);
border: 2px solid var(--primary-color);
background-color: var(--primary-color);
opacity: 0.6;
transition: opacity 0.2s ease-in-out;
}
.time-tag.moved {
opacity: 1;
z-index: 2;
}
.time-tag::before {
content: "";
position: absolute;
bottom: -0.8rem;
left: 50%;
width: 0;
height: 0;
border-style: solid;
border-width: 0.8rem 0.5rem 0 0.5rem;
border-color: var(--primary-color) transparent transparent transparent;
transform: translateX(-50%);
}
</style>