| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620 |
- import { defineStore } from "pinia";
- import { useToast } from "vue-toastification";
- import {
- CalendarEvent,
- TaughtClass,
- Module,
- PersonalEvent,
- PlanningDump,
- PlanEntry,
- } from "../types";
- import { useClassesStore } from "./classes";
- import { useModulesStore } from "./modules";
- import { useClassVersionStore } from "./ClassVersion";
- import { useStateStore } from "./state";
- import {
- semesterVersionStringC,
- waitTimeAfterOldPlanReminder,
- waitTimeAfterUpgradeRequestModal,
- } from "../helpers";
- const MAX_MODULE_COUNT = 40;
- const toast = useToast();
- function otherPlanModificationToast() {
- toast.error("Du kannst den Plan eines anderen nicht bearbeiten");
- }
- function reloadChosenModules() {
- const planningStore = usePlanningStore();
- planningStore.loadChosen();
- }
- const DEFAULT_PLAN_NAME = "default";
- export const BLOCKCOURSE_DAY_IDX = 6;
- export const usePlanningStore = defineStore("planning", {
- state: () => {
- return {
- usingModulesFromURL: false,
- chosen: [] as TaughtClass[],
- personalEvents: [] as PersonalEvent[],
- hourStart: 8,
- hourEnd: 22,
- hourDivisions: 2,
- cellHeight: 24,
- get currentPlanName(): string {
- if (this.__currentPlanName != null) return this.__currentPlanName;
- let name = localStorage["lastSelectedCalendarSet"];
- if (!name) {
- name = DEFAULT_PLAN_NAME;
- localStorage["lastSelectedCalendarSet"] = DEFAULT_PLAN_NAME;
- }
- this.__currentPlanName = name;
- return name;
- },
- set currentPlanName(newName: string) {
- this.__currentPlanName = newName;
- localStorage["lastSelectedCalendarSet"] = newName;
- reloadChosenModules();
- },
- __currentPlanName: null as string | null,
- allPlans: [] as PlanEntry[],
- saveChanges: true,
- lastPlanRequestTimestamp: undefined as undefined | number,
- oldPlanReminderTimestamp: undefined as undefined | number,
- doNotRequestVersionUpgrade: false,
- };
- },
- getters: {
- eventsByDay(): CalendarEvent[][] {
- // Prefill with 7, because school is from Mo-Sa (6) + 1 day for the block courses
- const events = new Array(7).fill(null).map(() => []) as CalendarEvent[][];
- const allPlacable = [...this.chosen, ...this.personalEvents];
- // Sort by longest duration, we want to have them leftmost on the calendar view
- const ch = allPlacable
- .filter((el) => el != null)
- .sort((a, b) => b.to - b.from - (a.to - a.from));
- ch.forEach((m, idx) => {
- const topOffset =
- (m.from / 3600 - this.hourStart) *
- this.cellHeight *
- this.hourDivisions;
- const length =
- ((m.to - m.from) / 3600) * this.cellHeight * this.hourDivisions;
- let overlapCount = 0;
- let overlapIdx = 0;
- ch.forEach((om, oidx) => {
- if (m.weekday != om.weekday || idx == oidx) return;
- if (om.from >= m.from && om.from <= m.to) {
- overlapCount++;
- if (idx > oidx) overlapIdx++;
- } else if (m.from >= om.from && m.from <= om.to) {
- overlapCount++;
- if (idx > oidx) overlapIdx++;
- }
- });
- const width = 100 / (overlapCount + 1) - overlapIdx;
- const leftOffset = overlapIdx * width + overlapIdx;
- let tc = null as null | TaughtClass;
- let pe = null as null | PersonalEvent;
- if ((m as TaughtClass).teachers) tc = m as TaughtClass;
- else pe = m as PersonalEvent;
- events[m.weekday ?? BLOCKCOURSE_DAY_IDX].push({
- taughtClass: tc,
- personalEvent: pe,
- topOffset: topOffset,
- leftOffset: leftOffset,
- length: length,
- width: width,
- });
- });
- return events;
- },
- sharedDisplayName(): string | null {
- if (!this.usingModulesFromURL) return null;
- const pn = this.currentPlanName;
- if (pn.length == 0 || pn == DEFAULT_PLAN_NAME) return null;
- return pn;
- },
- totalECTSCount(): { ects: number; unsureModules: string[] } {
- let ects = 0;
- const unsureModules = [] as string[];
- const countedModules = [] as string[];
- this.chosen.forEach((c) => {
- const module = c.module;
- if (module == null || module.ects == null) {
- unsureModules.push(c.name);
- return;
- }
- const moduleName = module.name;
- if (countedModules.indexOf(moduleName) == -1) {
- ects += module.ects;
- countedModules.push(moduleName);
- }
- });
- this.personalEvents.forEach((pe) => (ects += pe.ects ?? 0));
- return { ects, unsureModules };
- },
- modulesWithMSP(): Module[] {
- const modulesStore = useModulesStore();
- return this.chosen
- .map((c) => modulesStore.fromModuleShort(c.name))
- .filter((m) => m?.hasMSP === true) as Module[];
- },
- ectsInBBVTClasses(): { bb: number; vt: number } {
- const res = { bb: 0, vt: 0 };
- const seen = { bb: [], vt: [] } as { bb: string[]; vt: string[] };
- this.chosen.forEach((c) => {
- const moduleName = c.name;
- const ects = c.module?.ects ?? 0;
- if (c.isBB) {
- const found = seen.bb.indexOf(moduleName) >= 0;
- if (found) return;
- seen.bb.push(moduleName);
- res.bb += ects;
- } else {
- const found = seen.vt.indexOf(moduleName) >= 0;
- if (found) return;
- seen.vt.push(moduleName);
- res.vt += ects;
- }
- });
- return res;
- },
- ectsInEngClasses(): number {
- const seen = [] as string[];
- return this.chosen
- .filter((c) => {
- const moduleName = c.name;
- const found = seen.indexOf(moduleName) >= 0;
- if (!found) seen.push(moduleName);
- return found;
- })
- .reduce((sum, c) => sum + (c.isEnglish ? c.module?.ects ?? 0 : 0), 0);
- },
- ectsInContextClasses(): number {
- const seen = [] as string[];
- return this.chosen
- .filter((c) => {
- const moduleName = c.name;
- const found = seen.indexOf(moduleName) >= 0;
- if (!found) seen.push(moduleName);
- return found;
- })
- .reduce((sum, c) => sum + (c.isContext ? c.module?.ects ?? 0 : 0), 0);
- },
- ectsWithMSP(): number {
- const seen = [] as string[];
- return this.chosen
- .filter((c) => {
- const moduleName = c.name;
- const found = seen.indexOf(moduleName) >= 0;
- if (!found) seen.push(moduleName);
- return found;
- })
- .reduce(
- (sum, c) => sum + (c.module?.hasMSP ? c.module?.ects ?? 0 : 0),
- 0,
- );
- },
- },
- actions: {
- /**
- * Updates the current plan from using the old ids in the format of
- * `<className>-<name>-<weekday>-<room1<_room2, ...>>` to
- * `<className>-<name>-<weekday>-<from_time>-<to_time>`.
- *
- * This function and it's calls can be safely (and should be) removed by the end of
- * 2023! In addition to this code, some further code in `loadChosen` can also be
- * removed (marked).
- */
- checkAndUpdateChosenIDs(modules: string[]): string[] {
- const newIdRE = /^.*-.*-\d-\d+-\d+$/;
- const classesStore = useClassesStore();
- const currentClasses = [
- ...(classesStore.currentData?.taughtClasses ?? []),
- ...(classesStore.currentData?.blockClasses ?? []),
- ];
- return modules.map((el) => {
- if (newIdRE.test(el)) return el;
- const split = el.split("-", 4);
- const cls = split[0];
- const name = split[1];
- const weekday = split[2];
- const rooms = split[3];
- // find the new id
- const found = currentClasses.filter(
- (c) =>
- c.class == cls &&
- c.name == name &&
- `${c.weekday}` == weekday &&
- c.rooms.join("_") == rooms,
- );
- if (found.length == 0) {
- console.warn("Failed to find a replacement ID!");
- return el;
- }
- return found[0].id;
- });
- },
- loadFromStorage() {
- // Saved plans
- this.allPlans = [] as PlanEntry[];
- for (const key in localStorage) {
- if (key.startsWith("moduleset-")) {
- const name = key.replace("moduleset-", "");
- if (name.length > 0) {
- const data = JSON.parse(
- localStorage[`moduleset-${name}`],
- ) as PlanningDump;
- const version = data.version;
- this.allPlans.push({ name, planVersion: version });
- }
- }
- }
- if (this.allPlans.length == 0) {
- this.createPlan(DEFAULT_PLAN_NAME);
- this.currentPlanName = DEFAULT_PLAN_NAME;
- }
- // Last selected plan
- this.loadChosen();
- },
- createPlan(name: string): boolean {
- if (this.allPlans.some((el) => el.name == name)) {
- toast.warning(
- `Der Plan '${name}' existiert bereits. Wähle einen anderen Namen.`,
- );
- return false;
- }
- const planVersion =
- semesterVersionStringC(useClassVersionStore().latestOverall) ?? "";
- this.allPlans.push({ name, planVersion });
- this.saveChosen(name, true);
- return true;
- },
- add(m: TaughtClass) {
- if (this.chosen.includes(m) || this.chosen.length > MAX_MODULE_COUNT)
- return;
- if (this.usingModulesFromURL) {
- otherPlanModificationToast();
- return;
- }
- this.chosen.push(m);
- this.saveChosen();
- },
- remove(m: TaughtClass) {
- if (this.usingModulesFromURL) {
- otherPlanModificationToast();
- return;
- }
- const idx = this.chosen.indexOf(m);
- if (idx == -1) return;
- this.chosen.splice(idx, 1);
- this.saveChosen();
- },
- removeIdx(idx: number) {
- if (this.usingModulesFromURL) {
- otherPlanModificationToast();
- return;
- }
- this.chosen.splice(idx, 1);
- this.saveChosen();
- },
- isModuleChosen(m: TaughtClass) {
- return this.chosen.includes(m);
- },
- saveChosen(name: string | null = null, forceEmpty = false) {
- if (!this.saveChanges) return;
- if (this.usingModulesFromURL) {
- console.log("Not saving the current plan, as this is a shared one!");
- return;
- }
- if (name == null) name = this.currentPlanName;
- localStorage[`moduleset-${name}`] = this.stateToJSON(forceEmpty);
- },
- loadChosen(name: string | null = null, useSavedVersion = true): boolean {
- if (name == null) name = this.currentPlanName;
- const params = window.location.href
- .substr(window.location.href.indexOf("?"))
- .replace("+", "%2B");
- const param = new URLSearchParams(params);
- let strData;
- const stateParam = param.get("state");
- if (stateParam) {
- const state = stateParam.replace("%2B", "+");
- strData = decodeURIComponent(escape(atob(state)));
- this.usingModulesFromURL = true;
- } else {
- strData = localStorage[`moduleset-${name}`];
- if (strData == null) return false;
- this.usingModulesFromURL = false;
- }
- const classVersionStore = useClassVersionStore();
- const modulesStore = useClassesStore();
- const data = JSON.parse(strData) as PlanningDump;
- classVersionStore.useFromPDFString(
- useSavedVersion
- ? data.version
- : useClassVersionStore().semVer ?? data.version,
- );
- modulesStore.fetchData();
- if (!modulesStore.currentData?.loaded) return false;
- this.doNotRequestVersionUpgrade =
- data.doNotRequestVersionUpgrade ?? false;
- let shouldSave = false;
- const cTimestamp = new Date().getTime();
- if (!this.doNotRequestVersionUpgrade) {
- this.lastPlanRequestTimestamp = data.lastPlanRequestTimestamp;
- if (
- !classVersionStore.isLatestSemesterVersion &&
- (this.lastPlanRequestTimestamp == undefined ||
- cTimestamp - this.lastPlanRequestTimestamp >
- waitTimeAfterUpgradeRequestModal)
- ) {
- const stateStore = useStateStore();
- stateStore.showingClassUpgradeModal = true;
- this.lastPlanRequestTimestamp = cTimestamp;
- shouldSave = true;
- }
- }
- this.oldPlanReminderTimestamp = data.oldPlanReminderTimestamp;
- if (
- !classVersionStore.isLatestSemester &&
- (this.oldPlanReminderTimestamp == undefined ||
- cTimestamp - this.oldPlanReminderTimestamp >
- waitTimeAfterOldPlanReminder)
- ) {
- const stateStore = useStateStore();
- stateStore.showingOldPlanReminderModal = true;
- this.oldPlanReminderTimestamp = cTimestamp;
- shouldSave = true;
- }
- /**
- * The following code can be removed at the end of 2023!
- */
- const updatedIDs = this.checkAndUpdateChosenIDs(data.modules);
- this.chosen = updatedIDs
- .map((el) => modulesStore.getById(el))
- .filter((el) => el) as TaughtClass[];
- /**
- * End code that can be removed at the end of 2023!
- */
- if (this.usingModulesFromURL) this.__currentPlanName = data.name ?? "";
- if (data.personalEvents) this.personalEvents = data.personalEvents;
- else this.personalEvents = [];
- /**
- * The following code can be removed at the end of 2023!
- */
- shouldSave ||= !(
- updatedIDs.length == data.modules.length &&
- updatedIDs.every(function (element, index) {
- return element === data.modules[index];
- })
- );
- /**
- * End code that can be removed at the end of 2023!
- */
- if (shouldSave) {
- console.log("Saving due to ...");
- this.saveChosen(name);
- }
- return true;
- },
- copySharedTo(name: string) {
- const params = window.location.href
- .substr(window.location.href.indexOf("?"))
- .replace("+", "%2B");
- const param = new URLSearchParams(params);
- const stateParam = param.get("state");
- if (!stateParam) return;
- const state = stateParam.replace("%2B", "+");
- const strData = decodeURIComponent(escape(atob(state)));
- localStorage[`moduleset-${name}`] = strData;
- window.history.pushState(null, "", window.location.href.split("?")[0]);
- this.currentPlanName = name;
- this.loadChosen(name);
- },
- deleteChosen(name: string | null = null): boolean {
- if (name == null) name = this.currentPlanName;
- const pIdx = this.allPlans.findIndex((el) => el.name == name);
- if (pIdx == -1) return false;
- this.allPlans.splice(pIdx, 1);
- localStorage.removeItem(`moduleset-${name}`);
- if (this.allPlans.length == 0) {
- const planVersion =
- semesterVersionStringC(useClassVersionStore().latestOverall) ?? "";
- this.allPlans.push({ name: DEFAULT_PLAN_NAME, planVersion });
- this.currentPlanName = DEFAULT_PLAN_NAME;
- this.chosen = [];
- this.saveChosen();
- this.loadChosen();
- return true;
- }
- if (this.currentPlanName == name)
- this.currentPlanName = this.allPlans[0].name;
- this.loadChosen();
- return true;
- },
- emptyPlan() {
- this.chosen = [];
- this.saveChosen();
- },
- copyPlan(
- copyName: string | null = null,
- copyFrom: string | null = null,
- ): string | null {
- if (copyFrom == null) copyFrom = this.currentPlanName;
- if (copyName == null) {
- let counter = 2;
- copyName = `Kopie von ${copyFrom}`;
- while (this.allPlans.findIndex((el) => el.name == copyName) >= 0) {
- copyName = `Kopie ${counter} von ${copyFrom}`;
- counter++;
- }
- } else {
- if (this.allPlans.findIndex((el) => el.name == copyName) >= 0) {
- // Key already exists
- toast.error(`Der Name '${copyName}' ist bereits vergeben!`);
- return null;
- }
- }
- if (localStorage[`moduleset-${copyFrom}`] == null) {
- // copyFrom name does not exist!
- toast.error(
- "Die zu kopierende Modulauswahl wurde nicht im Speicher gefunden!",
- );
- return null;
- }
- localStorage[`moduleset-${copyName}`] =
- localStorage[`moduleset-${copyFrom}`];
- const copyData = JSON.parse(
- localStorage[`moduleset-${copyName}`],
- ) as PlanningDump;
- this.allPlans.push({
- name: copyName,
- planVersion: copyData.version,
- });
- this.currentPlanName = copyName;
- return copyName;
- },
- renamePlan(newName: string, current: string | null = null): boolean {
- if (current == null) current = this.currentPlanName;
- if (current == newName) return true;
- if (localStorage[`moduleset-${newName}`] != null) {
- // Key already exists
- toast.error(`Der Name '${newName}' ist bereits vergeben!`);
- return false;
- }
- if (localStorage[`moduleset-${current}`] == null) {
- // Current name does not exist!
- toast.error(
- "Die zu umzubenennende Modulauswahl wurde nicht im Speicher gefunden!",
- );
- return false;
- }
- localStorage[`moduleset-${newName}`] =
- localStorage[`moduleset-${current}`];
- localStorage.removeItem(`moduleset-${current}`);
- this.currentPlanName = newName;
- const pIdx = this.allPlans.findIndex((el) => el.name == current);
- if (pIdx >= 0) {
- const planVersion = useClassVersionStore().semVer ?? "";
- this.allPlans.splice(pIdx, 1, { name: newName, planVersion });
- }
- return true;
- },
- stateToJSON(forceEmpty = false, includeName = false): string {
- let modules = [] as string[];
- if (!forceEmpty) modules = this.chosen.map((m) => m.id);
- const data = {
- version: useClassVersionStore().semVer,
- modules: modules,
- personalEvents: this.personalEvents,
- lastPlanRequestTimestamp: this.lastPlanRequestTimestamp,
- oldPlanReminderTimestamp: this.oldPlanReminderTimestamp,
- doNotRequestVersionUpgrade: this.doNotRequestVersionUpgrade,
- } as PlanningDump;
- if (includeName) data.name = this.currentPlanName;
- return JSON.stringify(data);
- },
- getShareURL(): string {
- const state = this.stateToJSON(false, true);
- const encodedState = btoa(unescape(encodeURIComponent(state)));
- return `${window.location.href}?state=${encodedState}`;
- },
- removePersonalEvent(event: PersonalEvent | null): boolean {
- if (event == null) return false;
- const idx = this.personalEvents.indexOf(event);
- if (idx == -1) return false;
- this.personalEvents.splice(idx, 1);
- this.saveChosen();
- return true;
- },
- addPersonalEvent(event: PersonalEvent) {
- this.personalEvents.push(event);
- this.saveChosen();
- },
- updatePersonalEvent(
- oldEvent: PersonalEvent,
- newEvent: PersonalEvent,
- ): boolean {
- const idx = this.personalEvents.indexOf(oldEvent);
- if (idx == -1) return false;
- this.personalEvents.splice(idx, 1, newEvent);
- this.saveChosen();
- return true;
- },
- noLongerRequestVersionUpgrade() {
- this.doNotRequestVersionUpgrade = true;
- this.saveChosen();
- },
- },
- });
|