mirror of
https://github.com/ae-utbm/sith.git
synced 2025-11-03 10:33:06 +00:00
185 lines
5.6 KiB
TypeScript
185 lines
5.6 KiB
TypeScript
import html2canvas from "html2canvas";
|
|
|
|
// see https://regex101.com/r/QHSaPM/2
|
|
const TIMETABLE_ROW_RE: RegExp =
|
|
/^(?<ueCode>\w.+\w)\s+(?<courseType>[A-Z]{2}\d)\s+((?<weekGroup>[AB])\s+)?(?<weekday>(lundi)|(mardi)|(mercredi)|(jeudi)|(vendredi)|(samedi)|(dimanche))\s+(?<startHour>\d{2}:\d{2})\s+(?<endHour>\d{2}:\d{2})\s+[\dA-B]\s+((?<attendance>[\wé]*)\s+)?(?<room>\w+(?:, \w+)?)$/;
|
|
|
|
const DEFAULT_TIMETABLE: string = `DS52\t\tCM1\t\tlundi\t08:00\t10:00\t1\tPrésentiel\tA113
|
|
DS53\t\tCM1\t\tlundi\t10:15\t12:15\t1\tPrésentiel\tA101
|
|
DS53\t\tTP1\t\tlundi\t13:00\t16:00\t1\tPrésentiel\tH010
|
|
SO03\t\tCM1\t\tlundi\t16:15\t17:45\t1\tPrésentiel\tA103
|
|
SO03\t\tTD1\t\tlundi\t17:45\t19:45\t1\tPrésentiel\tA103
|
|
DS50\t\tTP1\t\tmardi\t08:00\t10:00\t1\tPrésentiel\tA216
|
|
DS51\t\tCM1\t\tmardi\t10:15\t12:15\t1\tPrésentiel\tA216
|
|
DS51\t\tTP1\t\tmardi\t14:00\t18:00\t1\tPrésentiel\tH010
|
|
DS52\t\tTP2\tA\tjeudi\t08:00\t10:00\tA\tPrésentiel\tA110a, A110b
|
|
DS52\t\tTD1\t\tjeudi\t10:15\t12:15\t1\tPrésentiel\tA110a, A110b
|
|
LC02\t\tTP1\t\tjeudi\t15:00\t16:00\t1\tPrésentiel\tA209
|
|
LC02\t\tTD1\t\tjeudi\t16:15\t18:15\t1\tPrésentiel\tA206`;
|
|
|
|
type WeekDay =
|
|
| "lundi"
|
|
| "mardi"
|
|
| "mercredi"
|
|
| "jeudi"
|
|
| "vendredi"
|
|
| "samedi"
|
|
| "dimanche";
|
|
|
|
const WEEKDAYS = [
|
|
"lundi",
|
|
"mardi",
|
|
"mercredi",
|
|
"jeudi",
|
|
"vendredi",
|
|
"samedi",
|
|
"dimanche",
|
|
] as const;
|
|
|
|
const SLOT_HEIGHT = 20 as const; // Each 15min has a height of 20px in the timetable
|
|
const SLOT_WIDTH = 250 as const; // Each weekday ha a width of 400px in the timetable
|
|
const MINUTES_PER_SLOT = 15 as const;
|
|
|
|
interface TimetableSlot {
|
|
courseType: string;
|
|
room: string;
|
|
startHour: string;
|
|
endHour: string;
|
|
startSlot: number;
|
|
endSlot: number;
|
|
ueCode: string;
|
|
weekGroup?: string;
|
|
weekday: WeekDay;
|
|
}
|
|
|
|
function parseSlots(s: string): TimetableSlot[] {
|
|
return s
|
|
.split("\n")
|
|
.filter((s: string) => s.length > 0)
|
|
.map((row: string) => {
|
|
const parsed = TIMETABLE_ROW_RE.exec(row);
|
|
if (!parsed) {
|
|
throw new Error(`Couldn't parse row ${row}`);
|
|
}
|
|
const [startHour, startMin] = parsed.groups.startHour
|
|
.split(":")
|
|
.map((i) => Number.parseInt(i));
|
|
const [endHour, endMin] = parsed.groups.endHour
|
|
.split(":")
|
|
.map((i) => Number.parseInt(i));
|
|
return {
|
|
...parsed.groups,
|
|
startSlot: Math.floor((startHour * 60 + startMin) / MINUTES_PER_SLOT),
|
|
endSlot: Math.floor((endHour * 60 + endMin) / MINUTES_PER_SLOT),
|
|
} as unknown as TimetableSlot;
|
|
});
|
|
}
|
|
|
|
document.addEventListener("alpine:init", () => {
|
|
Alpine.data("timetableGenerator", () => ({
|
|
content: DEFAULT_TIMETABLE,
|
|
error: "",
|
|
displayedWeekdays: [] as WeekDay[],
|
|
courses: [] as TimetableSlot[],
|
|
startSlot: 0,
|
|
endSlot: 0,
|
|
table: {
|
|
height: 0,
|
|
width: 0,
|
|
},
|
|
|
|
colors: {} as Record<string, string>,
|
|
colorPalette: [
|
|
"#27ae60",
|
|
"#2980b9",
|
|
"#c0392b",
|
|
"#7f8c8d",
|
|
"#f1c40f",
|
|
"#1abc9c",
|
|
"#95a5a6",
|
|
"#26C6DA",
|
|
"#c2185b",
|
|
"#e64a19",
|
|
"#1b5e20",
|
|
],
|
|
|
|
generate() {
|
|
try {
|
|
this.courses = parseSlots(this.content);
|
|
} catch {
|
|
this.error = gettext(
|
|
"Wrong timetable format. Make sure you copied if from your student folder.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
// color each UE
|
|
let colorIndex = 0;
|
|
for (const slot of this.courses) {
|
|
if (!this.colors[slot.ueCode]) {
|
|
this.colors[slot.ueCode] =
|
|
this.colorPalette[colorIndex % this.colorPalette.length];
|
|
colorIndex++;
|
|
}
|
|
}
|
|
|
|
this.displayedWeekdays = WEEKDAYS.filter((day) =>
|
|
this.courses.some((slot: TimetableSlot) => slot.weekday === day),
|
|
);
|
|
this.startSlot = this.courses.reduce(
|
|
(acc: number, curr: TimetableSlot) => Math.min(acc, curr.startSlot),
|
|
25 * 4,
|
|
);
|
|
this.endSlot = this.courses.reduce(
|
|
(acc: number, curr: TimetableSlot) => Math.max(acc, curr.endSlot),
|
|
1,
|
|
);
|
|
this.table.height = SLOT_HEIGHT * (this.endSlot - this.startSlot);
|
|
this.table.width = SLOT_WIDTH * this.displayedWeekdays.length;
|
|
},
|
|
|
|
getStyle(slot: TimetableSlot) {
|
|
const hasWeekGroup = slot.weekGroup !== undefined;
|
|
const width = hasWeekGroup ? SLOT_WIDTH / 2 : SLOT_WIDTH;
|
|
const leftOffset = slot.weekGroup === "B" ? SLOT_WIDTH / 2 : 0;
|
|
return {
|
|
height: `${(slot.endSlot - slot.startSlot) * SLOT_HEIGHT}px`,
|
|
width: `${width}px`,
|
|
top: `${(slot.startSlot - this.startSlot) * SLOT_HEIGHT}px`,
|
|
left: `${this.displayedWeekdays.indexOf(slot.weekday) * SLOT_WIDTH + leftOffset}px`,
|
|
backgroundColor: this.colors[slot.ueCode],
|
|
};
|
|
},
|
|
|
|
getHours(): [string, object][] {
|
|
let hour: number = Number.parseInt(
|
|
this.courses
|
|
.map((c: TimetableSlot) => c.startHour)
|
|
.reduce((res: string, hour: string) => (hour < res ? hour : res), "24:00")
|
|
.split(":")[0],
|
|
);
|
|
const res: [string, object][] = [];
|
|
for (let i = 0; i <= this.endSlot - this.startSlot; i += 60 / MINUTES_PER_SLOT) {
|
|
res.push([`${hour}:00`, { top: `${i * SLOT_HEIGHT}px` }]);
|
|
hour += 1;
|
|
}
|
|
return res;
|
|
},
|
|
|
|
getWidth() {
|
|
return this.displayedWeekdays.length * SLOT_WIDTH + 20;
|
|
},
|
|
|
|
async savePng() {
|
|
const elem = document.getElementById("timetable");
|
|
const img = (await html2canvas(elem)).toDataURL();
|
|
const downloadLink = document.createElement("a");
|
|
downloadLink.href = img;
|
|
downloadLink.download = "edt.png";
|
|
document.body.appendChild(downloadLink);
|
|
downloadLink.click();
|
|
downloadLink.remove();
|
|
},
|
|
}));
|
|
});
|