mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-16 09:38:51 +02:00
Merge branch '22-semi-offline-room-finder' into 'main'
Resolve "semi offline room finder: episode v - the cache stacks back" See merge request htwk-software/htwkalender-pwa!26
This commit is contained in:
@ -29,6 +29,7 @@ type RoomOccupancy struct {
|
||||
|
||||
type RoomOccupancyList struct {
|
||||
Start time.Time `bson:"start"`
|
||||
Updated time.Time `bson:"updated"`
|
||||
Granularity int `bson:"granularity"`
|
||||
Blocks int `bson:"blocks"`
|
||||
Rooms []RoomOccupancy `bson:"rooms"`
|
||||
|
@ -119,21 +119,6 @@ paths:
|
||||
/api/schedule/rooms:
|
||||
get:
|
||||
summary: Get Room Occupancy
|
||||
parameters:
|
||||
- name: from
|
||||
in: query
|
||||
description: date
|
||||
example: "2024-12-24T00:00:00.000Z"
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: to
|
||||
in: query
|
||||
description: date
|
||||
example: "2024-12-25T00:00:00.000Z"
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
@ -294,6 +279,9 @@ components:
|
||||
start:
|
||||
type: string
|
||||
format: date-time
|
||||
updated:
|
||||
type: string
|
||||
format: date-time
|
||||
granularity:
|
||||
type: integer
|
||||
blocks:
|
||||
|
@ -181,9 +181,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/schedule/rooms",
|
||||
Handler: func(c echo.Context) error {
|
||||
from := c.QueryParam("from")
|
||||
to := c.QueryParam("to")
|
||||
rooms, err := room.GetRoomOccupancyList(app, from, to, RoomOccupancyGranularity)
|
||||
rooms, err := room.GetRoomOccupancyList(app, RoomOccupancyGranularity)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Failed to get room occupancy: %v", "error", err)
|
||||
|
@ -16,15 +16,46 @@
|
||||
|
||||
package functions
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const START_OF_SUMMER_SEMESTER_MONTH = time.April
|
||||
const START_OF_WINTER_SEMESTER_MONTH = time.October
|
||||
|
||||
// GetCurrentSemesterString returns the current semester as string
|
||||
// if current month is between 10 and 03 -> winter semester "ws"
|
||||
func GetCurrentSemesterString() string {
|
||||
|
||||
if time.Now().Month() >= 10 || time.Now().Month() <= 3 {
|
||||
if now := time.Now(); isBeforeSummerSemester(now) || isAfterSummerSemester(now) {
|
||||
return "ws"
|
||||
} else {
|
||||
return "ss"
|
||||
}
|
||||
}
|
||||
|
||||
// GetSemesterStart gibt das Startdatum des aktuellen Semesters zurück
|
||||
func GetSemesterStart(date time.Time) time.Time {
|
||||
if isBeforeSummerSemester(date) {
|
||||
return time.Date(date.Year()-1, START_OF_WINTER_SEMESTER_MONTH, 1, 0, 0, 0, 0, date.Location())
|
||||
} else if isAfterSummerSemester(date) {
|
||||
return time.Date(date.Year(), START_OF_WINTER_SEMESTER_MONTH, 1, 0, 0, 0, 0, date.Location())
|
||||
} else {
|
||||
return time.Date(date.Year(), START_OF_SUMMER_SEMESTER_MONTH, 1, 0, 0, 0, 0, date.Location())
|
||||
}
|
||||
}
|
||||
|
||||
// Check if is in last month of semester
|
||||
func IsLastMonthOfSemester(date time.Time) bool {
|
||||
return date.Month() == START_OF_WINTER_SEMESTER_MONTH-1 || date.Month() == START_OF_SUMMER_SEMESTER_MONTH-1
|
||||
}
|
||||
|
||||
// Check if the given date is before the start of summer semester
|
||||
func isBeforeSummerSemester(date time.Time) bool {
|
||||
return date.Month() < START_OF_SUMMER_SEMESTER_MONTH
|
||||
}
|
||||
|
||||
// Check if the given date is after the end of summer semester
|
||||
func isAfterSummerSemester(date time.Time) bool {
|
||||
return date.Month() >= START_OF_WINTER_SEMESTER_MONTH
|
||||
}
|
||||
|
@ -68,15 +68,14 @@ func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to st
|
||||
* @return room occupancy list
|
||||
* @return error if the database query fails
|
||||
*/
|
||||
func GetRoomOccupancyList(app *pocketbase.PocketBase, from string, to string, granularity int) (model.RoomOccupancyList, error) {
|
||||
// try parsing the time strings
|
||||
fromTime, err := time.Parse(time.RFC3339, from)
|
||||
if err != nil {
|
||||
return model.RoomOccupancyList{}, err
|
||||
}
|
||||
toTime, err := time.Parse(time.RFC3339, to)
|
||||
if err != nil {
|
||||
return model.RoomOccupancyList{}, err
|
||||
func GetRoomOccupancyList(app *pocketbase.PocketBase, granularity int) (model.RoomOccupancyList, error) {
|
||||
|
||||
now := time.Now()
|
||||
fromTime := functions.GetSemesterStart(now)
|
||||
toTime := functions.GetSemesterStart(now.AddDate(0, 6, 0))
|
||||
|
||||
if functions.IsLastMonthOfSemester(now) {
|
||||
toTime = functions.GetSemesterStart(now.AddDate(1, 0, 0))
|
||||
}
|
||||
|
||||
// calculate the number of blocks for the given time range and granularity
|
||||
@ -142,6 +141,7 @@ func getRelevantRooms(app *pocketbase.PocketBase) ([]string, error) {
|
||||
func emptyRoomOccupancyList(from time.Time, granularity int, blockCount int) model.RoomOccupancyList {
|
||||
return model.RoomOccupancyList{
|
||||
Start: from,
|
||||
Updated: time.Now(),
|
||||
Granularity: granularity,
|
||||
Blocks: blockCount,
|
||||
Rooms: []model.RoomOccupancy{},
|
||||
|
@ -26,3 +26,38 @@ self.addEventListener("install", (event) => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Custom precaching logic for the /api/schedule/rooms endpoint
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open("room-schedule-cache").then((cache) => {
|
||||
return fetch("/api/schedule/rooms")
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return cache.put("/api/schedule/rooms", response);
|
||||
}
|
||||
throw new Error("Failed to fetch /api/schedule/rooms");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Precaching /api/schedule/rooms failed:", error);
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// StaleWhileRevalidate strategy for the /api/schedule/rooms endpoint
|
||||
self.addEventListener("fetch", (event) => {
|
||||
if (event.request.url.includes("/api/schedule/rooms")) {
|
||||
event.respondWith(
|
||||
caches.open("room-schedule-cache").then((cache) => {
|
||||
return cache.match(event.request).then((response) => {
|
||||
const fetchPromise = fetch(event.request).then((networkResponse) => {
|
||||
cache.put(event.request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
});
|
||||
return response || fetchPromise;
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
import { BSON } from "bson";
|
||||
import { RoomOccupancyList } from "@/model/roomOccupancyList.ts";
|
||||
import { addMonths } from "date-fns";
|
||||
import { formatYearMonthDay } from "@/helpers/dates";
|
||||
|
||||
const END_OF_SUMMER_SEMESTER = "0930";
|
||||
@ -59,32 +58,19 @@ export function getSemesterStart(date: Date): Date {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns RoomOccupancyList - the room occupancy list containing all rooms
|
||||
*/
|
||||
|
||||
export async function fetchRoomOccupancy(
|
||||
from_date?: string,
|
||||
to_date?: string,
|
||||
): Promise<RoomOccupancyList> {
|
||||
if (from_date == undefined) {
|
||||
const new_from_date = getSemesterStart(new Date());
|
||||
from_date = new_from_date.toISOString();
|
||||
}
|
||||
if (to_date == undefined) {
|
||||
const new_to_date = getSemesterStart(addMonths(new Date(), 6));
|
||||
to_date = new_to_date.toISOString();
|
||||
}
|
||||
|
||||
export async function fetchRoomOccupancy(): Promise<RoomOccupancyList> {
|
||||
let roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
|
||||
new Date(),
|
||||
new Date(2000, 0, 1),
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
);
|
||||
|
||||
await fetch("/api/schedule/rooms?from=" + from_date + "&to=" + to_date)
|
||||
await fetch("/api/schedule/rooms")
|
||||
.then((response) => {
|
||||
return response.arrayBuffer();
|
||||
})
|
||||
|
@ -27,8 +27,6 @@ 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 { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy";
|
||||
import { isValid } from "date-fns";
|
||||
import { RoomOccupancyList } from "@/model/roomOccupancyList";
|
||||
|
||||
@ -39,6 +37,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
occupancy: {
|
||||
type: RoomOccupancyList,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const date: Ref<Date> = ref(new Date());
|
||||
@ -56,6 +58,7 @@ function setDateFromQuery() {
|
||||
const day = queryDate.substring(6, 8);
|
||||
date.value = new Date(`${year}-${month}-${day}`);
|
||||
if (!isValid(date.value)) {
|
||||
console.error("Invalid date in URL, using current date instead.");
|
||||
date.value = new Date();
|
||||
}
|
||||
}
|
||||
@ -77,6 +80,9 @@ const selectedRoom = computed(() => props.room);
|
||||
* @returns Anonymized occupancy events
|
||||
*/
|
||||
function transformData(data: RoomOccupancyList) {
|
||||
if (!data || !selectedRoom.value || !currentDateFrom.value || !currentDateTo.value) {
|
||||
return [];
|
||||
}
|
||||
return data
|
||||
.decodeOccupancy(
|
||||
selectedRoom.value,
|
||||
@ -89,15 +95,9 @@ function transformData(data: RoomOccupancyList) {
|
||||
}));
|
||||
}
|
||||
|
||||
const { data: occupancy } = useQuery({
|
||||
queryKey: ["roomOccupancy"], //, selectedRoom, currentDateFrom, currentDateTo],
|
||||
queryFn: () => fetchRoomOccupancy(),
|
||||
staleTime: 12 * 3600000, // 12 hours
|
||||
});
|
||||
|
||||
const occupations = computed(() => {
|
||||
if (!occupancy.value) return;
|
||||
return transformData(occupancy.value);
|
||||
if (!props.occupancy) return;
|
||||
return transformData(props.occupancy);
|
||||
});
|
||||
|
||||
watch(occupations, () => fullCalendar.value?.getApi().refetchEvents());
|
||||
|
@ -33,7 +33,9 @@
|
||||
"noRoomsAvailable": "Keine Räume verfügbar",
|
||||
"available": "verfügbar",
|
||||
"occupied": "belegt",
|
||||
"stub": "bitte online prüfen"
|
||||
"stub": "bitte online prüfen",
|
||||
"lastUpdate": "Zeitpunkt der letzten Aktualisierung",
|
||||
"noData": "Es konnten keine Daten geladen werden."
|
||||
},
|
||||
"freeRooms": {
|
||||
"freeRooms": "Freie Räume",
|
||||
|
@ -33,7 +33,9 @@
|
||||
"noRoomsAvailable": "no rooms listed",
|
||||
"available": "available",
|
||||
"occupied": "occupied",
|
||||
"stub": "please check online"
|
||||
"stub": "please check online",
|
||||
"lastUpdate": "time of last update",
|
||||
"noData": "could not load data"
|
||||
},
|
||||
"freeRooms": {
|
||||
"freeRooms": "free rooms",
|
||||
|
@ -33,7 +33,9 @@
|
||||
"noRoomsAvailable": "利用可能な部屋がありません",
|
||||
"available": "利用可能",
|
||||
"occupied": "占有中",
|
||||
"stub": "オンラインでご確認ください。"
|
||||
"stub": "オンラインでご確認ください。",
|
||||
"lastUpdate": "最終更新時刻",
|
||||
"noData": "データをロードできませんでした。"
|
||||
},
|
||||
"freeRooms": {
|
||||
"freeRooms": "空いている部屋",
|
||||
|
@ -65,6 +65,7 @@ class RoomOccupancy {
|
||||
export class RoomOccupancyList {
|
||||
constructor(
|
||||
public start: Date,
|
||||
public updated: Date,
|
||||
public granularity: number,
|
||||
public blocks: number,
|
||||
public rooms: RoomOccupancy[],
|
||||
@ -319,6 +320,7 @@ export class RoomOccupancyList {
|
||||
public static fromJSON(json: Document): RoomOccupancyList {
|
||||
return new RoomOccupancyList(
|
||||
json.start,
|
||||
json.updated,
|
||||
json.granularity,
|
||||
json.blocks,
|
||||
json.rooms.map(
|
||||
|
@ -18,16 +18,29 @@ 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";
|
||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy.ts";
|
||||
|
||||
type Room = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: occupancy } = useQuery({
|
||||
queryKey: ["roomOccupancy"],
|
||||
queryFn: () => fetchRoomOccupancy(),
|
||||
staleTime: 12 * 3600000, // 12 hours
|
||||
});
|
||||
|
||||
// Manually refetch the room occupancy data
|
||||
function refetchRoomOccupancy() {
|
||||
queryClient.invalidateQueries({ queryKey: ["roomOccupancy"] });
|
||||
}
|
||||
|
||||
const selectedRoom: Ref<Room> = ref({ name: "" });
|
||||
|
||||
// Watch for changes in URL parameter
|
||||
@ -38,21 +51,15 @@ router.afterEach(async (to) => {
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
const rooms = computed<Set<string>>(() => {
|
||||
const rooms: Set<string> = new Set();
|
||||
|
||||
occupancy.value?.rooms.forEach((room) => {
|
||||
rooms.add(room.name);
|
||||
});
|
||||
}, new Set());
|
||||
|
||||
return rooms;
|
||||
});
|
||||
|
||||
const roomsList = computed(() => {
|
||||
return Array.from(rooms.value).map((room) => {
|
||||
@ -102,7 +109,16 @@ watch(selectedRoom, (newRoom: Room) => {
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<RoomOccupationOffline :room="selectedRoom.name" />
|
||||
<RoomOccupationOffline v-if="occupancy" :room="selectedRoom.name" :occupancy="occupancy"/>
|
||||
<div v-else>
|
||||
<p>{{ $t('roomFinderPage.noData') }}</p>
|
||||
</div>
|
||||
|
||||
<!--last update date-->
|
||||
<div class="flex align-items-baseline justify-content-end text-right text-xs text-gray-500">
|
||||
<span v-if="occupancy">{{ $t("roomFinderPage.lastUpdate") }}: {{ $d(occupancy.updated, 'long') }}</span>
|
||||
<Button size="small" icon="pi pi-refresh" class="p-button-text p-button-sm" @click="refetchRoomOccupancy" />
|
||||
</div>
|
||||
</template>
|
||||
</DynamicPage>
|
||||
</template>
|
||||
|
@ -89,7 +89,7 @@ export default defineConfig({
|
||||
registerType: "autoUpdate",
|
||||
strategies: "injectManifest",
|
||||
injectManifest: {
|
||||
globPatterns: ["**/*.{js,css,html,png,svg}"],
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg,json,vue,txt,woff2}", "/api/schedule/rooms"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
Reference in New Issue
Block a user