Files
Timetable-V2/src/store.js
minie4 21b2e68198 Add profile management
- Save class filter, timetable and timetable groups in profiles
- Easily switch between profiles
- Rename profiles
- Export/Import/Duplicate profiles
2023-08-26 21:54:02 +02:00

270 lines
7.9 KiB
JavaScript

import { ref, watch, computed } from "vue";
import { getNextAndPrevDay, setUTCMidnight } from "@/util";
import i18n from "@/i18n";
/* Router */
export const shouldLogin = ref(false);
export const loading = ref(false);
export const loadingProgress = ref(0);
export const loadingFailed = ref(false);
/* Preferences */
export const profiles = ref(
JSON.parse(localStorage.getItem("profiles")) || [
{
id: 0,
name: "Default Profile",
classFilter: "none",
timetableId: "none",
timetableGroups: [],
},
],
);
export const activeProfile = computed(() => {
return (
profiles.value.find((e) => e.id == activeProfileId.value) ||
profiles.value[0]
);
});
export const activeProfileId = ref(
localStorage.getItem("activeProfile") || profiles.value[0].id,
);
watch(
() => activeProfile.value.classFilter,
() => {
fetchData(getNextAndPrevDay(selectedDate.value), false);
},
);
export const localTimetables = ref(
JSON.parse(localStorage.getItem("timetables")) || [],
);
export const theme = ref(localStorage.getItem("theme") || "auto");
watch(
profiles,
(newValue) => {
localStorage.setItem("profiles", JSON.stringify(newValue));
},
{ deep: true },
);
watch(activeProfileId, (newValue) => {
localStorage.setItem("activeProfile", newValue);
});
watch(
localTimetables,
(newValue) => {
localStorage.setItem("timetables", JSON.stringify(newValue));
},
{ deep: true },
);
watch(theme, (newValue) => {
localStorage.setItem("theme", newValue);
});
/* Date selector */
export const selectedDate = ref(setUTCMidnight(new Date()));
export const selectedDay = computed(() => selectedDate.value.getDay() - 1);
// Jump to next Monday if it is weekend
if (selectedDay.value == 5)
selectedDate.value = new Date(selectedDate.value.getTime() + 86400000 * 2);
if (selectedDay.value == -1)
selectedDate.value = new Date(selectedDate.value.getTime() + 86400000);
// Set this to a positive or negative integer
// to change selectedDate by this number
export const changeDay = ref(0);
// Set this to jump to a specific date
export const changeDate = ref(new Date());
/* Data store */
export const timetable = computed(() => {
const localTimetable = localTimetables.value.find(
(e) => e.id == activeProfile.value.timetableId,
);
const remoteTimetable = timetables.value.find(
(e) => e.id == activeProfile.value.timetableId,
);
return (
localTimetable || remoteTimetable || { trusted: true, source: "", data: [] }
);
});
export const sessionInfo = ref({});
export const timetables = ref([]);
export const times = ref([]);
export const substitutions = ref({});
export const history = ref({});
export const classList = ref([]);
/* API functions */
// Set the `VITE_API_ENDPOINT` env variable when
// building the frontend to use an external api server
export const baseUrl = import.meta.env.VITE_API_ENDPOINT || "/api";
export async function fetchData(days, partial) {
if (!timetable.value.data || Object.keys(classList.value).length == 0) {
partial = false;
}
const steps = 2 * days.length + (partial ? 0 : 2);
let step = 1;
loadingFailed.value = false;
loadingProgress.value = 0.1;
loading.value = true;
// Check if the API server is reachable
// and the user is authenticated
if (!(await fetchSessionInfo())) {
loadingFailed.value = true;
loadingProgress.value = 1;
return;
}
loadingProgress.value = step++ / steps;
if (!partial) {
await fetchClassList();
loadingProgress.value = step++ / steps;
await fetchTimetables();
loadingProgress.value = step++ / steps;
}
for (const day of days) {
await fetchSubstitutions(day);
loadingProgress.value = step++ / steps;
await fetchHistory(day);
loadingProgress.value = step++ / steps;
}
loadingProgress.value = 1;
loading.value = false;
}
// Load new data if date changes
watch(selectedDate, () =>
fetchData(getNextAndPrevDay(selectedDate.value), true),
);
export async function fetchSessionInfo() {
try {
const checkResponse = await fetch(`${baseUrl}/info`);
if (checkResponse.status == 401) {
shouldLogin.value = true;
return false;
} else if (checkResponse.status != 200) {
console.log("Other error while fetching data: " + checkResponse.status);
return false;
} else {
sessionInfo.value = await checkResponse.json();
}
} catch {
console.log("Error while fetching data: No internet connection!");
return false;
}
return true;
}
export async function fetchClassList() {
const classListResponse = await fetch(`${baseUrl}/classes`);
const classListData = await classListResponse.json();
classList.value = classListData;
}
export async function fetchTimetables() {
const timetableResponse = await fetch(
`${baseUrl}/timetable?class=${activeProfile.value.classFilter}`,
);
const timetableData = await timetableResponse.json();
if (timetableData.error) {
console.warn("API Error: " + timetableData.error);
timetables.value = [];
} else {
timetables.value = timetableData.timetables;
times.value = timetableData.times;
}
}
export async function fetchSubstitutions(day) {
const requestDate = `?date=${day}`;
const substitutionResponse = await fetch(
activeProfile.value.classFilter == "none"
? `${baseUrl}/substitutions${requestDate}`
: `${baseUrl}/substitutions${requestDate}&class=${activeProfile.value.classFilter}`,
);
const substitutionData = await substitutionResponse.json();
substitutions.value[day] = substitutionData;
}
export async function fetchHistory(day) {
const requestDate = `?date=${day}`;
const historyResponse = await fetch(
activeProfile.value.classFilter == "none"
? `${baseUrl}/history${requestDate}`
: `${baseUrl}/history${requestDate}&class=${activeProfile.value.classFilter}`,
);
const historyData = await historyResponse.json();
if (historyData.error) console.warn("API Error: " + historyData.error);
else history.value[day] = historyData;
}
/* Preprocess the timetable data */
export const parsedTimetable = computed(() => {
// Check if timetable data exists
if (!timetable.value.data) return [];
return timetable.value.data.map((day) => {
const parsedDay = [];
for (const lesson of day) {
let usedLesson = { ...lesson };
// Check if lesson has multiple options
// (timetable groups)
if (Array.isArray(lesson) && lesson.length > 1) {
let matchingLesson = lesson.find((e) =>
activeProfile.value.timetableGroups.includes(e.group),
);
// If no valid timetable group is configured
// add a dummy lesson showing a notice
if (!matchingLesson) {
matchingLesson = {
subject: lesson.map((e) => e.subject).join(" / "),
teacher: i18n.global.t("timetable.configureTimetableGroup"),
length: lesson[0].length || 1,
};
}
usedLesson = { ...matchingLesson };
} else if (Array.isArray(lesson)) {
usedLesson = { ...lesson[0] };
}
// Duplicate the lesson if its length is > 1 for it
// to show up multiple times in the timetable view
const lessonLength = usedLesson.length || 1;
delete usedLesson.length;
for (var i = 0; i < lessonLength; i++) parsedDay.push(usedLesson);
}
return parsedDay;
});
});
export const possibleTimetableGroups = computed(() => {
const foundTimetableGroups = [];
if (!timetable.value.data) return [];
// Make a list of all possible timetable groups
// found in the current timetable
for (const day of timetable.value.data) {
for (const lesson of day) {
if (Array.isArray(lesson) && lesson.length > 1) {
for (const group of lesson) {
if (group.group && !foundTimetableGroups.includes(group.group)) {
foundTimetableGroups.push(group.group);
}
}
}
}
}
return foundTimetableGroups;
});
// Initially fetch data for the
// current, next and previous day
fetchData(getNextAndPrevDay(selectedDate.value), false);