mirror of
https://github.com/ae-utbm/sith.git
synced 2026-10-04 01:44:15 +00:00
Certains utilisateurs font remonter régulièrement que le générateur ne marche pas. C'est normal, on n'a pas forcément prévu tous les cas et on n'est pas à l'abri des changements de format de l'UTBM. Ce qui est très embêtant, dans ces cas-là, c'est qu'on donne très peu d'informations sur ce qui n'a pas marché, et que les utilisateurs peuvent difficilement faire remonter plus que ça. En décrivant la ligne qui n'a pas pu être parsée, ça devrait déjà rendre la gestion de ce genre de problème un peu plus facile, tout en étant plus transparent pour l'utilisateur
185 lines
5.7 KiB
TypeScript
185 lines
5.7 KiB
TypeScript
import html2canvas from "html2canvas";
|
|
|
|
// see https://regex101.com/r/QHSaPM/3
|
|
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?.groups) {
|
|
throw new Error(`Couldn't parse row ${row}`, { cause: { row: row } });
|
|
}
|
|
const [startHour, startMin] = parsed.groups.startHour
|
|
.split(":")
|
|
.map((i) => Number.parseInt(i, 10));
|
|
const [endHour, endMin] = parsed.groups.endHour
|
|
.split(":")
|
|
.map((i) => Number.parseInt(i, 10));
|
|
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: null as { incorrectRow?: string },
|
|
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);
|
|
this.error = null;
|
|
} catch (err) {
|
|
this.error = { incorrectRow: err?.cause?.row };
|
|
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],
|
|
10,
|
|
);
|
|
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 as HTMLElement)).toDataURL();
|
|
const downloadLink = document.createElement("a");
|
|
downloadLink.href = img;
|
|
downloadLink.download = "edt.png";
|
|
document.body.appendChild(downloadLink);
|
|
downloadLink.click();
|
|
downloadLink.remove();
|
|
},
|
|
}));
|
|
});
|