finished backend

This commit is contained in:
2026-04-17 21:30:46 +02:00
parent 8d4c54fdef
commit 6889cf15bf
5 changed files with 952 additions and 49 deletions
+392
View File
@@ -0,0 +1,392 @@
[
{
"room": "027",
"teacher": "DavA",
"subject": "BI 2",
"startTime": "08:00",
"endTime": "09:40",
"weekday": "Freitag",
"date": "17.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "09:40",
"endTime": "10:25",
"weekday": "Freitag",
"date": "17.04.2026",
"cancelled": false
},
{
"room": "U56",
"teacher": "BürD",
"subject": "TC G1",
"startTime": "10:55",
"endTime": "12:25",
"weekday": "Freitag",
"date": "17.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "12:30",
"endTime": "13:15",
"weekday": "Freitag",
"date": "17.04.2026",
"cancelled": false
},
{
"room": "254",
"teacher": "RadF",
"subject": "E G4",
"startTime": "14:00",
"endTime": "14:45",
"weekday": "Freitag",
"date": "17.04.2026",
"cancelled": false
},
{
"room": "230",
"teacher": "HeeA",
"subject": "Ku G3",
"startTime": "14:45",
"endTime": "16:20",
"weekday": "Freitag",
"date": "17.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "09:40",
"endTime": "10:25",
"weekday": "Montag",
"date": "20.04.2026",
"cancelled": false
},
{
"room": "255",
"teacher": "ScLa",
"subject": "M G2",
"startTime": "10:55",
"endTime": "12:25",
"weekday": "Montag",
"date": "20.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "12:30",
"endTime": "13:15",
"weekday": "Montag",
"date": "20.04.2026",
"cancelled": false
},
{
"room": "U53",
"teacher": "WenS",
"subject": "Ph G2",
"startTime": "14:00",
"endTime": "14:45",
"weekday": "Montag",
"date": "20.04.2026",
"cancelled": false
},
{
"room": "NGB",
"teacher": "NGB",
"subject": "H0",
"startTime": "14:45",
"endTime": "16:20",
"weekday": "Montag",
"date": "20.04.2026",
"cancelled": false
},
{
"room": "255",
"teacher": "VanC",
"subject": "D G2",
"startTime": "08:55",
"endTime": "09:40",
"weekday": "Dienstag",
"date": "21.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "09:40",
"endTime": "10:25",
"weekday": "Dienstag",
"date": "21.04.2026",
"cancelled": false
},
{
"room": "U53",
"teacher": "WenS",
"subject": "Ph G2",
"startTime": "10:55",
"endTime": "11:40",
"weekday": "Dienstag",
"date": "21.04.2026",
"cancelled": false
},
{
"room": "255",
"teacher": "RadF",
"subject": "E G4",
"startTime": "11:40",
"endTime": "12:25",
"weekday": "Dienstag",
"date": "21.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "12:30",
"endTime": "13:15",
"weekday": "Dienstag",
"date": "21.04.2026",
"cancelled": false
},
{
"room": "TKS-3",
"teacher": "TKS",
"subject": "F7",
"startTime": "14:45",
"endTime": "16:20",
"weekday": "Dienstag",
"date": "21.04.2026",
"cancelled": false
},
{
"room": "261",
"teacher": "PerJ",
"subject": "S G1",
"startTime": "14:45",
"endTime": "16:20",
"weekday": "Dienstag",
"date": "21.04.2026",
"cancelled": false
},
{
"room": "253",
"teacher": "BecN",
"subject": "SW G2",
"startTime": "08:00",
"endTime": "09:40",
"weekday": "Mittwoch",
"date": "22.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "09:40",
"endTime": "10:25",
"weekday": "Mittwoch",
"date": "22.04.2026",
"cancelled": false
},
{
"room": "255",
"teacher": "VanC",
"subject": "D G2",
"startTime": "10:55",
"endTime": "11:40",
"weekday": "Mittwoch",
"date": "22.04.2026",
"cancelled": false
},
{
"room": "040-2",
"teacher": "ScLa",
"subject": "Sp G2",
"startTime": "11:40",
"endTime": "12:25",
"weekday": "Mittwoch",
"date": "22.04.2026",
"cancelled": false
},
{
"room": "040-3",
"teacher": "HofS",
"subject": "Sp G4",
"startTime": "11:40",
"endTime": "12:25",
"weekday": "Mittwoch",
"date": "22.04.2026",
"cancelled": true
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "12:30",
"endTime": "13:15",
"weekday": "Mittwoch",
"date": "22.04.2026",
"cancelled": false
},
{
"room": "261",
"teacher": "MasL",
"subject": "ER G1",
"startTime": "14:00",
"endTime": "15:30",
"weekday": "Mittwoch",
"date": "22.04.2026",
"cancelled": false
},
{
"room": "",
"teacher": "",
"subject": "ER G4",
"startTime": "14:00",
"endTime": "15:30",
"weekday": "Mittwoch",
"date": "22.04.2026",
"cancelled": false
},
{
"room": "TKS-3",
"teacher": "TKS-1",
"subject": "F7",
"startTime": "08:00",
"endTime": "09:40",
"weekday": "Donnerstag",
"date": "23.04.2026",
"cancelled": false
},
{
"room": "255",
"teacher": "PerJ",
"subject": "S G1",
"startTime": "08:00",
"endTime": "09:40",
"weekday": "Donnerstag",
"date": "23.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "09:40",
"endTime": "10:25",
"weekday": "Donnerstag",
"date": "23.04.2026",
"cancelled": false
},
{
"room": "254",
"teacher": "WedL",
"subject": "EK G2",
"startTime": "10:55",
"endTime": "12:25",
"weekday": "Donnerstag",
"date": "23.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "12:30",
"endTime": "13:15",
"weekday": "Donnerstag",
"date": "23.04.2026",
"cancelled": false
},
{
"room": "040-2",
"teacher": "ScLa",
"subject": "Sp G2",
"startTime": "14:45",
"endTime": "16:20",
"weekday": "Donnerstag",
"date": "23.04.2026",
"cancelled": false
},
{
"room": "040-3",
"teacher": "HofS",
"subject": "Sp G4",
"startTime": "14:45",
"endTime": "16:20",
"weekday": "Donnerstag",
"date": "23.04.2026",
"cancelled": true
},
{
"room": "027",
"teacher": "DavA",
"subject": "BI 2",
"startTime": "08:00",
"endTime": "09:40",
"weekday": "Freitag",
"date": "24.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "09:40",
"endTime": "10:25",
"weekday": "Freitag",
"date": "24.04.2026",
"cancelled": false
},
{
"room": "U56",
"teacher": "BürD",
"subject": "TC G1",
"startTime": "10:55",
"endTime": "12:25",
"weekday": "Freitag",
"date": "24.04.2026",
"cancelled": false
},
{
"room": "-",
"teacher": "-",
"subject": "SZ",
"startTime": "12:30",
"endTime": "13:15",
"weekday": "Freitag",
"date": "24.04.2026",
"cancelled": false
},
{
"room": "254",
"teacher": "RadF",
"subject": "E G4",
"startTime": "14:00",
"endTime": "14:45",
"weekday": "Freitag",
"date": "24.04.2026",
"cancelled": false
},
{
"room": "230",
"teacher": "HeeA",
"subject": "Ku G3",
"startTime": "14:45",
"endTime": "16:20",
"weekday": "Freitag",
"date": "24.04.2026",
"cancelled": false
}
]
+216
View File
@@ -0,0 +1,216 @@
export const formatData = (rawData) => {
const weekdays = [
"Sonntag",
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag",
];
const MERGE_GAP_MINUTES = 15;
function formatTime(time) {
if (time === null || time === undefined) return "";
const n = Number(time);
if (!Number.isFinite(n)) return "";
const str = String(time).padStart(4, "0");
return str.slice(0, 2) + ":" + str.slice(2);
}
function formatDate(date) {
if (date === null || date === undefined) return "";
const str = String(date);
if (str.length < 8) return "";
return str.slice(6, 8) + "." + str.slice(4, 6) + "." + str.slice(0, 4);
}
function getWeekday(date) {
if (date === null || date === undefined) return "";
const str = String(date);
if (str.length < 8) return "";
const d = new Date(str.slice(0, 4), str.slice(4, 6) - 1, str.slice(6, 8));
if (Number.isNaN(d.getTime())) return "";
return weekdays[d.getDay()];
}
function parseTimeToMinutes(timeStr) {
if (!timeStr || timeStr === "") return null;
const [h, m] = String(timeStr).split(":");
const hh = Number(h);
const mm = Number(m);
if (!Number.isFinite(hh) || !Number.isFinite(mm)) return null;
if (hh < 0 || hh > 23 || mm < 0 || mm > 59) return null;
return hh * 60 + mm;
}
function dateKeyFromFormatted(dateStr) {
if (!dateStr || dateStr === "") return null;
const [dd, mm, yyyy] = String(dateStr).split(".");
const d = Number(dd);
const m = Number(mm);
const y = Number(yyyy);
if (!Number.isFinite(d) || !Number.isFinite(m) || !Number.isFinite(y)) {
return null;
}
return y * 10000 + m * 100 + d;
}
const items = rawData?.result ?? [];
const lessons = [];
for (const item of items) {
if (item?.activityType !== "Unterricht") continue;
const subject = item?.su?.[0]?.name ?? "";
const normalizedSubject = String(subject).trimStart();
const isSz = normalizedSubject.startsWith("SZ");
const cancelled =
String(item?.code ?? "")
.trim()
.toLowerCase() === "cancelled";
lessons.push({
room: item?.ro?.[0]?.name ?? "",
teacher: item?.te?.[0]?.name ?? "",
subject,
startTime: formatTime(item?.startTime),
endTime: formatTime(item?.endTime),
weekday: getWeekday(item?.date),
date: formatDate(item?.date),
cancelled,
// internal fields for grouping/sorting
_dateRaw: Number(item?.date) || 0,
_startTimeRaw: Number(item?.startTime) || 0,
_endTimeRaw: Number(item?.endTime) || 0,
_isSz: isSz,
});
}
lessons.sort((a, b) => {
if (a._dateRaw !== b._dateRaw) return a._dateRaw - b._dateRaw;
if (a._startTimeRaw !== b._startTimeRaw)
return a._startTimeRaw - b._startTimeRaw;
return a._endTimeRaw - b._endTimeRaw;
});
// Group by same day + same start time.
// If any subject in a group starts with "SZ", collapse the whole group into the SZ lesson.
const groups = new Map();
for (const lesson of lessons) {
const key = `${lesson._dateRaw}-${lesson._startTimeRaw}`;
const group = groups.get(key);
if (group) group.push(lesson);
else groups.set(key, [lesson]);
}
const timetable = [];
for (const group of groups.values()) {
const hasSz = group.some((l) => l._isSz);
if (!hasSz) {
for (const lesson of group) {
const { _dateRaw, _startTimeRaw, _endTimeRaw, _isSz, ...publicLesson } =
lesson;
timetable.push(publicLesson);
}
continue;
}
// Normalize: if any SZ* exists, show exactly one SZ block for that slot.
const base = group.find((l) => l._isSz) ?? group[0];
const maxEndRaw = group.reduce(
(max, l) => (l._endTimeRaw > max ? l._endTimeRaw : max),
0,
);
const cancelled = group.every((l) => l.cancelled);
timetable.push({
room: "-",
teacher: "-",
subject: "SZ",
startTime: base.startTime,
endTime: maxEndRaw ? formatTime(maxEndRaw) : base.endTime,
weekday: base.weekday,
date: base.date,
cancelled,
});
}
// Merge double lessons (same subject/teacher/room/day) that are consecutive
// (allows a short gap, e.g. breaks).
const bucketKey = (e) =>
`${e.date}|${e.weekday}|${e.subject}|${e.teacher}|${e.room}`;
const buckets = new Map();
for (const entry of timetable) {
const key = bucketKey(entry);
const arr = buckets.get(key);
if (arr) arr.push(entry);
else buckets.set(key, [entry]);
}
const merged = [];
for (const arr of buckets.values()) {
arr.sort((a, b) => {
const as = parseTimeToMinutes(a.startTime) ?? 0;
const bs = parseTimeToMinutes(b.startTime) ?? 0;
if (as !== bs) return as - bs;
const ae = parseTimeToMinutes(a.endTime) ?? 0;
const be = parseTimeToMinutes(b.endTime) ?? 0;
return ae - be;
});
const mergedArr = [];
for (const entry of arr) {
const prev = mergedArr[mergedArr.length - 1];
if (!prev) {
mergedArr.push(entry);
continue;
}
const prevEnd = parseTimeToMinutes(prev.endTime);
const currStart = parseTimeToMinutes(entry.startTime);
const currEnd = parseTimeToMinutes(entry.endTime);
const canMergeTimes =
prevEnd !== null &&
currStart !== null &&
currEnd !== null &&
currStart >= prevEnd &&
currStart - prevEnd <= MERGE_GAP_MINUTES;
if (canMergeTimes) {
prev.endTime = entry.endTime;
prev.cancelled = Boolean(prev.cancelled) || Boolean(entry.cancelled);
continue;
}
mergedArr.push(entry);
}
merged.push(...mergedArr);
}
merged.sort((a, b) => {
const ad = dateKeyFromFormatted(a.date) ?? 0;
const bd = dateKeyFromFormatted(b.date) ?? 0;
if (ad !== bd) return ad - bd;
const as = parseTimeToMinutes(a.startTime) ?? 0;
const bs = parseTimeToMinutes(b.startTime) ?? 0;
if (as !== bs) return as - bs;
// stable-ish: keep similar subjects grouped
const subj = String(a.subject).localeCompare(String(b.subject));
if (subj !== 0) return subj;
const room = String(a.room).localeCompare(String(b.room));
if (room !== 0) return room;
return String(a.teacher).localeCompare(String(b.teacher));
});
return merged;
};
+151 -3
View File
@@ -9,6 +9,8 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"axios": "^1.15.0",
"dotenv": "^17.4.2",
"express": "^5.2.1"
}
},
@@ -25,6 +27,23 @@
"node": ">= 0.6"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/body-parser": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
@@ -87,6 +106,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
@@ -144,6 +175,15 @@
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -153,6 +193,18 @@
"node": ">= 0.8"
}
},
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -212,6 +264,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -291,6 +358,63 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/follow-redirects": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -379,6 +503,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -561,9 +700,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -583,6 +722,15 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+4 -1
View File
@@ -2,6 +2,7 @@
"name": "backend",
"version": "1.0.0",
"main": "server.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
@@ -11,6 +12,8 @@
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.15.0",
"dotenv": "^17.4.2",
"express": "^5.2.1"
}
}
}
+189 -45
View File
@@ -1,54 +1,198 @@
import express from "express";
import dotenv from "dotenv";
import axios from "axios";
import { formatData } from "./formatter.js";
dotenv.config();
const app = express();
app.get("/api/today", (req, res) => {
const userLink = req.query.userLink;
app.use(express.json());
// Simulate fetching today's schedule
const todaySchedule = {
subjects: [
{
lesson_nr: "1.",
name: "D G2",
room: "255",
teacher: "VanC",
clock: "08:00-08:55",
cancelled: false,
},
{
lesson_nr: "2. & 3.",
name: "M G2",
room: "255",
teacher: "ScLa",
clock: "08:55-10:25",
cancelled: false,
},
{
lesson_nr: "4.",
name: "E G4",
room: "254",
teacher: "RadF",
clock: "10:55-11:40",
cancelled: false,
},
{
lesson_nr: "5.",
name: "CH G2",
room: "133",
teacher: "LenT",
clock: "11:40-12:25",
cancelled: true,
},
],
date: "15.04.2026",
day: "Mittwoch",
week: "16",
student: "Max Mustermann",
app.get("/api/get-timetable/", async (req, res) => {
const username = req.query.username; // required
const password = req.query.password; // required
const school = req.query.school; // required
if (!username || !password || !school) {
res.status(400).json({
error: "Missing required query params: username, password, school",
});
return;
}
let startDate = req.query.startDate; // optional - expected format: YYYYMMDD
let endDate = req.query.endDate; // optional - expected format: YYYYMMDD
let sessionId = ""; // will be set after authentication by server response
let personId = ""; // will be set after authentication by server response
let personType = ""; // will be set after authentication by server response
// static function for checking or/and generating start and end date for the current week
function setDates() {
if (startDate && endDate) {
return;
}
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const dayOfWeek = today.getDay(); // 0 (Sunday) to 6 (Saturday)
const addDays = (date, days) =>
new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
// format dates to YYYYMMDD
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}${month}${day}`;
};
// Desired behavior:
// - Weekday (Mon-Thu): startDate = today, endDate = coming Friday (same week)
// - Friday: startDate = today, endDate = coming Friday (next week)
// - Weekend (Sat/Sun): startDate = coming Monday, endDate = coming Friday (of that week)
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
let start;
let end;
if (isWeekend) {
const daysUntilMonday = dayOfWeek === 0 ? 1 : 2; // Sun->Mon:1, Sat->Mon:2
start = addDays(today, daysUntilMonday);
end = addDays(start, 4); // Monday + 4 days = Friday
} else {
start = today;
const daysUntilFriday = 5 - dayOfWeek; // Mon(1)->4 ... Fri(5)->0
end = addDays(today, daysUntilFriday === 0 ? 7 : daysUntilFriday);
}
startDate = formatDate(start);
endDate = formatDate(end);
}
setDates();
const baseUrl = `https://${school}.webuntis.com/WebUntis/jsonrpc.do?school=${school}`;
const toJsonRpcError = (data) => {
const err = data?.error;
if (!err) return null;
const code = err.code;
const message = err.message || "Unknown WebUntis error";
return { code, message };
};
console.log(`Fetching schedule for user: ${userLink}`);
res.json(todaySchedule);
try {
// 1) authenticate
const authResponse = await axios.post(baseUrl, {
id: "1",
method: "authenticate",
params: {
user: username,
password: password,
client: "watch-untis",
},
jsonrpc: "2.0",
});
const authErr = toJsonRpcError(authResponse.data);
if (authErr) {
if (authErr.message === "bad credentials") {
res.status(401).json({ error: "Invalid username or password" });
return;
}
if (authErr.code === -8504) {
res.status(403).json({ error: "WebUntis Error: -8504" });
return;
}
res.status(502).json({ error: authErr.message, code: authErr.code });
return;
}
sessionId = authResponse.data?.result?.sessionId;
personId = authResponse.data?.result?.personId;
personType = authResponse.data?.result?.personType;
if (!sessionId || !personId) {
res.status(502).json({
error: "WebUntis auth succeeded but returned no sessionId/personId",
});
return;
}
// 2) get timetable (after we have sessionId, personId & personType from auth response)
const timetableResponse = await axios.post(
baseUrl,
{
id: "3",
method: "getTimetable",
params: {
options: {
element: {
id: personId,
type: personType,
},
startDate,
endDate,
teacherFields: ["id", "name", "longname"],
subjectFields: ["id", "name", "longname"],
roomFields: ["id", "name", "longname"],
},
},
jsonrpc: "2.0",
},
{
headers: {
Cookie: `JSESSIONID=${sessionId}`,
},
},
);
const timetableErr = toJsonRpcError(timetableResponse.data);
if (timetableErr) {
res
.status(502)
.json({ error: timetableErr.message, code: timetableErr.code });
return;
}
// 3) format the data once it successfully returned
try {
const formattedData = formatData(timetableResponse.data);
res.json(formattedData);
} catch (formatError) {
console.error("formatData failed", formatError);
res.status(500).json({ error: "Failed to format timetable data" });
}
// respond to client
res.json(timetableResponse.data?.result ?? null);
return;
} catch (error) {
const status = error?.response?.status;
const data = error?.response?.data;
const jsonRpcErr = toJsonRpcError(data);
if (jsonRpcErr?.message === "bad credentials") {
res.status(401).json({ error: "Invalid username or password" });
return;
}
if (jsonRpcErr?.code === -8504) {
res.status(403).json({ error: "WebUntis Error: -8504" });
return;
}
console.error(error);
res.status(502).json({
error: "Failed to fetch timetable",
upstreamStatus: status,
});
}
});
// error handling code
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Something broke!");
});
app.listen(8001, () => {