classes.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import { defineStore } from "pinia";
  2. import {
  3. MinFullNameSearchLength,
  4. bbClassRe,
  5. contextClassRe,
  6. dayMap,
  7. englishClassRe,
  8. firstPhaseClassRe,
  9. toTime,
  10. } from "../helpers";
  11. import {
  12. TaughtClass,
  13. ClassSelectorColumn,
  14. Ordering,
  15. FilterRule,
  16. ModuleLanguages,
  17. ClassesVersion,
  18. SemesterConfiguration,
  19. } from "../types";
  20. import { useClassVersionStore } from "./ClassVersion";
  21. import { useModulesStore } from "./modules";
  22. import { usePlanningStore } from "./planning";
  23. import { useStateStore } from "./state";
  24. import { useStudenthubStore } from "./studenthub";
  25. // eslint-disable-next-line
  26. type SortingFunction = (o1: any, o2: any) => number;
  27. export const useClassesStore = defineStore("classes", {
  28. state: () => {
  29. const stateStore = useStateStore();
  30. const cvStore = useClassVersionStore();
  31. return {
  32. data: {} as Record<string, ClassesVersion>,
  33. filterRules: [
  34. {
  35. column: ClassSelectorColumn.Degree,
  36. filterData: { degree: `${stateStore.lastSelectedDegreeProgram}` },
  37. pk: Math.floor(Math.random() * 1e20),
  38. enabled: true,
  39. },
  40. {
  41. column: ClassSelectorColumn.Module,
  42. filterData: { term: "" },
  43. pk: Math.floor(Math.random() * 1e20),
  44. enabled: true,
  45. },
  46. ] as FilterRule[],
  47. sortBy: {} as Record<string, SortingFunction>,
  48. cvStore,
  49. };
  50. },
  51. getters: {
  52. allClasses(): TaughtClass[] {
  53. const cd = this.currentData;
  54. if (cd == null) return [];
  55. return [...cd.taughtClasses, ...cd.blockClasses];
  56. },
  57. classesFilteredByCompletion(): TaughtClass[] {
  58. const stateStore = useStateStore();
  59. if (!stateStore.hideCompletedClasses) return this.allClasses;
  60. const studenthubStore = useStudenthubStore();
  61. return this.allClasses.filter((c) => {
  62. const m = c.module;
  63. if (m?.module_id == null) return true;
  64. return !studenthubStore.hasCompletedModule(m.module_id);
  65. });
  66. },
  67. getAllSortedClasses(): TaughtClass[] {
  68. const keys = Object.keys(this.sortBy);
  69. const numberOfProperties = keys.length;
  70. let modules = [...this.classesFilteredByCompletion];
  71. if (numberOfProperties > 0) {
  72. modules = modules.sort((obj1, obj2) => {
  73. let i = 0;
  74. let result = 0;
  75. while (result === 0 && i < numberOfProperties) {
  76. result = this.sortBy[keys[i]](obj1, obj2);
  77. i++;
  78. }
  79. return result;
  80. });
  81. }
  82. return modules;
  83. },
  84. filteredModules(): TaughtClass[] {
  85. let classes = this.getAllSortedClasses;
  86. const transformedRules = {} as Record<ClassSelectorColumn, FilterRule[]>;
  87. this.filterRules.forEach((rule) => {
  88. if (!rule.enabled) return;
  89. if (rule.column in transformedRules)
  90. transformedRules[rule.column].push(rule);
  91. else transformedRules[rule.column] = [rule];
  92. });
  93. Object.keys(transformedRules).forEach((column) => {
  94. const foundClasses = [] as TaughtClass[];
  95. let didFilter = false;
  96. if (column == ClassSelectorColumn.Time) {
  97. transformedRules[column].forEach((rule) =>
  98. foundClasses.push(...filterTimeColumn(classes, rule)),
  99. );
  100. classes = [...new Set(foundClasses)];
  101. return;
  102. }
  103. if (column == ClassSelectorColumn.Degree) {
  104. let shouldFilter = true;
  105. transformedRules[column].forEach((rule) => {
  106. const degree = rule.filterData?.degree ?? "";
  107. if (degree.length == 0) {
  108. shouldFilter = false;
  109. return;
  110. }
  111. didFilter = true;
  112. foundClasses.push(
  113. ...classes.filter((c) => c.degree_prg == rule.filterData.degree),
  114. );
  115. });
  116. if (shouldFilter && didFilter) classes = [...new Set(foundClasses)];
  117. return;
  118. }
  119. switch (column) {
  120. case ClassSelectorColumn.Module:
  121. transformedRules[column].forEach((rule) => {
  122. const term = rule.filterData?.term?.toLowerCase() ?? "";
  123. const checkName = term.length >= MinFullNameSearchLength;
  124. if (term.length == 0) return;
  125. didFilter = true;
  126. foundClasses.push(
  127. ...classes.filter(
  128. (c) =>
  129. c.name.toLowerCase().includes(term) ||
  130. (checkName &&
  131. c.module?.name.toLocaleLowerCase().includes(term)),
  132. ),
  133. );
  134. });
  135. break;
  136. case ClassSelectorColumn.Room:
  137. transformedRules[column].forEach((rule) => {
  138. const term = rule.filterData?.term?.toLowerCase() ?? "";
  139. if (term.length == 0) return;
  140. didFilter = true;
  141. const clsss = classes.filter((c) => {
  142. if (c.rooms.length == 0) return false;
  143. return c.rooms.join(" ").toLowerCase().includes(term);
  144. });
  145. foundClasses.push(...clsss);
  146. });
  147. break;
  148. case ClassSelectorColumn.Class:
  149. transformedRules[column].forEach((rule) => {
  150. const term = rule.filterData?.term?.toLowerCase() ?? "";
  151. if (term.length == 0) return;
  152. didFilter = true;
  153. foundClasses.push(
  154. ...classes.filter((c) => c.class.toLowerCase().includes(term)),
  155. );
  156. });
  157. break;
  158. case ClassSelectorColumn.Lecturer:
  159. transformedRules[column].forEach((rule) => {
  160. const term = rule.filterData?.term?.toLowerCase() ?? "";
  161. if (term.length == 0) return;
  162. didFilter = true;
  163. foundClasses.push(
  164. ...classes.filter((c) =>
  165. c.teachers.join().toLowerCase().includes(term),
  166. ),
  167. );
  168. });
  169. break;
  170. case ClassSelectorColumn.TeachingType:
  171. transformedRules[column].forEach((rule) => {
  172. if (rule.filterData.teachingType?.length == 0) return;
  173. didFilter = true;
  174. foundClasses.push(
  175. ...classes.filter(
  176. (c) => c.teaching_type == rule.filterData.teachingType,
  177. ),
  178. );
  179. });
  180. break;
  181. default:
  182. console.warn(`Filter type ${column} is not supported!`);
  183. return;
  184. }
  185. if (didFilter) classes = [...new Set(foundClasses)];
  186. });
  187. return classes;
  188. },
  189. degreePrograms(): string[] {
  190. const cd = this.currentData;
  191. if (cd == null) return [];
  192. return cd.degreePrograms;
  193. },
  194. currentData(): ClassesVersion | null {
  195. const version = this.cvStore.semVer;
  196. if (version == null) return null;
  197. if (!(version in this.data)) {
  198. console.error(`The version ${version} has not yet been loaded!`);
  199. return null;
  200. }
  201. return this.data[version];
  202. },
  203. },
  204. actions: {
  205. fetchData(version: string | null = null): Promise<null> {
  206. if (version == null) {
  207. version = this.cvStore.semVer;
  208. if (version == null) {
  209. // console.error("The current semester/version was unexpectedly null!");
  210. return new Promise((res, rej) => rej);
  211. }
  212. }
  213. // Check if the data has already been loaded
  214. if (version in this.data)
  215. return this.data[version].ready ?? new Promise((res, rej) => rej);
  216. const semVer = version;
  217. this.data[semVer] = {
  218. version: semVer,
  219. ready: null,
  220. loaded: false,
  221. teachers: [],
  222. rooms: [],
  223. classes: [],
  224. days: [],
  225. taughtClasses: [],
  226. blockClasses: [],
  227. degreePrograms: [],
  228. config: { blockclass_file: "block.pdf" },
  229. };
  230. const semVerFolder = this.cvStore.semVerFolderFromSemVer(semVer);
  231. const semesterFolder = this.cvStore.semesterFolderFromSemVer(semVer);
  232. this.data[semVer].ready = new Promise((resolve, reject) => {
  233. const fe = fetch(`${semVerFolder}/classes.json`);
  234. const bc = fetch(`${semesterFolder}/blockclasses.json`);
  235. const sc = fetch(`${semesterFolder}/config.json`);
  236. const modulesStore = useModulesStore();
  237. const addModuleGetters = (cls: TaughtClass) => {
  238. Object.defineProperty(cls, "module", {
  239. get: function () {
  240. return (
  241. this._module ||
  242. (this._module = modulesStore.fromModuleShort(cls.name))
  243. );
  244. },
  245. });
  246. cls.isBB = cls.class.match(bbClassRe) !== null;
  247. cls.isFirstPhase = cls.class.match(firstPhaseClassRe) !== null;
  248. cls.isEnglish = cls.class.match(englishClassRe) !== null;
  249. cls.isContext = cls.class.match(contextClassRe) !== null;
  250. cls.languageString = cls.isEnglish
  251. ? ModuleLanguages.EN
  252. : ModuleLanguages.DE;
  253. cls.executionTime =
  254. cls.weekday !== null
  255. ? `${dayMap[cls.weekday]}, ${toTime(cls.from)}-${toTime(cls.to)}`
  256. : "Blockmodul";
  257. };
  258. /**
  259. * Process the block modules
  260. */
  261. const bmReady = new Promise((resolveBM) => {
  262. Promise.all([bc, modulesStore.ready])
  263. .then((response) => response[0].json())
  264. .then((data: TaughtClass[]) => {
  265. data.forEach(addModuleGetters);
  266. this.data[semVer].blockClasses = data;
  267. resolveBM(null);
  268. })
  269. .catch((error) => {
  270. console.error(error);
  271. this.data[semVer].blockClasses = [];
  272. resolveBM(null); // We will resolve and not reject, as it's okay to not have a blockmodules.json on the server
  273. });
  274. });
  275. /**
  276. * Process the semester config
  277. */
  278. const scReady = new Promise((resolveSC) => {
  279. Promise.all([sc, modulesStore.ready])
  280. .then((response) => response[0].json())
  281. .then((data: SemesterConfiguration) => {
  282. this.data[semVer].config = data;
  283. resolveSC(null);
  284. })
  285. .catch((error) => {
  286. console.error(error);
  287. this.data[semVer].config = { blockclass_file: "block.pdf" };
  288. resolveSC(null); // We will resolve and not reject, as it's okay to not have a blockmodules.json on the server
  289. });
  290. });
  291. /**
  292. * Process the standard classes
  293. */
  294. Promise.all([fe, bmReady, scReady, modulesStore.ready])
  295. .then((response) => (response[0] as Response).json())
  296. .then((data: TaughtClass[]) => {
  297. data.forEach(addModuleGetters);
  298. this.data[semVer].taughtClasses = data;
  299. this.updateUniqueDegreePrograms(semVer);
  300. this.data[semVer].loaded = true;
  301. const planningStore = usePlanningStore();
  302. planningStore.loadFromStorage();
  303. resolve(null);
  304. })
  305. .catch((error) => {
  306. console.error(error);
  307. this.data[semVer].taughtClasses = [];
  308. reject(null);
  309. });
  310. });
  311. return this.data[version].ready ?? new Promise((res, rej) => rej);
  312. },
  313. updateUniqueDegreePrograms(version: string) {
  314. this.data[version].degreePrograms = [];
  315. this.allClasses.forEach((m) => {
  316. if (
  317. m.degree_prg.length > 0 &&
  318. !this.data[version].degreePrograms.includes(m.degree_prg)
  319. )
  320. this.data[version].degreePrograms.push(m.degree_prg);
  321. });
  322. this.data[version].degreePrograms.sort((a, b) =>
  323. a.toLowerCase().localeCompare(b.toLowerCase()),
  324. );
  325. },
  326. getById(module_id: string): TaughtClass | null {
  327. const matched = this.allClasses.filter((c) => c.id == module_id);
  328. if (matched.length == 0) return null;
  329. return matched[0];
  330. },
  331. updateOrdering(col: ClassSelectorColumn, dir: Ordering) {
  332. const colName = col.valueOf();
  333. if (dir == Ordering.None && col in this.sortBy) {
  334. delete this.sortBy[col];
  335. return;
  336. }
  337. switch (col) {
  338. case ClassSelectorColumn.Time:
  339. this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
  340. const ta = 86400 * (a.weekday ?? 0) + a.from;
  341. const tb = 86400 * (b.weekday ?? 0) + b.from;
  342. return (ta - tb) * dir;
  343. };
  344. break;
  345. case ClassSelectorColumn.Lecturer:
  346. this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
  347. const ta = a.teachers.join("").toLowerCase();
  348. const tb = b.teachers.join("").toLowerCase();
  349. return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir;
  350. };
  351. break;
  352. case ClassSelectorColumn.TeachingType:
  353. this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
  354. const ta = a.teaching_type;
  355. const tb = b.teaching_type;
  356. return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir;
  357. };
  358. break;
  359. case ClassSelectorColumn.Room:
  360. this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
  361. const ta = a.rooms.join("").toLowerCase();
  362. const tb = b.rooms.join("").toLowerCase();
  363. return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir;
  364. };
  365. break;
  366. case ClassSelectorColumn.MSP:
  367. this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
  368. const aMSP = a.module?.hasMSP === true;
  369. const bMSP = b.module?.hasMSP === true;
  370. return (aMSP == bMSP ? 0 : aMSP ? -1 : 1) * dir;
  371. };
  372. break;
  373. case ClassSelectorColumn.Module:
  374. case ClassSelectorColumn.Class:
  375. this.sortBy[col] = (a, b) =>
  376. (a[colName] < b[colName] ? -1 : a[colName] > b[colName] ? 1 : 0) *
  377. dir;
  378. break;
  379. default:
  380. console.error("Unhandled sorting!");
  381. break;
  382. }
  383. },
  384. insertFilter(afterFilter: FilterRule | null) {
  385. const emptyFilter = {
  386. column: ClassSelectorColumn.Module,
  387. filterData: { term: "" },
  388. pk: Math.floor(Math.random() * 1e20),
  389. enabled: true,
  390. } as FilterRule;
  391. if (!afterFilter) {
  392. this.filterRules.push(emptyFilter);
  393. return;
  394. }
  395. const idx = this.filterRules.findIndex((r) => r.pk == afterFilter.pk);
  396. if (idx == -1) {
  397. this.filterRules.push(emptyFilter);
  398. return;
  399. }
  400. this.filterRules.splice(idx + 1, 0, emptyFilter);
  401. },
  402. removeFilter(filterRule: FilterRule) {
  403. const idx = this.filterRules.findIndex((r) => r.pk == filterRule.pk);
  404. if (idx == -1) {
  405. console.error("Failed to find the filter rule in the current set!");
  406. return;
  407. }
  408. this.filterRules.splice(idx, 1);
  409. },
  410. classesForModule(short: string): TaughtClass[] {
  411. const cd = this.currentData;
  412. if (cd == null) return [];
  413. return cd.taughtClasses.filter((c) => c.name == short);
  414. },
  415. },
  416. });
  417. function filterTimeColumn(
  418. modules: TaughtClass[],
  419. filterset: FilterRule,
  420. ): TaughtClass[] {
  421. const timeData = filterset.filterData as Record<string, number>;
  422. const start = timeData.startTime;
  423. const end = timeData.endTime;
  424. if (start == undefined && end == undefined) return modules;
  425. let startingDay = -1;
  426. if (start != undefined) {
  427. if (start < 0) {
  428. const pStart = -1 * start;
  429. modules = modules.filter((m) => m.from >= pStart);
  430. } else {
  431. startingDay = Math.floor(start / 86400);
  432. const startingTime = start - startingDay * 86400;
  433. modules = modules.filter(
  434. (m) =>
  435. ((m.weekday ?? 0) == startingDay && m.from >= startingTime) ||
  436. (m.weekday ?? 0) > startingDay,
  437. );
  438. }
  439. }
  440. if (end != undefined) {
  441. let aEnd = end;
  442. if (aEnd < 0) {
  443. if (startingDay >= 0) {
  444. const endingTime = aEnd * -1 - startingDay * 86400;
  445. modules = modules.filter(
  446. (m) =>
  447. (m.weekday ?? 0) == startingDay &&
  448. (endingTime == 0 || m.to <= endingTime),
  449. );
  450. } else {
  451. aEnd *= -1;
  452. modules = modules.filter((m) => m.to <= aEnd);
  453. }
  454. } else {
  455. const endingDay = Math.floor(aEnd / 86400);
  456. const endingTime = aEnd - endingDay * 86400;
  457. modules = modules.filter(
  458. (m) =>
  459. (m.weekday ?? 0) < endingDay ||
  460. ((m.weekday ?? 0) == endingDay &&
  461. (endingTime == 0 || m.to <= endingTime)),
  462. );
  463. }
  464. }
  465. return modules;
  466. }