mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-16 09:38:51 +02:00
feat:#3 frontend room occupancy decoder
This commit is contained in:
19
frontend/package-lock.json
generated
19
frontend/package-lock.json
generated
@ -16,7 +16,9 @@
|
||||
"@tanstack/vue-query": "^5.28.9",
|
||||
"@tanstack/vue-query-devtools": "^5.28.10",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"bson": "^5.5.1",
|
||||
"country-flag-emoji-polyfill": "^0.1.8",
|
||||
"date-fns": "^3.6.0",
|
||||
"pinia": "^2.1.7",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^6.0.1",
|
||||
@ -4220,6 +4222,14 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
|
||||
"integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==",
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@ -4562,6 +4572,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
|
@ -21,7 +21,9 @@
|
||||
"@tanstack/vue-query": "^5.28.9",
|
||||
"@tanstack/vue-query-devtools": "^5.28.10",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"bson": "^5.5.1",
|
||||
"country-flag-emoji-polyfill": "^0.1.8",
|
||||
"date-fns": "^3.6.0",
|
||||
"pinia": "^2.1.7",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^6.0.1",
|
||||
|
44
frontend/src/api/fetchRoomOccupancy.ts
Normal file
44
frontend/src/api/fetchRoomOccupancy.ts
Normal file
@ -0,0 +1,44 @@
|
||||
//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/>.
|
||||
|
||||
import { BSON } from "bson";
|
||||
import { RoomOccupancyList } from "@/model/roomOccupancyList.ts";
|
||||
|
||||
export async function fetchRoomOccupancy(
|
||||
from_date: string,
|
||||
to_date: string,
|
||||
): Promise<RoomOccupancyList> {
|
||||
var roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
|
||||
new Date(), 0, 0, []
|
||||
);
|
||||
|
||||
await fetch(
|
||||
"/api/schedule/rooms?from=" + from_date + "&to=" + to_date,
|
||||
)
|
||||
.then((response) => {
|
||||
return response.arrayBuffer();
|
||||
})
|
||||
.then((roomsResponse: ArrayBuffer | null) => {
|
||||
if (roomsResponse == null) {
|
||||
return null;
|
||||
}
|
||||
const data = new Uint8Array(roomsResponse);
|
||||
roomOccupancyList = BSON.deserialize(data) as RoomOccupancyList;
|
||||
return roomOccupancyList;
|
||||
});
|
||||
|
||||
return roomOccupancyList;
|
||||
}
|
@ -51,6 +51,11 @@ const items = computed(() => [
|
||||
icon: "pi pi-fw pi-calendar",
|
||||
route: "/rooms/free",
|
||||
},
|
||||
{
|
||||
label: t("roomFinderPage.roomSchedule") + " (offline)",
|
||||
icon: "pi pi-fw pi-ban",
|
||||
route: "/rooms/occupancy/offline",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -30,6 +30,7 @@ import router from "@/router";
|
||||
import { formatYearMonthDay } from "@/helpers/dates";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { watch } from "vue";
|
||||
import { isValid } from "date-fns";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
@ -50,11 +51,13 @@ function setDateFromQuery() {
|
||||
return;
|
||||
}
|
||||
// date is in format like YYYYMMDD
|
||||
// TODO check if date is valid
|
||||
const year = queryDate.substring(0, 4);
|
||||
const month = queryDate.substring(4, 6);
|
||||
const day = queryDate.substring(6, 8);
|
||||
date.value = new Date(`${year}-${month}-${day}`);
|
||||
if (!isValid(date.value)) {
|
||||
date.value = new Date();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
203
frontend/src/components/RoomOccupationOffline.vue
Normal file
203
frontend/src/components/RoomOccupationOffline.vue
Normal file
@ -0,0 +1,203 @@
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import { computed, ComputedRef, inject, ref, Ref } from "vue";
|
||||
import { CalendarOptions, DatesSetArg, EventInput } from "@fullcalendar/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import allLocales from "@fullcalendar/core/locales-all";
|
||||
import router from "@/router";
|
||||
import { formatYearMonthDay } from "@/helpers/dates";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { watch } from "vue";
|
||||
import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy";
|
||||
import { isValid } from "date-fns";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const props = defineProps({
|
||||
room: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const date: Ref<Date> = ref(new Date());
|
||||
|
||||
// Set the selected date from the URL
|
||||
function setDateFromQuery() {
|
||||
const queryDate = router.currentRoute.value.query.date;
|
||||
if (typeof queryDate === "string") {
|
||||
if (queryDate === formatYearMonthDay(date.value)) {
|
||||
return;
|
||||
}
|
||||
// date is in format like YYYYMMDD
|
||||
const year = queryDate.substring(0, 4);
|
||||
const month = queryDate.substring(4, 6);
|
||||
const day = queryDate.substring(6, 8);
|
||||
date.value = new Date(`${year}-${month}-${day}`);
|
||||
if (!isValid(date.value)) {
|
||||
date.value = new Date();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDateFromQuery();
|
||||
|
||||
const currentDateFrom: Ref<string> = ref("");
|
||||
const currentDateTo: Ref<string> = ref("");
|
||||
|
||||
const mobilePage = inject("mobilePage") as Ref<boolean>;
|
||||
|
||||
const selectedRoom = computed(() => props.room);
|
||||
|
||||
const { data: occupations } = useQuery({
|
||||
queryKey: ["roomOccupation", selectedRoom, currentDateFrom, currentDateTo],
|
||||
queryFn: () =>
|
||||
fetchRoomOccupancy(
|
||||
new Date(currentDateFrom.value).toISOString(),
|
||||
new Date(currentDateTo.value).toISOString()
|
||||
),
|
||||
select: (data) => data
|
||||
.decodeOccupancy(selectedRoom.value, new Date(currentDateFrom.value), new Date(currentDateTo.value))
|
||||
.map((event, index) => ({
|
||||
id: index,
|
||||
event: event,
|
||||
})),
|
||||
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
|
||||
staleTime: 5000000, // 500 seconds
|
||||
});
|
||||
|
||||
watch(occupations, () => fullCalendar.value?.getApi().refetchEvents());
|
||||
|
||||
const fullCalendar = ref<InstanceType<typeof FullCalendar>>();
|
||||
|
||||
const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
|
||||
locales: allLocales,
|
||||
locale: t("languageCode"),
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
|
||||
// local debugging of mobilePage variable in object creation on ternary expression
|
||||
initialView: mobilePage.value ? "Day" : "week",
|
||||
initialDate: date.value,
|
||||
dayHeaderFormat: { weekday: "short", omitCommas: true },
|
||||
slotDuration: "00:15:00",
|
||||
eventTimeFormat: {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
},
|
||||
height: "auto",
|
||||
views: {
|
||||
week: {
|
||||
type: "timeGrid",
|
||||
slotLabelFormat: {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
omitZeroMinute: false,
|
||||
meridiem: false,
|
||||
hour12: false,
|
||||
},
|
||||
dateAlignment: "week",
|
||||
titleFormat: { month: "short", day: "numeric" },
|
||||
slotMinTime: "06:00:00",
|
||||
slotMaxTime: "22:00:00",
|
||||
duration: { days: 7 },
|
||||
firstDay: 1,
|
||||
allDaySlot: false,
|
||||
hiddenDays: [0],
|
||||
},
|
||||
Day: {
|
||||
type: "timeGrid",
|
||||
slotLabelFormat: {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
omitZeroMinute: false,
|
||||
meridiem: false,
|
||||
hour12: false,
|
||||
},
|
||||
titleFormat: { month: "short", day: "numeric" },
|
||||
slotMinTime: "06:00:00",
|
||||
slotMaxTime: "22:00:00",
|
||||
duration: { days: 1 },
|
||||
allDaySlot: false,
|
||||
hiddenDays: [0],
|
||||
},
|
||||
},
|
||||
headerToolbar: {
|
||||
end: "prev,next today",
|
||||
center: "title",
|
||||
start: "week,Day",
|
||||
},
|
||||
|
||||
datesSet: function (dateInfo: DatesSetArg) {
|
||||
const view = dateInfo.view;
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
const startDate = new Date(view.activeStart.getTime() - offset * 60 * 1000);
|
||||
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
|
||||
currentDateFrom.value = startDate.toISOString().split("T")[0];
|
||||
currentDateTo.value = endDate.toISOString().split("T")[0];
|
||||
router.replace({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
date: formatYearMonthDay(endDate),
|
||||
},
|
||||
});
|
||||
},
|
||||
events: function (
|
||||
_info: unknown,
|
||||
successCallback: (events: EventInput[]) => void,
|
||||
) {
|
||||
if (!occupations.value) return;
|
||||
successCallback(
|
||||
occupations.value.map((event) => {
|
||||
return {
|
||||
id: event.id.toString(),
|
||||
start: event.event.start,
|
||||
end: event.event.end,
|
||||
color: event.event.free
|
||||
? "var(--htwk-gruen-500)"
|
||||
: "var(--htwk-grau-60-500)",
|
||||
textColor: event.event.free
|
||||
? "var(--green-50)"
|
||||
: "white",
|
||||
title: event.event.stub
|
||||
? t("roomFinderPage.stub")
|
||||
: event.event.free
|
||||
? t("roomFinderPage.available")
|
||||
: t("roomFinderPage.occupied"),
|
||||
} as EventInput;
|
||||
}),
|
||||
);
|
||||
},
|
||||
}));
|
||||
</script>
|
||||
<template>
|
||||
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.fc-toolbar.fc-header-toolbar) {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -26,7 +26,8 @@
|
||||
"dropDownSelect": "Bitte wähle einen Raum aus",
|
||||
"noRoomsAvailable": "Keine Räume verfügbar",
|
||||
"available": "verfügbar",
|
||||
"occupied": "belegt"
|
||||
"occupied": "belegt",
|
||||
"stub": "bitte online prüfen"
|
||||
},
|
||||
"freeRooms": {
|
||||
"freeRooms": "Freie Räume",
|
||||
|
@ -26,7 +26,8 @@
|
||||
"dropDownSelect": "please select a room",
|
||||
"noRoomsAvailable": "no rooms listed",
|
||||
"available": "available",
|
||||
"occupied": "occupied"
|
||||
"occupied": "occupied",
|
||||
"stub": "please check online"
|
||||
},
|
||||
"freeRooms": {
|
||||
"freeRooms": "free rooms",
|
||||
|
@ -26,7 +26,8 @@
|
||||
"dropDownSelect": "部屋を選択してください",
|
||||
"noRoomsAvailable": "利用可能な部屋がありません",
|
||||
"available": "利用可能",
|
||||
"occupied": "占有中"
|
||||
"occupied": "占有中",
|
||||
"stub": "オンラインでご確認ください。"
|
||||
},
|
||||
"freeRooms": {
|
||||
"freeRooms": "空いている部屋",
|
||||
|
@ -31,7 +31,17 @@ export class Event {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class AnonymizedEventDTO {
|
||||
export class AnonymizedOccupancy {
|
||||
constructor(
|
||||
public start: string,
|
||||
public end: string,
|
||||
public rooms: string,
|
||||
public free: boolean,
|
||||
public stub: boolean = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class AnonymizedEventDTO extends AnonymizedOccupancy {
|
||||
constructor(
|
||||
public day: string,
|
||||
public week: string,
|
||||
@ -39,5 +49,7 @@ export class AnonymizedEventDTO {
|
||||
public end: string,
|
||||
public rooms: string,
|
||||
public free: boolean,
|
||||
) {}
|
||||
) {
|
||||
super(start, end, rooms, free);
|
||||
}
|
||||
}
|
||||
|
240
frontend/src/model/roomOccupancyList.ts
Normal file
240
frontend/src/model/roomOccupancyList.ts
Normal file
@ -0,0 +1,240 @@
|
||||
//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/>.
|
||||
|
||||
import { Binary } from "bson";
|
||||
import { AnonymizedEventDTO, AnonymizedOccupancy } from "./event";
|
||||
import { Duration, NormalizedInterval, add, addDays, addMinutes, clamp, differenceInMinutes, eachDayOfInterval, endOfDay, interval, isAfter, isBefore, isEqual, max, min, startOfDay, startOfTomorrow, subDays } from "date-fns";
|
||||
|
||||
/**
|
||||
* Represents the occupancy of a single room.
|
||||
* occupancy is a binary string, where each bit represents a block of time.
|
||||
*/
|
||||
class RoomOccupancy {
|
||||
constructor(
|
||||
public name : string,
|
||||
public occupancy : Binary,
|
||||
) {}
|
||||
}
|
||||
|
||||
/// The start time of the day. 08:00
|
||||
const START_OF_WORKDAY : Duration = {hours: 8};
|
||||
/// The end time of the day. 20:00
|
||||
const END_OF_WORKDAY : Duration = {hours: 20};
|
||||
|
||||
/**
|
||||
* Represents the occupancy of multiple rooms.
|
||||
* start is the start date of the occupancy list.
|
||||
* granularity is the duration of a single block in minutes.
|
||||
* blocks is the number of time slices called blocks.
|
||||
* rooms is a list of RoomOccupancy objects representing the occupancy of each room.
|
||||
*/
|
||||
export class RoomOccupancyList {
|
||||
constructor(
|
||||
public start : Date,
|
||||
public granularity : number,
|
||||
public blocks : number,
|
||||
public rooms : RoomOccupancy[],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a list of all rooms encoded in this occupancy list.
|
||||
* @returns a list of room names.
|
||||
*/
|
||||
public getRooms() : string[] {
|
||||
return this.rooms.map((room) => room.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the occupancy of a room for a given time range.
|
||||
* @param room the room to decode.
|
||||
* @param from the start of the time range.
|
||||
* @param to the end of the time range.
|
||||
* @returns a list of AnonymizedEventDTO objects representing the occupancy of the room.
|
||||
*/
|
||||
public decodeOccupancy(room : string, from : Date, to : Date) : AnonymizedOccupancy[] {
|
||||
console.log("Decoding occupancy for room " + room + " from " + from.toISOString() + " to " + to.toISOString());
|
||||
const roomOccupancy = this.rooms.find((r) => r.name === room);
|
||||
|
||||
if (roomOccupancy === undefined) {
|
||||
return RoomOccupancyList.generateStubEvents(room, from, to);
|
||||
}
|
||||
|
||||
const occupancyList = [];
|
||||
|
||||
// Get start and end of decoded time range (within encoded list and requested range)
|
||||
let decodeInterval = interval(clamp(from, this.getOccupancyInterval()), clamp(to, this.getOccupancyInterval()));
|
||||
|
||||
// Calculate the slice of bytes, that are needed to decode the requested time range
|
||||
let minutesFromStart = differenceInMinutes(this.start, decodeInterval.start);
|
||||
let minutesToEnd = differenceInMinutes(this.start, decodeInterval.end);
|
||||
|
||||
let firstByte = Math.floor(minutesFromStart / this.granularity / 8);
|
||||
let lastByte = Math.floor(minutesToEnd / this.granularity / 8);
|
||||
|
||||
let decodeSliceStart = addMinutes(this.start, firstByte * 8 * this.granularity);
|
||||
let decodeSlice = roomOccupancy.occupancy.buffer.slice(firstByte, lastByte + 1);
|
||||
|
||||
// Decode the occupancy data
|
||||
occupancyList.push(...RoomOccupancyList.decodeOccupancyData(new Uint8Array(decodeSlice), decodeSliceStart, this.granularity, room));
|
||||
|
||||
// add stub events for the time before and after the decoded time range
|
||||
if (!isEqual(from, decodeInterval.start)) {
|
||||
occupancyList.push(...RoomOccupancyList.generateStubEvents(room, from, decodeInterval.start));
|
||||
}
|
||||
|
||||
if (!isEqual(to, decodeInterval.end)) {
|
||||
occupancyList.push(...RoomOccupancyList.generateStubEvents(room, decodeInterval.end, to));
|
||||
}
|
||||
|
||||
return occupancyList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the decoded time interval within the current occupancy list.
|
||||
* @returns the interval of the occupancy list.
|
||||
*/
|
||||
private getOccupancyInterval() : NormalizedInterval<Date> {
|
||||
return interval(this.start, addMinutes(this.start, this.granularity * this.blocks));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the whole array of occupancy data with the first bit representing the given start time.
|
||||
* @param occupancy the occupancy data to decode.
|
||||
* @param start the start time of the occupancy data.
|
||||
* @param granularity the duration of a single block in minutes.
|
||||
* @param room the room name.
|
||||
* @returns a list of AnonymizedOccupancy objects representing the occupancy of the room.
|
||||
*/
|
||||
public static decodeOccupancyData(occupancy : Uint8Array, start : Date, granularity : number, room : string) : AnonymizedOccupancy[] {
|
||||
let occupancyList = [];
|
||||
let firstOccupancyBit : number | null = null;
|
||||
|
||||
// Iterate over all bytes that are in the array
|
||||
for (let byte_i = 0; byte_i < occupancy.length; byte_i++) {
|
||||
let byte = occupancy[byte_i];
|
||||
|
||||
// Iterate over all bits in the current byte
|
||||
for (let bit_i = 0; bit_i < 8; bit_i++) {
|
||||
let isOccupied = (byte & (1 << bit_i)) !== 0;
|
||||
|
||||
if (firstOccupancyBit === null && isOccupied) {
|
||||
firstOccupancyBit = byte_i * 8 + bit_i;
|
||||
} else if (firstOccupancyBit !== null && !isOccupied) {
|
||||
let startTime = addMinutes(start, firstOccupancyBit * granularity);
|
||||
let endTime = addMinutes(start, (byte_i * 8 + bit_i) * granularity);
|
||||
|
||||
// add event between start and end of a block of boolean true values
|
||||
occupancyList.push(new AnonymizedOccupancy(
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
room,
|
||||
true
|
||||
));
|
||||
|
||||
firstOccupancyBit = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add last event if it is still ongoing
|
||||
if (firstOccupancyBit !== null) {
|
||||
let startTime = addMinutes(start, firstOccupancyBit * granularity);
|
||||
let endTime = addMinutes(start, occupancy.length * 8 * granularity);
|
||||
|
||||
occupancyList.push(new AnonymizedOccupancy(
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
room,
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
return occupancyList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a list of AnonymizedOccupancy objects for a given time range.
|
||||
* The generated events are always lying within the time range [START_OF_DAY, END_OF_DAY].
|
||||
*
|
||||
* @param from The start time within the specified start day.
|
||||
* @param to The end time within the specified end day.
|
||||
* @returns a list of AnonymizedEventDTO objects, from start to end.
|
||||
*/
|
||||
public static generateStubEvents(rooms : string, from : Date, to : Date) : AnonymizedOccupancy[] {
|
||||
from = RoomOccupancyList.shiftTimeForwardInsideWorkday(from);
|
||||
to = RoomOccupancyList.shiftTimeBackwardInsideWorkday(to);
|
||||
|
||||
if (isAfter(from, to)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return eachDayOfInterval({start: from, end: to}).map((day) => {
|
||||
let startTime = max([from, RoomOccupancyList.setTimeOfDay(day, START_OF_WORKDAY)]);
|
||||
let endTime = min([to, RoomOccupancyList.setTimeOfDay(day, END_OF_WORKDAY)]);
|
||||
|
||||
return new AnonymizedOccupancy(
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
rooms,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift the time forward to the start of the next day if it is after the end of the current day.
|
||||
*
|
||||
* @param date the date time to check if in bounds.
|
||||
* @returns the shifted time.
|
||||
*/
|
||||
private static shiftTimeForwardInsideWorkday(date : Date) : Date {
|
||||
// if the time of date is after the end of the workday
|
||||
if (isAfter(date, RoomOccupancyList.setTimeOfDay(date, END_OF_WORKDAY))) {
|
||||
// shift the time to the start of the next day
|
||||
return startOfDay(addDays(date,1));
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift the time backward to the end of the previous day if it is before the start of the current day.
|
||||
*
|
||||
* @param date the date time to check if in bounds.
|
||||
* @returns the shifted time.
|
||||
*/
|
||||
private static shiftTimeBackwardInsideWorkday(date : Date) : Date {
|
||||
// if the time of date is before the start of the workday
|
||||
if (isBefore(date, RoomOccupancyList.setTimeOfDay(date, START_OF_WORKDAY))) {
|
||||
// shift the time to the end of the previous day
|
||||
return endOfDay(subDays(date,1));
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the date to the specified time after 00:00.
|
||||
* @param date the date object to extract the day from.
|
||||
* @param time the time as Duration after 00:00.
|
||||
* @returns new date with changed time values.
|
||||
*/
|
||||
private static setTimeOfDay(date : Date, time : Duration) : Date {
|
||||
return add(startOfDay(date), time);
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ const AdditionalModules = () => import("../view/AdditionalModules.vue");
|
||||
const CalendarLink = () => import("../components/CalendarLink.vue");
|
||||
const RenameModules = () => import("../components/RenameModules.vue");
|
||||
const RoomFinder = () => import("../view/RoomFinder.vue");
|
||||
const RoomFinderOffline = () => import("../view/RoomFinderOffline.vue");
|
||||
const EditCalendarView = () => import("../view/EditCalendarView.vue");
|
||||
const EditAdditionalModules = () =>
|
||||
import("../view/editCalendar/EditAdditionalModules.vue");
|
||||
@ -43,6 +44,11 @@ const router = createRouter({
|
||||
name: "room-schedule",
|
||||
component: RoomFinder,
|
||||
},
|
||||
{
|
||||
path: "/rooms/occupancy/offline",
|
||||
name: "room-schedule-offline",
|
||||
component: RoomFinderOffline,
|
||||
},
|
||||
{
|
||||
path: "/rooms/free",
|
||||
name: "free-rooms",
|
||||
|
108
frontend/src/view/RoomFinderOffline.vue
Normal file
108
frontend/src/view/RoomFinderOffline.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Ref, computed, ref, watch } from "vue";
|
||||
import { fetchRoom } from "../api/fetchRoom.ts";
|
||||
import DynamicPage from "./DynamicPage.vue";
|
||||
import RoomOccupationOffline from "../components/RoomOccupationOffline.vue";
|
||||
import { computedAsync } from "@vueuse/core";
|
||||
import router from "@/router";
|
||||
|
||||
type Room = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const selectedRoom: Ref<Room> = ref({ name: "" });
|
||||
|
||||
// Watch for changes in URL parameter
|
||||
router.afterEach(async (to) => {
|
||||
const room = to.query.room;
|
||||
if (room && typeof room === "string") {
|
||||
setRoomFromList(room, rooms.value);
|
||||
}
|
||||
});
|
||||
|
||||
const rooms = computedAsync<Set<string>>(async () => {
|
||||
let rooms: Set<string> = new Set();
|
||||
return await fetchRoom()
|
||||
.then((data) => {
|
||||
rooms = new Set(data);
|
||||
return rooms;
|
||||
})
|
||||
.finally(() => {
|
||||
const room = router.currentRoute.value.query.room;
|
||||
if (room && typeof room === "string") {
|
||||
// check if room is available in roomsList
|
||||
setRoomFromList(room, rooms);
|
||||
}
|
||||
});
|
||||
}, new Set());
|
||||
|
||||
const roomsList = computed(() => {
|
||||
return Array.from(rooms.value).map((room) => {
|
||||
return { name: room } as Room;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Set the room from the list of rooms
|
||||
* @param room Name of the room
|
||||
* @param rooms List of available rooms
|
||||
*/
|
||||
function setRoomFromList(room: string, rooms: Set<string>) {
|
||||
// wait for the roomsList to be available
|
||||
const roomInList: boolean = rooms.has(room);
|
||||
if (roomInList) {
|
||||
selectedRoom.value.name = room;
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedRoom, (newRoom: Room) => {
|
||||
if (newRoom.name !== "") {
|
||||
router.push({
|
||||
query: { ...router.currentRoute.value.query, room: newRoom.name },
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DynamicPage
|
||||
:hide-content="selectedRoom.name === ''"
|
||||
:headline="$t('roomFinderPage.headline') + ' – Offline'"
|
||||
:sub-title="$t('roomFinderPage.detail')"
|
||||
icon="pi pi-search"
|
||||
>
|
||||
<template #selection>
|
||||
<Dropdown
|
||||
v-model="selectedRoom"
|
||||
:options="roomsList"
|
||||
class="flex-1 m-0"
|
||||
filter
|
||||
option-label="name"
|
||||
:placeholder="$t('roomFinderPage.dropDownSelect')"
|
||||
:empty-message="$t('roomFinderPage.noRoomsAvailable')"
|
||||
:auto-filter-focus="true"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<RoomOccupationOffline :room="selectedRoom.name" />
|
||||
</template>
|
||||
</DynamicPage>
|
||||
</template>
|
@ -21,7 +21,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [
|
||||
|
@ -96,4 +96,16 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
esbuild: {
|
||||
supported: {
|
||||
'top-level-await': true
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
supported: {
|
||||
'top-level-await': true
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user