feat:#5 added first offline calendar functions

This commit is contained in:
Elmar Kresse
2024-08-13 19:16:07 +02:00
parent b00528dfc1
commit b848d26704
9 changed files with 134 additions and 53 deletions

View File

@ -78,10 +78,10 @@ export async function fetchRoomOccupancy(
} }
let roomOccupancyList: RoomOccupancyList = new RoomOccupancyList( let roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
new Date(), new Date(),
0, 0,
0, 0,
[], [],
); );
await fetch("/api/schedule/rooms?from=" + from_date + "&to=" + to_date) await fetch("/api/schedule/rooms?from=" + from_date + "&to=" + to_date)

View File

@ -15,7 +15,8 @@
//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 { Module } from "../model/module"; import { Module } from "../model/module";
import { Calendar } from "../model/calendar"; import tokenStore from "@/store/tokenStore.ts";
import { Calendar } from "@/model/calendar.ts";
export async function getCalender(token: string): Promise<Module[]> { export async function getCalender(token: string): Promise<Module[]> {
const request = new Request("/api/collections/feeds/records/" + token, { const request = new Request("/api/collections/feeds/records/" + token, {
@ -24,9 +25,10 @@ export async function getCalender(token: string): Promise<Module[]> {
return await fetch(request).then((response) => { return await fetch(request).then((response) => {
if (response.ok) { if (response.ok) {
return response return response.json().then((calendarResponse: Calendar) => {
.json() tokenStore().feed = calendarResponse;
.then((calendarResponse: Calendar) => calendarResponse.modules); return calendarResponse.modules;
});
} else { } else {
return []; return [];
} }

View File

@ -21,16 +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, watch} 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 {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,15 +78,15 @@ const selectedRoom = computed(() => props.room);
*/ */
function transformData(data: RoomOccupancyList) { function transformData(data: RoomOccupancyList) {
return 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,
})); }));
} }
const { data: occupancy } = useQuery({ const { data: occupancy } = useQuery({

View File

@ -95,8 +95,13 @@
"error": "Error", "error": "Error",
"successDetail": "calendar successfully deleted", "successDetail": "calendar successfully deleted",
"errorDetail": "calendar could not be deleted", "errorDetail": "calendar could not be deleted",
"successDetailLoad": "calendar successfully loaded" "successDetailLoad": "calendar successfully loaded",
} "info": "info"
},
"calendarContainsNoModules": "calendar contains no modules",
"noCalendarInOfflineMode": "calendar isn't available in offline mode",
"calendarLoadedFromOfflineMode": "calendar loaded from offline cache",
"calendarLoadedFromServer": "calendar loaded from web"
}, },
"additionalModules": { "additionalModules": {
"subTitle": "select additional modules that are not listed in the regular semester for your course", "subTitle": "select additional modules that are not listed in the regular semester for your course",

View File

@ -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, Document} from "bson"; import { Binary, Document } from "bson";
import { AnonymizedOccupancy } from "./event"; import { AnonymizedOccupancy } from "./event";
import { import {
Duration, Duration,
@ -218,31 +218,38 @@ export class RoomOccupancyList {
// 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++) {
const 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; const calculateOccupancyBitIndex = (byte_i: number, bit_i: number) =>
byte_i * 8 + bit_i;
if(firstOccupancyBit === null){ if (firstOccupancyBit === null) {
if (isOccupied) { if (isOccupied) {
firstOccupancyBit = calculateOccupancyBitIndex(byte_i, bit_i); firstOccupancyBit = calculateOccupancyBitIndex(byte_i, bit_i);
} }
} else { } else {
if (!isOccupied) { if (!isOccupied) {
const startTime = addMinutes(start, firstOccupancyBit * granularity); const startTime = addMinutes(
const endTime = addMinutes(start, calculateOccupancyBitIndex(byte_i, bit_i) * granularity); start,
firstOccupancyBit * granularity,
);
const endTime = addMinutes(
start,
calculateOccupancyBitIndex(byte_i, bit_i) * granularity,
);
// add event between start and end of a block of boolean true values // add event between start and end of a block of boolean true values
occupancyList.push( occupancyList.push(
new AnonymizedOccupancy( new AnonymizedOccupancy(
startTime.toISOString(), startTime.toISOString(),
endTime.toISOString(), endTime.toISOString(),
room, room,
false, false,
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) {
const startTime = addMinutes(start, firstOccupancyBit * granularity); const startTime = addMinutes(start, firstOccupancyBit * granularity);
@ -315,7 +322,8 @@ export class RoomOccupancyList {
json.granularity, json.granularity,
json.blocks, json.blocks,
json.rooms.map( json.rooms.map(
(room: { name: string, occupancy: Binary }) => new RoomOccupancy(room.name, room.occupancy), (room: { name: string; occupancy: Binary }) =>
new RoomOccupancy(room.name, room.occupancy),
), ),
); );
} }

View File

@ -15,10 +15,20 @@
//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 { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core";
import { Calendar } from "../model/calendar";
const tokenStore = defineStore("tokenStore", { const tokenStore = defineStore("tokenStore", {
state: () => ({ state: () => ({
token: "", token: "",
feed: useLocalStorage("feed", {
collectionId: "",
collectionName: "",
created: "",
id: "",
modules: [],
updated: "",
} as Calendar),
}), }),
persist: true, persist: true,
actions: { actions: {

View File

@ -50,22 +50,67 @@ function loadCalendar(): void {
moduleStore().removeAllModules(); moduleStore().removeAllModules();
tokenStore().setToken(token.value); tokenStore().setToken(token.value);
getCalender(token.value).then((data: Module[]) => { if (navigator.onLine) {
if (data.length > 0) { getCalender(token.value).then((data: Module[]) => {
data.forEach((module) => { if (data.length > 0) {
moduleStore().addModule(module); data.forEach((module) => {
}); moduleStore().addModule(module);
modules.value = moduleStore().modules; });
router.push("/edit-calendar"); modules.value = moduleStore().modules;
toast.add({
severity: "success",
summary: t("editCalendarView.toast.success"),
detail: t("editCalendarView.calendarLoadedFromServer"),
life: 3000,
});
router.push("/edit-calendar");
} else {
toast.add({
severity: "error",
summary: t("editCalendarView.toast.error"),
detail: t("editCalendarView.noCalendarFound"),
life: 3000,
});
}
});
} else {
if(tokenStore().feed.id === token.value) {
// get data from tokenStore feed if offline
const offlineModules = tokenStore().feed.modules;
if (offlineModules.length > 0) {
offlineModules.forEach((module) => {
moduleStore().addModule(module);
});
toast.add({
severity: "info",
summary: t("editCalendarView.toast.info"),
detail: t("editCalendarView.calendarLoadedFromOfflineMode"),
life: 3000,
});
} else {
toast.add({
severity: "info",
summary: t("editCalendarView.info"),
detail: t("editCalendarView.calendarContainsNoModules"),
life: 3000,
});
}
} else { } else {
toast.add({ toast.add({
severity: "error", severity: "error",
summary: t("editCalendarView.error"), summary: t("editCalendarView.toast.error"),
detail: t("editCalendarView.noCalendarFound"), detail: t("editCalendarView.noCalendarInOfflineMode"),
life: 3000, life: 3000,
}); });
return;
}
modules.value = moduleStore().modules;
router.push("/edit-calendar");
} }
});
} }
</script> </script>

View File

@ -12,6 +12,7 @@ const { t } = useI18n({ useScope: "global" });
const toast = useToast(); const toast = useToast();
const token = ref(tokenStore().token || ("" as string)); const token = ref(tokenStore().token || ("" as string));
const calendarViewerRef = ref<InstanceType<typeof CalendarViewer>>();
// parse token from query parameter // parse token from query parameter
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@ -22,8 +23,6 @@ if (tokenFromUrl) {
loadCalendar(); loadCalendar();
} }
const calendarViewerRef = ref<InstanceType<typeof CalendarViewer>>();
function loadCalendar() { function loadCalendar() {
try { try {
token.value = extractToken(token.value); token.value = extractToken(token.value);

View File

@ -96,6 +96,18 @@ export default defineConfig({
}, },
}, },
}, },
{
// Add the runtime caching strategy for /api/modules
urlPattern: ({url}) => url.pathname.startsWith('/api/modules'),
method: 'GET',
handler: 'NetworkFirst',
options: {
cacheName: 'modules-cache',
expiration: {
maxAgeSeconds: 24 * 60 * 60, // 1 day
},
},
},
{ {
urlPattern: /^https?.*/, urlPattern: /^https?.*/,
handler: "NetworkFirst", handler: "NetworkFirst",