finished backend
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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;
|
||||
};
|
||||
Generated
+151
-3
@@ -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",
|
||||
|
||||
@@ -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
@@ -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, () => {
|
||||
|
||||
Reference in New Issue
Block a user