import { defineStore } from "pinia"; import { MinFullNameSearchLength, bbClassRe, contextClassRe, dayMap, englishClassRe, firstPhaseClassRe, toTime, } from "../helpers"; import { TaughtClass, ClassSelectorColumn, Ordering, FilterRule, ModuleLanguages, ClassesVersion, SemesterConfiguration, } from "../types"; import { useClassVersionStore } from "./ClassVersion"; import { useModulesStore } from "./modules"; import { usePlanningStore } from "./planning"; import { useStateStore } from "./state"; import { useStudenthubStore } from "./studenthub"; // eslint-disable-next-line type SortingFunction = (o1: any, o2: any) => number; export const useClassesStore = defineStore("classes", { state: () => { const stateStore = useStateStore(); const cvStore = useClassVersionStore(); return { data: {} as Record, filterRules: [ { column: ClassSelectorColumn.Degree, filterData: { degree: `${stateStore.lastSelectedDegreeProgram}` }, pk: Math.floor(Math.random() * 1e20), enabled: true, }, { column: ClassSelectorColumn.Module, filterData: { term: "" }, pk: Math.floor(Math.random() * 1e20), enabled: true, }, ] as FilterRule[], sortBy: {} as Record, cvStore, }; }, getters: { allClasses(): TaughtClass[] { const cd = this.currentData; if (cd == null) return []; return [...cd.taughtClasses, ...cd.blockClasses]; }, classesFilteredByCompletion(): TaughtClass[] { const stateStore = useStateStore(); if (!stateStore.hideCompletedClasses) return this.allClasses; const studenthubStore = useStudenthubStore(); return this.allClasses.filter((c) => { const m = c.module; if (m?.module_id == null) return true; return !studenthubStore.hasCompletedModule(m.module_id); }); }, getAllSortedClasses(): TaughtClass[] { const keys = Object.keys(this.sortBy); const numberOfProperties = keys.length; let modules = [...this.classesFilteredByCompletion]; if (numberOfProperties > 0) { modules = modules.sort((obj1, obj2) => { let i = 0; let result = 0; while (result === 0 && i < numberOfProperties) { result = this.sortBy[keys[i]](obj1, obj2); i++; } return result; }); } return modules; }, filteredModules(): TaughtClass[] { let classes = this.getAllSortedClasses; const transformedRules = {} as Record; this.filterRules.forEach((rule) => { if (!rule.enabled) return; if (rule.column in transformedRules) transformedRules[rule.column].push(rule); else transformedRules[rule.column] = [rule]; }); Object.keys(transformedRules).forEach((column) => { const foundClasses = [] as TaughtClass[]; let didFilter = false; if (column == ClassSelectorColumn.Time) { transformedRules[column].forEach((rule) => foundClasses.push(...filterTimeColumn(classes, rule)), ); classes = [...new Set(foundClasses)]; return; } if (column == ClassSelectorColumn.Degree) { let shouldFilter = true; transformedRules[column].forEach((rule) => { const degree = rule.filterData?.degree ?? ""; if (degree.length == 0) { shouldFilter = false; return; } didFilter = true; foundClasses.push( ...classes.filter((c) => c.degree_prg == rule.filterData.degree), ); }); if (shouldFilter && didFilter) classes = [...new Set(foundClasses)]; return; } switch (column) { case ClassSelectorColumn.Module: transformedRules[column].forEach((rule) => { const term = rule.filterData?.term?.toLowerCase() ?? ""; const checkName = term.length >= MinFullNameSearchLength; if (term.length == 0) return; didFilter = true; foundClasses.push( ...classes.filter( (c) => c.name.toLowerCase().includes(term) || (checkName && c.module?.name.toLocaleLowerCase().includes(term)), ), ); }); break; case ClassSelectorColumn.Room: transformedRules[column].forEach((rule) => { const term = rule.filterData?.term?.toLowerCase() ?? ""; if (term.length == 0) return; didFilter = true; const clsss = classes.filter((c) => { if (c.rooms.length == 0) return false; return c.rooms.join(" ").toLowerCase().includes(term); }); foundClasses.push(...clsss); }); break; case ClassSelectorColumn.Class: transformedRules[column].forEach((rule) => { const term = rule.filterData?.term?.toLowerCase() ?? ""; if (term.length == 0) return; didFilter = true; foundClasses.push( ...classes.filter((c) => c.class.toLowerCase().includes(term)), ); }); break; case ClassSelectorColumn.Lecturer: transformedRules[column].forEach((rule) => { const term = rule.filterData?.term?.toLowerCase() ?? ""; if (term.length == 0) return; didFilter = true; foundClasses.push( ...classes.filter((c) => c.teachers.join().toLowerCase().includes(term), ), ); }); break; case ClassSelectorColumn.TeachingType: transformedRules[column].forEach((rule) => { if (rule.filterData.teachingType?.length == 0) return; didFilter = true; foundClasses.push( ...classes.filter( (c) => c.teaching_type == rule.filterData.teachingType, ), ); }); break; default: console.warn(`Filter type ${column} is not supported!`); return; } if (didFilter) classes = [...new Set(foundClasses)]; }); return classes; }, degreePrograms(): string[] { const cd = this.currentData; if (cd == null) return []; return cd.degreePrograms; }, currentData(): ClassesVersion | null { const version = this.cvStore.semVer; if (version == null) return null; if (!(version in this.data)) { console.error(`The version ${version} has not yet been loaded!`); return null; } return this.data[version]; }, }, actions: { fetchData(version: string | null = null): Promise { if (version == null) { version = this.cvStore.semVer; if (version == null) { // console.error("The current semester/version was unexpectedly null!"); return new Promise((res, rej) => rej); } } // Check if the data has already been loaded if (version in this.data) return this.data[version].ready ?? new Promise((res, rej) => rej); const semVer = version; this.data[semVer] = { version: semVer, ready: null, loaded: false, teachers: [], rooms: [], classes: [], days: [], taughtClasses: [], blockClasses: [], degreePrograms: [], config: { blockclass_file: "block.pdf" }, }; const semVerFolder = this.cvStore.semVerFolderFromSemVer(semVer); const semesterFolder = this.cvStore.semesterFolderFromSemVer(semVer); this.data[semVer].ready = new Promise((resolve, reject) => { const fe = fetch(`${semVerFolder}/classes.json`); const bc = fetch(`${semesterFolder}/blockclasses.json`); const sc = fetch(`${semesterFolder}/config.json`); const modulesStore = useModulesStore(); const addModuleGetters = (cls: TaughtClass) => { Object.defineProperty(cls, "module", { get: function () { return ( this._module || (this._module = modulesStore.fromModuleShort(cls.name)) ); }, }); cls.isBB = cls.class.match(bbClassRe) !== null; cls.isFirstPhase = cls.class.match(firstPhaseClassRe) !== null; cls.isEnglish = cls.class.match(englishClassRe) !== null; cls.isContext = cls.class.match(contextClassRe) !== null; cls.languageString = cls.isEnglish ? ModuleLanguages.EN : ModuleLanguages.DE; cls.executionTime = cls.weekday !== null ? `${dayMap[cls.weekday]}, ${toTime(cls.from)}-${toTime(cls.to)}` : "Blockmodul"; }; /** * Process the block modules */ const bmReady = new Promise((resolveBM) => { Promise.all([bc, modulesStore.ready]) .then((response) => response[0].json()) .then((data: TaughtClass[]) => { data.forEach(addModuleGetters); this.data[semVer].blockClasses = data; resolveBM(null); }) .catch((error) => { console.error(error); this.data[semVer].blockClasses = []; resolveBM(null); // We will resolve and not reject, as it's okay to not have a blockmodules.json on the server }); }); /** * Process the semester config */ const scReady = new Promise((resolveSC) => { Promise.all([sc, modulesStore.ready]) .then((response) => response[0].json()) .then((data: SemesterConfiguration) => { this.data[semVer].config = data; resolveSC(null); }) .catch((error) => { console.error(error); this.data[semVer].config = { blockclass_file: "block.pdf" }; resolveSC(null); // We will resolve and not reject, as it's okay to not have a blockmodules.json on the server }); }); /** * Process the standard classes */ Promise.all([fe, bmReady, scReady, modulesStore.ready]) .then((response) => (response[0] as Response).json()) .then((data: TaughtClass[]) => { data.forEach(addModuleGetters); this.data[semVer].taughtClasses = data; this.updateUniqueDegreePrograms(semVer); this.data[semVer].loaded = true; const planningStore = usePlanningStore(); planningStore.loadFromStorage(); resolve(null); }) .catch((error) => { console.error(error); this.data[semVer].taughtClasses = []; reject(null); }); }); return this.data[version].ready ?? new Promise((res, rej) => rej); }, updateUniqueDegreePrograms(version: string) { this.data[version].degreePrograms = []; this.allClasses.forEach((m) => { if ( m.degree_prg.length > 0 && !this.data[version].degreePrograms.includes(m.degree_prg) ) this.data[version].degreePrograms.push(m.degree_prg); }); this.data[version].degreePrograms.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()), ); }, getById(module_id: string): TaughtClass | null { const matched = this.allClasses.filter((c) => c.id == module_id); if (matched.length == 0) return null; return matched[0]; }, updateOrdering(col: ClassSelectorColumn, dir: Ordering) { const colName = col.valueOf(); if (dir == Ordering.None && col in this.sortBy) { delete this.sortBy[col]; return; } switch (col) { case ClassSelectorColumn.Time: this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => { const ta = 86400 * (a.weekday ?? 0) + a.from; const tb = 86400 * (b.weekday ?? 0) + b.from; return (ta - tb) * dir; }; break; case ClassSelectorColumn.Lecturer: this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => { const ta = a.teachers.join("").toLowerCase(); const tb = b.teachers.join("").toLowerCase(); return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir; }; break; case ClassSelectorColumn.TeachingType: this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => { const ta = a.teaching_type; const tb = b.teaching_type; return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir; }; break; case ClassSelectorColumn.Room: this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => { const ta = a.rooms.join("").toLowerCase(); const tb = b.rooms.join("").toLowerCase(); return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir; }; break; case ClassSelectorColumn.MSP: this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => { const aMSP = a.module?.hasMSP === true; const bMSP = b.module?.hasMSP === true; return (aMSP == bMSP ? 0 : aMSP ? -1 : 1) * dir; }; break; case ClassSelectorColumn.Module: case ClassSelectorColumn.Class: this.sortBy[col] = (a, b) => (a[colName] < b[colName] ? -1 : a[colName] > b[colName] ? 1 : 0) * dir; break; default: console.error("Unhandled sorting!"); break; } }, insertFilter(afterFilter: FilterRule | null) { const emptyFilter = { column: ClassSelectorColumn.Module, filterData: { term: "" }, pk: Math.floor(Math.random() * 1e20), enabled: true, } as FilterRule; if (!afterFilter) { this.filterRules.push(emptyFilter); return; } const idx = this.filterRules.findIndex((r) => r.pk == afterFilter.pk); if (idx == -1) { this.filterRules.push(emptyFilter); return; } this.filterRules.splice(idx + 1, 0, emptyFilter); }, removeFilter(filterRule: FilterRule) { const idx = this.filterRules.findIndex((r) => r.pk == filterRule.pk); if (idx == -1) { console.error("Failed to find the filter rule in the current set!"); return; } this.filterRules.splice(idx, 1); }, classesForModule(short: string): TaughtClass[] { const cd = this.currentData; if (cd == null) return []; return cd.taughtClasses.filter((c) => c.name == short); }, }, }); function filterTimeColumn( modules: TaughtClass[], filterset: FilterRule, ): TaughtClass[] { const timeData = filterset.filterData as Record; const start = timeData.startTime; const end = timeData.endTime; if (start == undefined && end == undefined) return modules; let startingDay = -1; if (start != undefined) { if (start < 0) { const pStart = -1 * start; modules = modules.filter((m) => m.from >= pStart); } else { startingDay = Math.floor(start / 86400); const startingTime = start - startingDay * 86400; modules = modules.filter( (m) => ((m.weekday ?? 0) == startingDay && m.from >= startingTime) || (m.weekday ?? 0) > startingDay, ); } } if (end != undefined) { let aEnd = end; if (aEnd < 0) { if (startingDay >= 0) { const endingTime = aEnd * -1 - startingDay * 86400; modules = modules.filter( (m) => (m.weekday ?? 0) == startingDay && (endingTime == 0 || m.to <= endingTime), ); } else { aEnd *= -1; modules = modules.filter((m) => m.to <= aEnd); } } else { const endingDay = Math.floor(aEnd / 86400); const endingTime = aEnd - endingDay * 86400; modules = modules.filter( (m) => (m.weekday ?? 0) < endingDay || ((m.weekday ?? 0) == endingDay && (endingTime == 0 || m.to <= endingTime)), ); } } return modules; }