mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender.git
synced 2026-01-16 19:42:26 +01:00
294 lines
8.3 KiB
Vue
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>
|