mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-16 17:48:51 +02:00
Merge branch '3-semi-offline-room-finder' into 'main'
fix:#3 fetch room occupancy once Closes #3 See merge request htwk-software/htwkalender-pwa!17
This commit is contained in:
@ -478,7 +478,8 @@ $highlightFocusBg: rgba($primaryColor, 0.24) !default;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-event-selected::after, .fc-event:focus::after {
|
.fc-event-selected::after,
|
||||||
|
.fc-event:focus::after {
|
||||||
background: var(--fc-event-selected-overlay-color);
|
background: var(--fc-event-selected-overlay-color);
|
||||||
inset: -1px;
|
inset: -1px;
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -488,7 +488,8 @@ $highlightFocusBg: rgba($primaryColor, 0.24) !default;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-event-selected::after, .fc-event:focus::after {
|
.fc-event-selected::after,
|
||||||
|
.fc-event:focus::after {
|
||||||
background: var(--fc-event-selected-overlay-color);
|
background: var(--fc-event-selected-overlay-color);
|
||||||
inset: -1px;
|
inset: -1px;
|
||||||
content: "";
|
content: "";
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -37,7 +37,7 @@ const disabledPages = [
|
|||||||
"edit-calendar",
|
"edit-calendar",
|
||||||
"rooms",
|
"rooms",
|
||||||
"free-rooms",
|
"free-rooms",
|
||||||
"room-schedule"
|
"room-schedule",
|
||||||
];
|
];
|
||||||
|
|
||||||
const store = moduleStore();
|
const store = moduleStore();
|
||||||
@ -59,13 +59,18 @@ const settings = settingsStore;
|
|||||||
const emit = defineEmits(["dark-mode-toggled"]);
|
const emit = defineEmits(["dark-mode-toggled"]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// set theme matching browser preference
|
// set theme matching browser preference
|
||||||
settings().setDarkMode(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
settings().setDarkMode(
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||||
|
);
|
||||||
setTheme(settings, primeVue, emit);
|
setTheme(settings, primeVue, emit);
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
window
|
||||||
settings().setDarkMode(e.matches)
|
.matchMedia("(prefers-color-scheme: dark)")
|
||||||
setTheme(settings, primeVue, emit);
|
.addEventListener("change", (e) => {
|
||||||
});
|
settings().setDarkMode(e.matches);
|
||||||
|
setTheme(settings, primeVue, emit);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -16,16 +16,72 @@
|
|||||||
|
|
||||||
import { BSON } from "bson";
|
import { BSON } from "bson";
|
||||||
import { RoomOccupancyList } from "@/model/roomOccupancyList.ts";
|
import { RoomOccupancyList } from "@/model/roomOccupancyList.ts";
|
||||||
|
import { addMonths } from "date-fns";
|
||||||
|
import { formatYearMonthDay } from "@/helpers/dates";
|
||||||
|
|
||||||
|
const END_OF_SUMMER_SEMESTER = "0930";
|
||||||
|
const END_OF_WINTER_SEMESTER = "0331";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if date is in winter semester before summer semester
|
||||||
|
* @param date - The date to check
|
||||||
|
* @returns boolean - true if date is in winter semester
|
||||||
|
*/
|
||||||
|
export function isBeforeSummer(date: Date): boolean {
|
||||||
|
const formattedDate = formatYearMonthDay(date).slice(4);
|
||||||
|
return formattedDate <= END_OF_WINTER_SEMESTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if date is in winter semester after summer semester
|
||||||
|
* @param date - The date to check
|
||||||
|
* @returns boolean - true if date is in winter semester
|
||||||
|
*/
|
||||||
|
export function isAfterSummer(date: Date): boolean {
|
||||||
|
const formattedDate = formatYearMonthDay(date).slice(4);
|
||||||
|
return formattedDate > END_OF_SUMMER_SEMESTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the start date of the current semester
|
||||||
|
* @param date - The date to check
|
||||||
|
* @returns Date - The start date of the current semester
|
||||||
|
*/
|
||||||
|
export function getSemesterStart(date: Date): Date {
|
||||||
|
if (isBeforeSummer(date)) {
|
||||||
|
return new Date(date.getFullYear() - 1, 9, 1);
|
||||||
|
} else if (isAfterSummer(date)) {
|
||||||
|
return new Date(date.getFullYear(), 9, 1);
|
||||||
|
} else {
|
||||||
|
return new Date(date.getFullYear(), 3, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the room occupancy for a given date range.
|
||||||
|
* @param from_date the start date of the date range
|
||||||
|
* @param to_date the end date of the date range
|
||||||
|
* @returns RoomOccupancyList - the room occupancy list
|
||||||
|
*/
|
||||||
|
|
||||||
export async function fetchRoomOccupancy(
|
export async function fetchRoomOccupancy(
|
||||||
from_date: string,
|
from_date?: string,
|
||||||
to_date: string,
|
to_date?: string,
|
||||||
): Promise<RoomOccupancyList> {
|
): Promise<RoomOccupancyList> {
|
||||||
var roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
|
if (from_date == undefined) {
|
||||||
new Date(),
|
const new_from_date = getSemesterStart(new Date());
|
||||||
0,
|
from_date = new_from_date.toISOString();
|
||||||
0,
|
}
|
||||||
[],
|
if (to_date == undefined) {
|
||||||
|
const new_to_date = getSemesterStart(addMonths(new Date(), 6));
|
||||||
|
to_date = new_to_date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
|
||||||
|
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)
|
||||||
|
@ -71,7 +71,6 @@ const toggle = (info: EventClickArg) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const selectedToken = computed(() => {
|
const selectedToken = computed(() => {
|
||||||
return props.token;
|
return props.token;
|
||||||
});
|
});
|
||||||
@ -79,7 +78,7 @@ const selectedToken = computed(() => {
|
|||||||
const mobilePage = inject("mobilePage") as Ref<boolean>;
|
const mobilePage = inject("mobilePage") as Ref<boolean>;
|
||||||
const date: Ref<Date> = ref(new Date());
|
const date: Ref<Date> = ref(new Date());
|
||||||
|
|
||||||
const { data: calendar} = useQuery({
|
const { data: calendar } = useQuery({
|
||||||
queryKey: ["userCalendar", selectedToken],
|
queryKey: ["userCalendar", selectedToken],
|
||||||
queryFn: () => fetchICalendarEvents(selectedToken.value),
|
queryFn: () => fetchICalendarEvents(selectedToken.value),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
@ -89,7 +88,7 @@ const { data: calendar} = useQuery({
|
|||||||
refetchOnWindowFocus: "always",
|
refetchOnWindowFocus: "always",
|
||||||
refetchOnReconnect: "always",
|
refetchOnReconnect: "always",
|
||||||
networkMode: "offlineFirst",
|
networkMode: "offlineFirst",
|
||||||
enabled: () => tokenStore().token !== ""
|
enabled: () => tokenStore().token !== "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -97,13 +96,13 @@ const queryClient = useQueryClient();
|
|||||||
const invalidateAndRefetchCalendar = () => {
|
const invalidateAndRefetchCalendar = () => {
|
||||||
console.debug("invalidateAndRefetchCalendar", selectedToken);
|
console.debug("invalidateAndRefetchCalendar", selectedToken);
|
||||||
const queryKey = ["userCalendar", selectedToken];
|
const queryKey = ["userCalendar", selectedToken];
|
||||||
queryClient.invalidateQueries({queryKey: queryKey}).then(() => {
|
queryClient.invalidateQueries({ queryKey: queryKey }).then(() => {
|
||||||
queryClient.refetchQueries({queryKey: queryKey});
|
queryClient.refetchQueries({ queryKey: queryKey });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
invalidateAndRefetchCalendar
|
invalidateAndRefetchCalendar,
|
||||||
});
|
});
|
||||||
|
|
||||||
const events = computed(() => {
|
const events = computed(() => {
|
||||||
|
@ -29,7 +29,6 @@ const emit = defineEmits(["dark-mode-toggled"]);
|
|||||||
const store = settingsStore;
|
const store = settingsStore;
|
||||||
|
|
||||||
const isDark = computed(() => store().isDark);
|
const isDark = computed(() => store().isDark);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -20,10 +20,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
import { computed, ComputedRef, ref } from "vue";
|
import { computed, ComputedRef, ref } from "vue";
|
||||||
import settingsStore from "../store/settingsStore.ts";
|
import settingsStore from "../store/settingsStore.ts";
|
||||||
|
|
||||||
const pageOptions: ComputedRef<(string | {
|
const pageOptions: ComputedRef<
|
||||||
label: string;
|
(
|
||||||
value: string;
|
| string
|
||||||
})[]> = computed(() => [...settingsStore().getDefaultPageOptions()]);
|
| {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
)[]
|
||||||
|
> = computed(() => [...settingsStore().getDefaultPageOptions()]);
|
||||||
|
|
||||||
const selectedPage = ref(settingsStore().defaultPage);
|
const selectedPage = ref(settingsStore().defaultPage);
|
||||||
|
|
||||||
|
@ -21,8 +21,6 @@ import { computed } from "vue";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const items = computed(() => [
|
const items = computed(() => [
|
||||||
{
|
{
|
||||||
label: t("calendar"),
|
label: t("calendar"),
|
||||||
@ -133,7 +131,15 @@ const items = computed(() => [
|
|||||||
<div class="flex align-items-stretch justify-content-center">
|
<div class="flex align-items-stretch justify-content-center">
|
||||||
<!-- Settings Button with Gear Icon -->
|
<!-- Settings Button with Gear Icon -->
|
||||||
<router-link v-slot="{ navigate }" :to="`/settings`" custom>
|
<router-link v-slot="{ navigate }" :to="`/settings`" custom>
|
||||||
<Button icon="pi pi-cog" severity="secondary" rounded text size="large" aria-label="Settings" @click="navigate" />
|
<Button
|
||||||
|
icon="pi pi-cog"
|
||||||
|
severity="secondary"
|
||||||
|
rounded
|
||||||
|
text
|
||||||
|
size="large"
|
||||||
|
aria-label="Settings"
|
||||||
|
@click="navigate"
|
||||||
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -86,7 +86,7 @@ const { data: occupations } = useQuery({
|
|||||||
showFree: event.free,
|
showFree: event.free,
|
||||||
})),
|
})),
|
||||||
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
|
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
|
||||||
staleTime: 5000000, // 500 seconds
|
staleTime: 5000000, // 5000 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(occupations, () => fullCalendar.value?.getApi().refetchEvents());
|
watch(occupations, () => fullCalendar.value?.getApi().refetchEvents());
|
||||||
|
@ -21,17 +21,16 @@ import FullCalendar from "@fullcalendar/vue3";
|
|||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import { computed, ComputedRef, inject, ref, Ref } from "vue";
|
import {computed, ComputedRef, inject, ref, Ref, watch} from "vue";
|
||||||
import { CalendarOptions, DatesSetArg, EventInput } from "@fullcalendar/core";
|
import {CalendarOptions, DatesSetArg, EventInput} from "@fullcalendar/core";
|
||||||
import { useI18n } from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import allLocales from "@fullcalendar/core/locales-all";
|
import allLocales from "@fullcalendar/core/locales-all";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { formatYearMonthDay } from "@/helpers/dates";
|
import {formatYearMonthDay} from "@/helpers/dates";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import {useQuery} from "@tanstack/vue-query";
|
||||||
import { watch } from "vue";
|
import {fetchRoomOccupancy} from "@/api/fetchRoomOccupancy";
|
||||||
import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy";
|
import {isValid} from "date-fns";
|
||||||
import { isValid } from "date-fns";
|
import {RoomOccupancyList} from "@/model/roomOccupancyList";
|
||||||
import { RoomOccupancyList } from "@/model/roomOccupancyList";
|
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
@ -78,29 +77,27 @@ const selectedRoom = computed(() => props.room);
|
|||||||
* @returns Anonymized occupancy events
|
* @returns Anonymized occupancy events
|
||||||
*/
|
*/
|
||||||
function transformData(data: RoomOccupancyList) {
|
function transformData(data: RoomOccupancyList) {
|
||||||
const events = data
|
return data
|
||||||
.decodeOccupancy(
|
.decodeOccupancy(
|
||||||
selectedRoom.value,
|
selectedRoom.value,
|
||||||
new Date(currentDateFrom.value),
|
new Date(currentDateFrom.value),
|
||||||
new Date(currentDateTo.value),
|
new Date(currentDateTo.value),
|
||||||
)
|
)
|
||||||
.map((event, index) => ({
|
.map((event, index) => ({
|
||||||
id: index,
|
id: index,
|
||||||
event: event,
|
event: event,
|
||||||
}));
|
}));
|
||||||
return events;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: occupations } = useQuery({
|
const { data: occupancy } = useQuery({
|
||||||
queryKey: ["roomOccupation", selectedRoom, currentDateFrom, currentDateTo],
|
queryKey: ["roomOccupancy"], //, selectedRoom, currentDateFrom, currentDateTo],
|
||||||
queryFn: () =>
|
queryFn: () => fetchRoomOccupancy(),
|
||||||
fetchRoomOccupancy(
|
staleTime: 12 * 3600000, // 12 hours
|
||||||
new Date(currentDateFrom.value).toISOString(),
|
});
|
||||||
new Date(currentDateTo.value).toISOString(),
|
|
||||||
),
|
const occupations = computed(() => {
|
||||||
select: (data) => transformData(data),
|
if (!occupancy.value) return;
|
||||||
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
|
return transformData(occupancy.value);
|
||||||
staleTime: 5000000, // 500 seconds
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(occupations, () => fullCalendar.value?.getApi().refetchEvents());
|
watch(occupations, () => fullCalendar.value?.getApi().refetchEvents());
|
||||||
|
@ -41,10 +41,13 @@ export function parseICalData(
|
|||||||
const jCalData = ICAL.parse(icalData);
|
const jCalData = ICAL.parse(icalData);
|
||||||
const comp = new ICAL.Component(jCalData);
|
const comp = new ICAL.Component(jCalData);
|
||||||
const vEvents = comp.getAllSubcomponents("vevent");
|
const vEvents = comp.getAllSubcomponents("vevent");
|
||||||
const events: CalendarComponent[] = vEvents.map((vevent: CalendarComponent) => {
|
const events: CalendarComponent[] = vEvents.map(
|
||||||
return new ICAL.Event(vevent);
|
(vevent: CalendarComponent) => {
|
||||||
});
|
return new ICAL.Event(vevent);
|
||||||
const colorDistinctionEvents: ColorDistinctionEvent[] = extractedColorizedEvents(events);
|
},
|
||||||
|
);
|
||||||
|
const colorDistinctionEvents: ColorDistinctionEvent[] =
|
||||||
|
extractedColorizedEvents(events);
|
||||||
|
|
||||||
return vEvents.map((vevent: CalendarComponent) => {
|
return vEvents.map((vevent: CalendarComponent) => {
|
||||||
const event = new ICAL.Event(vevent);
|
const event = new ICAL.Event(vevent);
|
||||||
|
@ -20,27 +20,27 @@ import { EmitFn } from "primevue/ts-helpers";
|
|||||||
import settingsStore from "@/store/settingsStore.ts";
|
import settingsStore from "@/store/settingsStore.ts";
|
||||||
|
|
||||||
const darkTheme = ref("lara-dark-blue"),
|
const darkTheme = ref("lara-dark-blue"),
|
||||||
lightTheme = ref("lara-light-blue");
|
lightTheme = ref("lara-light-blue");
|
||||||
|
|
||||||
export type SettingsStore = typeof settingsStore;
|
export type SettingsStore = typeof settingsStore;
|
||||||
|
|
||||||
export function toggleTheme(
|
export function toggleTheme(
|
||||||
store: SettingsStore,
|
store: SettingsStore,
|
||||||
primeVue: { changeTheme: PrimeVueChangeTheme },
|
primeVue: { changeTheme: PrimeVueChangeTheme },
|
||||||
emit: EmitFn<"dark-mode-toggled"[]>,
|
emit: EmitFn<"dark-mode-toggled"[]>,
|
||||||
): void {
|
): void {
|
||||||
store().setDarkMode(!store().isDark);
|
store().setDarkMode(!store().isDark);
|
||||||
setTheme(store, primeVue, emit);
|
setTheme(store, primeVue, emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTheme(
|
export function setTheme(
|
||||||
store: SettingsStore,
|
store: SettingsStore,
|
||||||
{ changeTheme }: { changeTheme: PrimeVueChangeTheme },
|
{ changeTheme }: { changeTheme: PrimeVueChangeTheme },
|
||||||
emit: EmitFn<"dark-mode-toggled"[]>
|
emit: EmitFn<"dark-mode-toggled"[]>,
|
||||||
) {
|
) {
|
||||||
const isDark = ref(store().isDark);
|
const isDark = ref(store().isDark);
|
||||||
const newTheme = isDark.value ? darkTheme.value : lightTheme.value,
|
const newTheme = isDark.value ? darkTheme.value : lightTheme.value,
|
||||||
oldTheme = isDark.value ? lightTheme.value : darkTheme.value;
|
oldTheme = isDark.value ? lightTheme.value : darkTheme.value;
|
||||||
changeTheme(oldTheme, newTheme, "theme-link", () => { });
|
changeTheme(oldTheme, newTheme, "theme-link", () => {});
|
||||||
emit("dark-mode-toggled", isDark.value);
|
emit("dark-mode-toggled", isDark.value);
|
||||||
}
|
}
|
@ -70,10 +70,10 @@ app.use(VueQueryPlugin, {
|
|||||||
queryClientConfig: {
|
queryClientConfig: {
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(PrimeVue);
|
app.use(PrimeVue);
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
//You should have received a copy of the GNU Affero General Public License
|
//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/>.
|
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { Binary } from "bson";
|
import {Binary, Document} from "bson";
|
||||||
import { AnonymizedOccupancy } from "./event";
|
import { AnonymizedOccupancy } from "./event";
|
||||||
import {
|
import {
|
||||||
Duration,
|
Duration,
|
||||||
@ -92,19 +92,23 @@ export class RoomOccupancyList {
|
|||||||
): AnonymizedOccupancy[] {
|
): AnonymizedOccupancy[] {
|
||||||
const roomOccupancy = this.rooms.find((r) => r.name === room);
|
const roomOccupancy = this.rooms.find((r) => r.name === room);
|
||||||
|
|
||||||
if (roomOccupancy === undefined) {
|
// Get start and end of decoded time range (within encoded list and requested range)
|
||||||
|
const decodeInterval = interval(
|
||||||
|
clamp(from, this.getOccupancyInterval()),
|
||||||
|
clamp(to, this.getOccupancyInterval()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// if the room is not in the list or the time range is empty, return stub events
|
||||||
|
if (
|
||||||
|
roomOccupancy === undefined ||
|
||||||
|
isEqual(decodeInterval.start, decodeInterval.end)
|
||||||
|
) {
|
||||||
return RoomOccupancyList.generateStubEvents(room, from, to);
|
return RoomOccupancyList.generateStubEvents(room, from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
const occupancyList = [];
|
const occupancyList = [];
|
||||||
|
|
||||||
// Get start and end of decoded time range (within encoded list and requested range)
|
const { decodeSliceStart, decodeSlice } = this.sliceOccupancy(
|
||||||
let decodeInterval = interval(
|
|
||||||
clamp(from, this.getOccupancyInterval()),
|
|
||||||
clamp(to, this.getOccupancyInterval()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let { decodeSliceStart, decodeSlice } = this.sliceOccupancy(
|
|
||||||
decodeInterval,
|
decodeInterval,
|
||||||
roomOccupancy.occupancy.buffer,
|
roomOccupancy.occupancy.buffer,
|
||||||
);
|
);
|
||||||
@ -141,10 +145,10 @@ export class RoomOccupancyList {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Slice the important parts of the occupancy list for a given time range.
|
* Slice the important parts of the occupancy list for a given time range.
|
||||||
* @param from the start of the time range.
|
|
||||||
* @param to the end of the time range.
|
|
||||||
* @returns a new occupancy byte array with the starting time of the first byte
|
* @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.
|
* @throws an error, if the selected time range is outside of the occupancy list.
|
||||||
|
* @param decodeInterval
|
||||||
|
* @param occupancy
|
||||||
*/
|
*/
|
||||||
private sliceOccupancy(
|
private sliceOccupancy(
|
||||||
decodeInterval: NormalizedInterval,
|
decodeInterval: NormalizedInterval,
|
||||||
@ -152,14 +156,14 @@ export class RoomOccupancyList {
|
|||||||
): { decodeSliceStart: Date; decodeSlice: Uint8Array } {
|
): { decodeSliceStart: Date; decodeSlice: Uint8Array } {
|
||||||
// Calculate the slice of bytes, that are needed to decode the requested time range
|
// Calculate the slice of bytes, that are needed to decode the requested time range
|
||||||
// Note: differenceInMinutes calculates (left - right)
|
// Note: differenceInMinutes calculates (left - right)
|
||||||
let minutesFromStart = differenceInMinutes(
|
const minutesFromStart = differenceInMinutes(
|
||||||
decodeInterval.start,
|
decodeInterval.start,
|
||||||
this.start,
|
this.start,
|
||||||
);
|
);
|
||||||
let minutesToEnd = differenceInMinutes(decodeInterval.end, this.start);
|
const minutesToEnd = differenceInMinutes(decodeInterval.end, this.start);
|
||||||
|
|
||||||
let firstByte = Math.floor(minutesFromStart / this.granularity / 8);
|
const firstByte = Math.floor(minutesFromStart / this.granularity / 8);
|
||||||
let lastByte = Math.ceil(minutesToEnd / this.granularity / 8);
|
const lastByte = Math.ceil(minutesToEnd / this.granularity / 8);
|
||||||
|
|
||||||
// check if firstByte and lastByte are within the bounds of the occupancy array and throw an error if not
|
// check if firstByte and lastByte are within the bounds of the occupancy array and throw an error if not
|
||||||
if (
|
if (
|
||||||
@ -171,11 +175,11 @@ export class RoomOccupancyList {
|
|||||||
throw new Error("Requested time range is outside of the occupancy list.");
|
throw new Error("Requested time range is outside of the occupancy list.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let decodeSliceStart = addMinutes(
|
const decodeSliceStart = addMinutes(
|
||||||
this.start,
|
this.start,
|
||||||
firstByte * 8 * this.granularity,
|
firstByte * 8 * this.granularity,
|
||||||
);
|
);
|
||||||
let decodeSlice = occupancy.buffer.slice(firstByte, lastByte);
|
const decodeSlice = occupancy.buffer.slice(firstByte, lastByte);
|
||||||
|
|
||||||
return { decodeSliceStart, decodeSlice: new Uint8Array(decodeSlice) };
|
return { decodeSliceStart, decodeSlice: new Uint8Array(decodeSlice) };
|
||||||
}
|
}
|
||||||
@ -205,43 +209,44 @@ export class RoomOccupancyList {
|
|||||||
granularity: number,
|
granularity: number,
|
||||||
room: string,
|
room: string,
|
||||||
): AnonymizedOccupancy[] {
|
): AnonymizedOccupancy[] {
|
||||||
let occupancyList = [];
|
const occupancyList = [];
|
||||||
let firstOccupancyBit: number | null = null;
|
let firstOccupancyBit: number | null = null;
|
||||||
|
|
||||||
// Iterate over all bytes that are in the array
|
// Iterate over all bytes that are in the array
|
||||||
for (let byte_i = 0; byte_i < occupancy.length; byte_i++) {
|
for (let byte_i = 0; byte_i < occupancy.length; byte_i++) {
|
||||||
let byte = occupancy[byte_i];
|
const byte = occupancy[byte_i];
|
||||||
|
|
||||||
// Iterate over all bits in the current byte
|
// Iterate over all bits in the current byte
|
||||||
for (let bit_i = 0; bit_i < 8; bit_i++) {
|
for (let bit_i = 0; bit_i < 8; bit_i++) {
|
||||||
let isOccupied = (byte & (1 << (7 - bit_i))) !== 0;
|
const isOccupied = (byte & (1 << (7 - bit_i))) !== 0;
|
||||||
|
const calculateOccupancyBitIndex = (byte_i: number, bit_i: number) => byte_i * 8 + bit_i;
|
||||||
|
|
||||||
if (firstOccupancyBit === null && isOccupied) {
|
if(firstOccupancyBit === null){
|
||||||
firstOccupancyBit = byte_i * 8 + bit_i;
|
if (isOccupied) {
|
||||||
} else if (firstOccupancyBit !== null && !isOccupied) {
|
firstOccupancyBit = calculateOccupancyBitIndex(byte_i, bit_i);
|
||||||
let startTime = addMinutes(start, firstOccupancyBit * granularity);
|
}
|
||||||
let endTime = addMinutes(start, (byte_i * 8 + bit_i) * granularity);
|
} else {
|
||||||
|
if (!isOccupied) {
|
||||||
// add event between start and end of a block of boolean true values
|
const startTime = addMinutes(start, firstOccupancyBit * granularity);
|
||||||
occupancyList.push(
|
const endTime = addMinutes(start, calculateOccupancyBitIndex(byte_i, bit_i) * granularity);
|
||||||
new AnonymizedOccupancy(
|
// add event between start and end of a block of boolean true values
|
||||||
startTime.toISOString(),
|
occupancyList.push(
|
||||||
endTime.toISOString(),
|
new AnonymizedOccupancy(
|
||||||
room,
|
startTime.toISOString(),
|
||||||
false,
|
endTime.toISOString(),
|
||||||
false,
|
room,
|
||||||
),
|
false,
|
||||||
);
|
false,
|
||||||
|
),
|
||||||
firstOccupancyBit = null;
|
);
|
||||||
|
firstOccupancyBit = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// add last event if it is still ongoing
|
// add last event if it is still ongoing
|
||||||
if (firstOccupancyBit !== null) {
|
if (firstOccupancyBit !== null) {
|
||||||
let startTime = addMinutes(start, firstOccupancyBit * granularity);
|
const startTime = addMinutes(start, firstOccupancyBit * granularity);
|
||||||
let endTime = addMinutes(start, occupancy.length * 8 * granularity);
|
const endTime = addMinutes(start, occupancy.length * 8 * granularity);
|
||||||
|
|
||||||
occupancyList.push(
|
occupancyList.push(
|
||||||
new AnonymizedOccupancy(
|
new AnonymizedOccupancy(
|
||||||
@ -261,6 +266,7 @@ export class RoomOccupancyList {
|
|||||||
* Generate a list of AnonymizedOccupancy objects for a given time range.
|
* 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].
|
* The generated events are always lying within the time range [START_OF_DAY, END_OF_DAY].
|
||||||
*
|
*
|
||||||
|
* @param rooms
|
||||||
* @param from The start time within the specified start day.
|
* @param from The start time within the specified start day.
|
||||||
* @param to The end time within the specified end day.
|
* @param to The end time within the specified end day.
|
||||||
* @returns a list of AnonymizedEventDTO objects, from start to end.
|
* @returns a list of AnonymizedEventDTO objects, from start to end.
|
||||||
@ -278,11 +284,11 @@ export class RoomOccupancyList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return eachDayOfInterval({ start: from, end: to }).map((day) => {
|
return eachDayOfInterval({ start: from, end: to }).map((day) => {
|
||||||
let startTime = max([
|
const startTime = max([
|
||||||
from,
|
from,
|
||||||
RoomOccupancyList.setTimeOfDay(day, START_OF_WORKDAY),
|
RoomOccupancyList.setTimeOfDay(day, START_OF_WORKDAY),
|
||||||
]);
|
]);
|
||||||
let endTime = min([
|
const endTime = min([
|
||||||
to,
|
to,
|
||||||
RoomOccupancyList.setTimeOfDay(day, END_OF_WORKDAY),
|
RoomOccupancyList.setTimeOfDay(day, END_OF_WORKDAY),
|
||||||
]);
|
]);
|
||||||
@ -303,13 +309,13 @@ export class RoomOccupancyList {
|
|||||||
* @param json the JS object to read from.
|
* @param json the JS object to read from.
|
||||||
* @returns a RoomOccupancyList object.
|
* @returns a RoomOccupancyList object.
|
||||||
*/
|
*/
|
||||||
public static fromJSON(json: any): RoomOccupancyList {
|
public static fromJSON(json: Document): RoomOccupancyList {
|
||||||
return new RoomOccupancyList(
|
return new RoomOccupancyList(
|
||||||
json.start,
|
json.start,
|
||||||
json.granularity,
|
json.granularity,
|
||||||
json.blocks,
|
json.blocks,
|
||||||
json.rooms.map(
|
json.rooms.map(
|
||||||
(room: any) => new RoomOccupancy(room.name, room.occupancy),
|
(room: { name: string, occupancy: Binary }) => new RoomOccupancy(room.name, room.occupancy),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,6 @@ const NotFound = () => import("../view/NotFound.vue");
|
|||||||
import i18n from "../i18n";
|
import i18n from "../i18n";
|
||||||
import settingsStore from "@/store/settingsStore.ts";
|
import settingsStore from "@/store/settingsStore.ts";
|
||||||
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
|
@ -23,7 +23,10 @@ const settingsStore = defineStore("settingsStore", {
|
|||||||
return {
|
return {
|
||||||
locale: useLocalStorage("locale", "en"), //useLocalStorage takes in a key of 'count' and default value of 0
|
locale: useLocalStorage("locale", "en"), //useLocalStorage takes in a key of 'count' and default value of 0
|
||||||
isDark: true,
|
isDark: true,
|
||||||
defaultPage: useLocalStorage("defaultPage", {label: "Home", value: "/home"}),
|
defaultPage: useLocalStorage("defaultPage", {
|
||||||
|
label: "Home",
|
||||||
|
value: "/home",
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -36,10 +39,7 @@ const settingsStore = defineStore("settingsStore", {
|
|||||||
getDarkMode(): boolean {
|
getDarkMode(): boolean {
|
||||||
return this.isDark;
|
return this.isDark;
|
||||||
},
|
},
|
||||||
setDefaultPage(page: {
|
setDefaultPage(page: { label: string; value: string }) {
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}) {
|
|
||||||
this.defaultPage = page;
|
this.defaultPage = page;
|
||||||
},
|
},
|
||||||
getDefaultPageOptions(): {
|
getDefaultPageOptions(): {
|
||||||
@ -58,10 +58,10 @@ const settingsStore = defineStore("settingsStore", {
|
|||||||
label: route.name,
|
label: route.name,
|
||||||
value: route.path,
|
value: route.path,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return options;
|
});
|
||||||
|
return options;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,4 @@ import DynamicPage from "@/view/DynamicPage.vue";
|
|||||||
</DynamicPage>
|
</DynamicPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
import LocaleSwitcher from "@/components/LocaleSwitcher.vue";
|
import LocaleSwitcher from "@/components/LocaleSwitcher.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import DarkModeSwitcher from "@/components/DarkModeSwitcher.vue";
|
import DarkModeSwitcher from "@/components/DarkModeSwitcher.vue";
|
||||||
@ -14,7 +13,6 @@ function handleDarkModeToggled(isDarkVar: boolean) {
|
|||||||
// Assuming the root component has an isDark ref
|
// Assuming the root component has an isDark ref
|
||||||
isDark.value = isDarkVar;
|
isDark.value = isDarkVar;
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -34,7 +32,7 @@ function handleDarkModeToggled(isDarkVar: boolean) {
|
|||||||
<slot flex-specs="flex-1 m-0" name="selection"></slot>
|
<slot flex-specs="flex-1 m-0" name="selection"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="opacity-100 transition-all transition-duration-500 transition-ease-in-out w-full lg:w-8"
|
class="opacity-100 transition-all transition-duration-500 transition-ease-in-out w-full lg:w-8"
|
||||||
>
|
>
|
||||||
<div class="flex flex-column justify-content-center">
|
<div class="flex flex-column justify-content-center">
|
||||||
<div class="grid my-2">
|
<div class="grid my-2">
|
||||||
@ -68,11 +66,9 @@ class="opacity-100 transition-all transition-duration-500 transition-ease-in-out
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.col {
|
.col {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -42,12 +42,12 @@ function loadCalendar() {
|
|||||||
|
|
||||||
calendarViewerRef.value?.invalidateAndRefetchCalendar();
|
calendarViewerRef.value?.invalidateAndRefetchCalendar();
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: "success",
|
severity: "success",
|
||||||
summary: t("editCalendarView.toast.success"),
|
summary: t("editCalendarView.toast.success"),
|
||||||
detail: t("editCalendarView.toast.successDetailLoad"),
|
detail: t("editCalendarView.toast.successDetailLoad"),
|
||||||
life: 3000,
|
life: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -77,7 +77,7 @@ onMounted(() => {
|
|||||||
></Button>
|
></Button>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<CalendarViewer :token="tokenStore().token" ref="calendarViewerRef" />
|
<CalendarViewer ref="calendarViewerRef" :token="tokenStore().token" />
|
||||||
</template>
|
</template>
|
||||||
</DynamicPage>
|
</DynamicPage>
|
||||||
</template>
|
</template>
|
||||||
|
@ -40,18 +40,18 @@ export default defineConfig({
|
|||||||
start_url: "/",
|
start_url: "/",
|
||||||
id: "de.htwk-leipzig.htwkalender",
|
id: "de.htwk-leipzig.htwkalender",
|
||||||
screenshots: [
|
screenshots: [
|
||||||
{
|
{
|
||||||
src: "/1280x720.png",
|
src: "/1280x720.png",
|
||||||
sizes: "1280x720",
|
sizes: "1280x720",
|
||||||
form_factor: "wide",
|
form_factor: "wide",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "/390x844.png",
|
src: "/390x844.png",
|
||||||
sizes: "1170x2532",
|
sizes: "1170x2532",
|
||||||
form_factor: "narrow",
|
form_factor: "narrow",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
@ -86,11 +86,11 @@ export default defineConfig({
|
|||||||
cleanupOutdatedCaches: true,
|
cleanupOutdatedCaches: true,
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: ({ url }) => url.pathname.startsWith('/api/feed'),
|
urlPattern: ({ url }) => url.pathname.startsWith("/api/feed"),
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
handler: 'NetworkFirst',
|
handler: "NetworkFirst",
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'calendar-feed-cache',
|
cacheName: "calendar-feed-cache",
|
||||||
expiration: {
|
expiration: {
|
||||||
maxAgeSeconds: 12 * 60 * 60, // 12 hours
|
maxAgeSeconds: 12 * 60 * 60, // 12 hours
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user