lint:#13 formatted

This commit is contained in:
Elmar Kresse
2024-06-10 10:50:27 +02:00
parent ee0894d048
commit 439850f69b
127 changed files with 15740 additions and 11990 deletions

View File

@@ -22,12 +22,13 @@ export async function fetchRoomOccupancy(
to_date: string,
): Promise<RoomOccupancyList> {
var roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
new Date(), 0, 0, []
new Date(),
0,
0,
[],
);
await fetch(
"/api/schedule/rooms?from=" + from_date + "&to=" + to_date,
)
await fetch("/api/schedule/rooms?from=" + from_date + "&to=" + to_date)
.then((response) => {
return response.arrayBuffer();
})

View File

@@ -20,4 +20,4 @@ export async function fetchICalendarEvents(token: string): Promise<string> {
throw new Error("Network response was not ok");
}
return response.text();
}
}

View File

@@ -104,7 +104,7 @@ const actions = computed(() => [
label: t("calendarLink.toHTWKalendar"),
icon: "pi pi-home",
command: forwardToHTWKalendar,
}
},
]);
</script>

View File

@@ -17,10 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script setup lang="ts">
import FullCalendar from "@fullcalendar/vue3";
import { computed, ComputedRef, inject, Ref, ref, watch } from "vue";
import { CalendarOptions, DatesSetArg, EventClickArg } from "@fullcalendar/core";
import {
CalendarOptions,
DatesSetArg,
EventClickArg,
} from "@fullcalendar/core";
import allLocales from "@fullcalendar/core/locales-all";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
@@ -55,7 +58,6 @@ const toggle = (info: EventClickArg) => {
op.value.hide();
return;
} else {
clickedEvent.value = {
title: info.event._def.title,
start: start,
@@ -66,11 +68,8 @@ const toggle = (info: EventClickArg) => {
};
op.value.show(info.jsEvent);
op.value.target = info.el;
}
}
};
const selectedToken = computed(() => props.token);
@@ -79,8 +78,7 @@ const date: Ref<Date> = ref(new Date());
const { data: calendar } = useQuery({
queryKey: ["userCalendar", selectedToken],
queryFn: () =>
fetchICalendarEvents(selectedToken.value),
queryFn: () => fetchICalendarEvents(selectedToken.value),
select: (data) => {
return data;
},
@@ -175,20 +173,22 @@ watch(mobilePage, () => {
</script>
<template>
<FullCalendar id="overlay-mount-point" ref="fullCalendar" :options="calendarOptions" >
<FullCalendar
id="overlay-mount-point"
ref="fullCalendar"
:options="calendarOptions"
>
</FullCalendar>
<OverlayPanel ref="op" >
<div>
<h3>{{ clickedEvent.title }}</h3>
<p>Location: {{ clickedEvent.location }}</p>
<p>Start: {{ clickedEvent.start?.toLocaleString()}}</p>
<p>End: {{ clickedEvent.end?.toLocaleString() }}</p>
<p>Notes: {{ clickedEvent.notes }}</p>
</div>
<OverlayPanel ref="op">
<div>
<h3>{{ clickedEvent.title }}</h3>
<p>Location: {{ clickedEvent.location }}</p>
<p>Start: {{ clickedEvent.start?.toLocaleString() }}</p>
<p>End: {{ clickedEvent.end?.toLocaleString() }}</p>
<p>Notes: {{ clickedEvent.notes }}</p>
</div>
</OverlayPanel>
</template>
<style scoped>
@@ -197,4 +197,4 @@ watch(mobilePage, () => {
justify-content: space-between;
gap: 0.5rem;
}
</style>
</style>

View File

@@ -45,7 +45,7 @@ onMounted(() => {
// set theme matching browser preference
setTheme(
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches,
window.matchMedia("(prefers-color-scheme: dark)").matches,
);
window
@@ -63,7 +63,7 @@ onMounted(() => {
class="p-button-rounded w-full md:w-auto"
style="margin-right: 1rem"
:severity="isDark ? 'warning' : 'success'"
@click="toggleTheme();"
@click="toggleTheme()"
>
<i v-if="isDark" class="pi pi-sun"></i>
<i v-else class="pi pi-moon"></i>

View File

@@ -68,7 +68,14 @@ updateLocale(localeStore().locale);
<template #value="slotProps">
<div v-if="slotProps.value" class="flex align-items-center">
<div class="mr-2 flag">{{ displayIcon(slotProps.value) }}</div>
<div style="font-family: 'Twemoji Country Flags', 'Helvetica', 'Comic Sans', serif;">{{ displayCountry(slotProps.value) }}</div>
<div
style="
font-family: &quot;Twemoji Country Flags&quot;,
&quot;Helvetica&quot;, &quot;Comic Sans&quot;, serif;
"
>
{{ displayCountry(slotProps.value) }}
</div>
</div>
<span v-else>
{{ slotProps.placeholder }}

View File

@@ -67,7 +67,7 @@ const items = computed(() => [
label: t("roomFinderPage.roomSchedule") + " (offline)",
icon: "pi pi-fw pi-ban",
route: "/rooms/occupancy/offline",
}
},
],
},
{
@@ -140,7 +140,9 @@ function handleDarkModeToggled(isDarkVar: boolean) {
</template>
<template #end>
<div class="flex align-items-stretch justify-content-center">
<DarkModeSwitcher @dark-mode-toggled="handleDarkModeToggled"></DarkModeSwitcher>
<DarkModeSwitcher
@dark-mode-toggled="handleDarkModeToggled"
></DarkModeSwitcher>
<LocaleSwitcher></LocaleSwitcher>
</div>
</template>

View File

@@ -181,9 +181,7 @@ const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
borderColor: event.showFree
? "var(--htwk-gruen-600)"
: "var(--htwk-grau-60-600)",
textColor: event.showFree
? "var(--green-50)"
: "white",
textColor: event.showFree ? "var(--green-50)" : "white",
title: event.showFree
? t("roomFinderPage.available")
: t("roomFinderPage.occupied"),

View File

@@ -73,18 +73,22 @@ const selectedRoom = computed(() => props.room);
/**
* Transform decoded JSON object with binary data
* to anonymized occupancy events
* to anonymized occupancy events
* @param data RoomOccupancyList with binary data
* @returns Anonymized occupancy events
*/
function transformData(data: RoomOccupancyList) {
const events = data
.decodeOccupancy(selectedRoom.value, new Date(currentDateFrom.value), new Date(currentDateTo.value))
.decodeOccupancy(
selectedRoom.value,
new Date(currentDateFrom.value),
new Date(currentDateTo.value),
)
.map((event, index) => ({
id: index,
event: event,
}));
return events;
return events;
}
const { data: occupations } = useQuery({
@@ -92,7 +96,7 @@ const { data: occupations } = useQuery({
queryFn: () =>
fetchRoomOccupancy(
new Date(currentDateFrom.value).toISOString(),
new Date(currentDateTo.value).toISOString()
new Date(currentDateTo.value).toISOString(),
),
select: (data) => transformData(data),
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
@@ -185,17 +189,15 @@ const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
id: event.id.toString(),
start: event.event.start,
end: event.event.end,
color: event.event.free
? "var(--htwk-gruen-500)"
color: event.event.free
? "var(--htwk-gruen-500)"
: "var(--htwk-grau-60-500)",
textColor: event.event.free
? "var(--green-50)"
: "white",
textColor: event.event.free ? "var(--green-50)" : "white",
title: event.event.stub
? t("roomFinderPage.stub")
: event.event.free
? t("roomFinderPage.available")
: t("roomFinderPage.occupied"),
? t("roomFinderPage.available")
: t("roomFinderPage.occupied"),
} as EventInput;
}),
);

View File

@@ -23,7 +23,6 @@ export function formatYearMonthDay(date: Date): string {
return date.toISOString().split("T")[0].replace(/-/g, "");
}
export function removeTZ(date: Date): Date {
return new Date(date.getTime() + date.getTimezoneOffset() * 60000)
}
return new Date(date.getTime() + date.getTimezoneOffset() * 60000);
}

View File

@@ -18,124 +18,106 @@ import { expect, test } from "vitest";
import { exportedForTesting } from "@/helpers/ical.ts";
import { CalendarComponent } from "ical";
// colorizeEvents has only the function to colorize the events that are passed to it
test("colorizeEventsSameSummary", () => {
const events: CalendarComponent[] = [
{
type: "VEVENT",
summary: "Operations Research",
},
{
type: "VEVENT",
summary: "Operations Research",
},
];
const events: CalendarComponent[] =
[
{
type: "VEVENT",
summary: "Operations Research",
},
{
type: "VEVENT",
summary: "Operations Research",
},
];
expect(exportedForTesting.colorizeEvents(
events
)
).toEqual([{ summary: "Operations Research", color: "var(--htwk-rot-200)" },{ summary: "Operations Research", color: "var(--htwk-rot-200)" }]);
}
);
expect(exportedForTesting.colorizeEvents(events)).toEqual([
{ summary: "Operations Research", color: "var(--htwk-rot-200)" },
{ summary: "Operations Research", color: "var(--htwk-rot-200)" },
]);
});
test("colorizeEventsDifferentSummary", () => {
const events: CalendarComponent[] = [
{
type: "VEVENT",
summary: "Algorithmische Mathematik",
},
{
type: "VEVENT",
summary: "Funktionale Programmierung",
},
];
const events: CalendarComponent[] =
[
{
type: "VEVENT",
summary: "Algorithmische Mathematik",
},
{
type: "VEVENT",
summary: "Funktionale Programmierung",
},
];
expect(exportedForTesting.colorizeEvents(
events
)
).toEqual([{ summary: "Algorithmische Mathematik", color: "var(--htwk-rot-200)" },{ summary: "Funktionale Programmierung", color: "var(--htwk-gruen-300)" }]);
}
);
expect(exportedForTesting.colorizeEvents(events)).toEqual([
{ summary: "Algorithmische Mathematik", color: "var(--htwk-rot-200)" },
{ summary: "Funktionale Programmierung", color: "var(--htwk-gruen-300)" },
]);
});
test("filterEventsDistinct", () => {
const events: CalendarComponent[] =
[
{
type: "VEVENT",
summary: "Operations Research",
},
{
type: "VEVENT",
summary: "Operations Research",
},
];
const events: CalendarComponent[] = [
{
type: "VEVENT",
summary: "Operations Research",
},
{
type: "VEVENT",
summary: "Operations Research",
},
];
expect(exportedForTesting.filterEventsDistinct(
events
)
).toEqual([{ type: "VEVENT", summary: "Operations Research" }]);
expect(exportedForTesting.filterEventsDistinct(events)).toEqual([
{ type: "VEVENT", summary: "Operations Research" },
]);
});
test("filterEventsDistinctDifferentSummary", () => {
const events: CalendarComponent[] =
[
{
type: "VEVENT",
summary: "Algorithmische Mathematik",
},
{
type: "VEVENT",
summary: "Funktionale Programmierung",
},
];
const events: CalendarComponent[] = [
{
type: "VEVENT",
summary: "Algorithmische Mathematik",
},
{
type: "VEVENT",
summary: "Funktionale Programmierung",
},
];
expect(exportedForTesting.filterEventsDistinct(
events
)
).toEqual(events);
expect(exportedForTesting.filterEventsDistinct(events)).toEqual(events);
});
test("extractedColorizedEvents", () => {
const events: CalendarComponent[] =
[
{
type: "VEVENT",
summary: "Operations Research",
},
{
type: "VEVENT",
summary: "Operations Research",
},
];
const events: CalendarComponent[] = [
{
type: "VEVENT",
summary: "Operations Research",
},
{
type: "VEVENT",
summary: "Operations Research",
},
];
expect(exportedForTesting.extractedColorizedEvents(
events
)
).toEqual([{ summary: "Operations Research", color: "var(--htwk-rot-200)" }]);
expect(exportedForTesting.extractedColorizedEvents(events)).toEqual([
{ summary: "Operations Research", color: "var(--htwk-rot-200)" },
]);
});
test("extractedColorizedEventsDifferentSummary", () => {
const events: CalendarComponent[] =
[
{
type: "VEVENT",
summary: "Algorithmische Mathematik",
},
{
type: "VEVENT",
summary: "Funktionale Programmierung",
},
];
const events: CalendarComponent[] = [
{
type: "VEVENT",
summary: "Algorithmische Mathematik",
},
{
type: "VEVENT",
summary: "Funktionale Programmierung",
},
];
expect(exportedForTesting.extractedColorizedEvents(
events
)
).toEqual([{ summary: "Algorithmische Mathematik", color: "var(--htwk-rot-200)" },{ summary: "Funktionale Programmierung", color: "var(--htwk-gruen-300)" }]);
});
expect(exportedForTesting.extractedColorizedEvents(events)).toEqual([
{ summary: "Algorithmische Mathematik", color: "var(--htwk-rot-200)" },
{ summary: "Funktionale Programmierung", color: "var(--htwk-gruen-300)" },
]);
});

View File

@@ -23,7 +23,7 @@ import { CalendarComponent } from "ical";
* @param color Color code for the event
*/
export interface ColorDistinctionEvent {
summary: string;
summary: string | undefined;
color: string;
}
@@ -32,13 +32,15 @@ export interface ColorDistinctionEvent {
* @param icalData iCal data to parse
* @returns Array of calendar components
*/
export function parseICalData(icalData: string | undefined): CalendarComponent[] {
export function parseICalData(
icalData: string | undefined,
): CalendarComponent[] {
if (icalData === undefined || !icalData) {
return [];
} else {
const jCalData = ICAL.parse(icalData);
const comp = new ICAL.Component(jCalData);
const vEvents = comp.getAllSubcomponents('vevent');
const vEvents = comp.getAllSubcomponents("vevent");
const colorDistinctionEvents = extractedColorizedEvents(vEvents);
return vEvents.map((vevent: CalendarComponent) => {
@@ -50,7 +52,9 @@ export function parseICalData(icalData: string | undefined): CalendarComponent[]
end: event.endDate.toJSDate(),
notes: event.description,
allDay: event.startDate.isDate,
color: colorDistinctionEvents.find((e: ColorDistinctionEvent) => e.summary === event.summary)?.color,
color: colorDistinctionEvents.find(
(e: ColorDistinctionEvent) => e.summary === event.summary,
)?.color,
id: event.uid,
location: event.location,
};
@@ -63,32 +67,37 @@ export function parseICalData(icalData: string | undefined): CalendarComponent[]
* @param vEvents Array of calendar components
* @returns Array of objects with event name and color
*/
function extractedColorizedEvents(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
function extractedColorizedEvents(
vEvents: CalendarComponent[],
): ColorDistinctionEvent[] {
return colorizeEvents(filterEventsDistinct(vEvents));
}
/**
* Filters out duplicate events
* @param vEvents Array of calendar components
* @returns Array of calendar components without duplicates
*/
function filterEventsDistinct(vEvents: CalendarComponent[]): CalendarComponent[] {
return vEvents.filter((vevent: CalendarComponent, index: number, self: CalendarComponent[]) => {
return self.findIndex((v) => {
return v.summary === vevent.summary;
}) === index;
});
function filterEventsDistinct(
vEvents: CalendarComponent[],
): CalendarComponent[] {
return vEvents.filter(
(vevent: CalendarComponent, index: number, self: CalendarComponent[]) => {
return (
self.findIndex((v) => {
return v.summary === vevent.summary;
}) === index
);
},
);
}
/**
* Assigns a color to each event
* @param vEvents Array of calendar components
* @returns Array of objects with event name and color
*/
function colorizeEvents(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
return vEvents.map((vevent: CalendarComponent) => {
const colors: string[] = [
"var(--htwk-rot-200)",
@@ -101,16 +110,19 @@ function colorizeEvents(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
"var(--htwk-dunkelblau-200)",
"var(--htwk-rot-400)",
"var(--htwk-gruen-400)",
"var(--htwk-blau-200)"
"var(--htwk-blau-200)",
];
const randomColor = colors[vEvents.findIndex((e: CalendarComponent) => {
return e.summary === vevent.summary?? "";
}) % colors.length];
const randomColor =
colors[
vEvents.findIndex((e: CalendarComponent) => {
return e.summary === vevent.summary;
}) % colors.length
];
return {
summary: vevent.summary?? "",
color: randomColor
summary: vevent.summary,
color: randomColor,
};
});
}
@@ -118,5 +130,5 @@ function colorizeEvents(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
export const exportedForTesting = {
extractedColorizedEvents,
filterEventsDistinct,
colorizeEvents
};
colorizeEvents,
};

View File

@@ -32,4 +32,4 @@ export function extractToken(token: string): string {
}
throw new Error("Invalid token");
}
}

View File

@@ -10,7 +10,7 @@
"privacy": "privacy",
"english": "English",
"german": "German",
"japanese" : "Japanese",
"japanese": "Japanese",
"courseSelection": {
"headline": "welcome to HTWKalender",
"winterSemester": "winter semester",

View File

@@ -26,41 +26,15 @@
"金曜日",
"土曜日"
],
"dayNamesMin": [
"日",
"月",
"火",
"水",
"木",
"金",
"土"
],
"dayNamesShort": [
"日",
"月",
"火",
"水",
"木",
"金",
"土"
],
"dayNamesMin": ["日", "月", "火", "水", "木", "金", "土"],
"dayNamesShort": ["日", "月", "火", "水", "木", "金", "土"],
"emptyFilterMessage": "オプションなし",
"emptyMessage": "結果なし",
"emptySearchMessage": "該当なし",
"emptySelectionMessage": "選択なし",
"endsWith": "終わる",
"equals": "等しい",
"fileSizeTypes": [
"B",
"KB",
"MB",
"GB",
"TB",
"PB",
"EB",
"ZB",
"YB"
],
"fileSizeTypes": ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
"filter": "フィルター",
"firstDayOfWeek": 0,
"gt": "超える",

View File

@@ -31,7 +31,7 @@ import Card from "primevue/card";
import DataView from "primevue/dataview";
import Dialog from "primevue/dialog";
import Slider from "primevue/slider";
import OverlayPanel from 'primevue/overlaypanel';
import OverlayPanel from "primevue/overlaypanel";
import ToggleButton from "primevue/togglebutton";
import "primeicons/primeicons.css";
import "primeflex/primeflex.css";
@@ -57,14 +57,14 @@ import Calendar from "primevue/calendar";
import i18n from "./i18n";
import { VueQueryPlugin } from "@tanstack/vue-query";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
polyfillCountryFlagEmojis();
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate)
pinia.use(piniaPluginPersistedstate);
app.use(VueQueryPlugin, {
queryClientConfig: {

File diff suppressed because it is too large Load Diff

View File

@@ -16,13 +16,31 @@
import { Binary } from "bson";
import { AnonymizedOccupancy } from "./event";
import { Duration, NormalizedInterval, add, addDays, addMinutes, clamp, differenceInMinutes, eachDayOfInterval, endOfDay, interval, isAfter, isBefore, isEqual, max, min, startOfDay, subDays } from "date-fns";
import {
Duration,
NormalizedInterval,
add,
addDays,
addMinutes,
clamp,
differenceInMinutes,
eachDayOfInterval,
endOfDay,
interval,
isAfter,
isBefore,
isEqual,
max,
min,
startOfDay,
subDays,
} from "date-fns";
import { fromZonedTime, toZonedTime } from "date-fns-tz";
/// The start time of the day. 07:00
const START_OF_WORKDAY : Duration = {hours: 7};
const START_OF_WORKDAY: Duration = { hours: 7 };
/// The end time of the day. 20:00
const END_OF_WORKDAY : Duration = {hours: 20};
const END_OF_WORKDAY: Duration = { hours: 20 };
/// The timezone of the data (Leipzig)
const TIMEZONE = "Europe/Berlin";
@@ -32,8 +50,8 @@ const TIMEZONE = "Europe/Berlin";
*/
class RoomOccupancy {
constructor(
public name : string,
public occupancy : Binary,
public name: string,
public occupancy: Binary,
) {}
}
@@ -41,22 +59,22 @@ class RoomOccupancy {
* 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.
* 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[],
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[] {
public getRooms(): string[] {
return this.rooms.map((room) => room.name);
}
@@ -67,9 +85,13 @@ export class RoomOccupancyList {
* @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[] {
public decodeOccupancy(
room: string,
from: Date,
to: Date,
): AnonymizedOccupancy[] {
const roomOccupancy = this.rooms.find((r) => r.name === room);
if (roomOccupancy === undefined) {
return RoomOccupancyList.generateStubEvents(room, from, to);
}
@@ -77,23 +99,41 @@ export class RoomOccupancyList {
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()));
let decodeInterval = interval(
clamp(from, this.getOccupancyInterval()),
clamp(to, this.getOccupancyInterval()),
);
let {decodeSliceStart, decodeSlice} = this.sliceOccupancy(
let { decodeSliceStart, decodeSlice } = this.sliceOccupancy(
decodeInterval,
roomOccupancy.occupancy.buffer
roomOccupancy.occupancy.buffer,
);
// Decode the occupancy data
occupancyList.push(...RoomOccupancyList.decodeOccupancyData(new Uint8Array(decodeSlice), decodeSliceStart, this.granularity, room));
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));
occupancyList.push(
...RoomOccupancyList.generateStubEvents(
room,
from,
decodeInterval.start,
),
);
}
if (!isEqual(to, decodeInterval.end)) {
occupancyList.push(...RoomOccupancyList.generateStubEvents(room, decodeInterval.end, to));
occupancyList.push(
...RoomOccupancyList.generateStubEvents(room, decodeInterval.end, to),
);
}
return occupancyList;
@@ -106,10 +146,16 @@ export class RoomOccupancyList {
* @returns a new occupancy byte array with the starting time of the first byte
* @throws an error, if the selected time range is outside of the occupancy list.
*/
private sliceOccupancy(decodeInterval : NormalizedInterval, occupancy : Uint8Array) : {decodeSliceStart: Date, decodeSlice: Uint8Array} {
private sliceOccupancy(
decodeInterval: NormalizedInterval,
occupancy: Uint8Array,
): { decodeSliceStart: Date; decodeSlice: Uint8Array } {
// Calculate the slice of bytes, that are needed to decode the requested time range
// Note: differenceInMinutes calculates (left - right)
let minutesFromStart = differenceInMinutes(decodeInterval.start, this.start);
let minutesFromStart = differenceInMinutes(
decodeInterval.start,
this.start,
);
let minutesToEnd = differenceInMinutes(decodeInterval.end, this.start);
let firstByte = Math.floor(minutesFromStart / this.granularity / 8);
@@ -117,13 +163,18 @@ export class RoomOccupancyList {
// check if firstByte and lastByte are within the bounds of the occupancy array and throw an error if not
if (
firstByte < 0 || firstByte >= occupancy.length ||
lastByte < 0 || lastByte > occupancy.length
firstByte < 0 ||
firstByte >= occupancy.length ||
lastByte < 0 ||
lastByte > occupancy.length
) {
throw new Error("Requested time range is outside of the occupancy list.");
}
let decodeSliceStart = addMinutes(this.start, firstByte * 8 * this.granularity);
let decodeSliceStart = addMinutes(
this.start,
firstByte * 8 * this.granularity,
);
let decodeSlice = occupancy.buffer.slice(firstByte, lastByte);
return { decodeSliceStart, decodeSlice: new Uint8Array(decodeSlice) };
@@ -133,8 +184,11 @@ export class RoomOccupancyList {
* 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));
private getOccupancyInterval(): NormalizedInterval<Date> {
return interval(
this.start,
addMinutes(this.start, this.granularity * this.blocks),
);
}
/**
@@ -145,9 +199,14 @@ export class RoomOccupancyList {
* @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[] {
public static decodeOccupancyData(
occupancy: Uint8Array,
start: Date,
granularity: number,
room: string,
): AnonymizedOccupancy[] {
let occupancyList = [];
let firstOccupancyBit : number | null = null;
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++) {
@@ -155,7 +214,7 @@ export class RoomOccupancyList {
// Iterate over all bits in the current byte
for (let bit_i = 0; bit_i < 8; bit_i++) {
let isOccupied = (byte & (1 << (7-bit_i))) !== 0;
let isOccupied = (byte & (1 << (7 - bit_i))) !== 0;
if (firstOccupancyBit === null && isOccupied) {
firstOccupancyBit = byte_i * 8 + bit_i;
@@ -164,31 +223,35 @@ export class RoomOccupancyList {
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,
false,
false,
));
occupancyList.push(
new AnonymizedOccupancy(
startTime.toISOString(),
endTime.toISOString(),
room,
false,
false,
),
);
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,
false,
false,
));
occupancyList.push(
new AnonymizedOccupancy(
startTime.toISOString(),
endTime.toISOString(),
room,
false,
false,
),
);
}
return occupancyList;
@@ -197,22 +260,32 @@ export class RoomOccupancyList {
/**
* 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[] {
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 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(),
@@ -230,26 +303,28 @@ export class RoomOccupancyList {
* @param json the JS object to read from.
* @returns a RoomOccupancyList object.
*/
public static fromJSON(json : any) : RoomOccupancyList {
public static fromJSON(json: any): RoomOccupancyList {
return new RoomOccupancyList(
json.start,
json.granularity,
json.blocks,
json.rooms.map((room : any) => new RoomOccupancy(room.name, room.occupancy)
));
json.rooms.map(
(room: any) => new RoomOccupancy(room.name, room.occupancy),
),
);
}
/**
* 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 {
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 RoomOccupancyList.startOfDay(addDays(date,1));
return RoomOccupancyList.startOfDay(addDays(date, 1));
} else {
return date;
}
@@ -257,15 +332,17 @@ export class RoomOccupancyList {
/**
* 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 {
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))) {
if (
isBefore(date, RoomOccupancyList.setTimeOfDay(date, START_OF_WORKDAY))
) {
// shift the time to the end of the previous day
return RoomOccupancyList.endOfDay(subDays(date,1));
return RoomOccupancyList.endOfDay(subDays(date, 1));
} else {
return date;
}
@@ -277,16 +354,16 @@ export class RoomOccupancyList {
* @param time the time as Duration after 00:00.
* @returns new date with changed time values.
*/
private static setTimeOfDay(date : Date, time : Duration) : Date {
private static setTimeOfDay(date: Date, time: Duration): Date {
return add(RoomOccupancyList.startOfDay(date), time);
}
/**
* Start of day in server timezone defined as const TIMEZONE.
* @param date
* @param date
* @returns the start of the day.
*/
private static startOfDay(date : Date) : Date {
private static startOfDay(date: Date): Date {
const dateInLocalTimezone = toZonedTime(date, TIMEZONE);
return fromZonedTime(startOfDay(dateInLocalTimezone), TIMEZONE);
}
@@ -296,10 +373,8 @@ export class RoomOccupancyList {
* @param date
* @returns the end of the day.
*/
private static endOfDay(date : Date) : Date {
private static endOfDay(date: Date): Date {
const dateInLocalTimezone = toZonedTime(date, TIMEZONE);
return fromZonedTime(endOfDay(dateInLocalTimezone), TIMEZONE);
}
}

View File

@@ -129,8 +129,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
/>
</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>
<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"

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import CalendarViewer from "@/components/CalendarViewer.vue";
import DynamicPage from "@/view/DynamicPage.vue";
import { useI18n } from "vue-i18n";
@@ -12,7 +11,7 @@ import tokenStore from "@/store/tokenStore.ts";
const { t } = useI18n({ useScope: "global" });
const toast = useToast();
const token = ref(tokenStore().token || "" as string );
const token = ref(tokenStore().token || ("" as string));
// parse token from query parameter
const urlParams = new URLSearchParams(window.location.search);
@@ -46,7 +45,6 @@ onMounted(() => {
loadCalendar();
}
});
</script>
<template>
@@ -55,7 +53,7 @@ onMounted(() => {
:headline="$t('userCalender.headline')"
:sub-title="$t('userCalender.subTitle')"
>
<template #selection="{ flexSpecs }">
<template #selection="{ flexSpecs }">
<InputText
v-model="token"
:placeholder="$t('userCalender.searchPlaceholder')"
@@ -67,15 +65,11 @@ onMounted(() => {
icon="pi pi-refresh"
@click="loadCalendar()"
/>
</template>
<template #content>
<CalendarViewer
:token="tokenStore().token"
/>
</template>
</DynamicPage>
</template>
<template #content>
<CalendarViewer :token="tokenStore().token" />
</template>
</DynamicPage>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@@ -16,4 +16,4 @@
/// <reference types="vite/client" />
declare module 'ical.js';
declare module "ical.js";