/* ==========================================================================
librus-tui frontend - timetable view
========================================================================== */
"use strict";
async function renderTimetable(token) {
const data = await api(`/api/timetable?week=${state.timetableWeek}`);
if (token !== state.loadToken) return;
const timetable = data.Timetable || {};
const weekStart = parseDate(data.weekStart) || mondayOfCurrentWeek(state.timetableWeek);
const weekEnd = parseDate(data.weekEnd) || addDays(weekStart, 6);
const weekStartIso = isoDate(weekStart);
const weekEndIso = isoDate(weekEnd);
const columns = WEEK_ORDER.filter(
(day, index) => index < 5 || (timetable[day] || []).length > 0
);
const numericLessons = [];
let hasExtra = false;
for (const day of columns) {
for (const entry of timetable[day] || []) {
const raw = String(entry.Lesson ?? "");
if (/^\d+$/.test(raw)) {
numericLessons.push(Number(raw));
} else {
hasExtra = true;
}
}
}
const maxLesson = Math.max(8, ...numericLessons);
const rows = [];
for (let number = 1; number <= maxLesson; number += 1) rows.push(String(number));
if (hasExtra) rows.push("extra");
const grid = {};
for (const day of columns) {
grid[day] = new Map();
for (const entry of timetable[day] || []) {
const raw = String(entry.Lesson ?? "");
const key = /^\d+$/.test(raw) ? raw : "extra";
if (!grid[day].has(key)) grid[day].set(key, []);
grid[day].get(key).push(entry);
}
}
const today = isoDate();
const weekLabel =
state.timetableWeek === 0
? "current week"
: state.timetableWeek === 1
? "next week"
: `week +${state.timetableWeek}`;
const header = columns
.map((day) => {
const index = WEEK_ORDER.indexOf(day);
const date = addDays(weekStart, index);
const dateIso = isoDate(date);
return `
${esc(day.slice(0, 3))}
${esc(fmtDate(dateIso, false))}
`;
})
.join("");
const body = rows
.map((row) => {
let timeLabel = "";
if (row !== "extra") {
for (const day of columns) {
const entry = (grid[day].get(row) || [])[0];
if (entry && entry.HourFrom) {
timeLabel = shortTime(entry.HourFrom);
break;
}
}
} else {
for (const day of columns) {
const entry = (grid[day].get("extra") || [])[0];
if (entry && entry.HourFrom) {
timeLabel = shortTime(entry.HourFrom);
break;
}
}
}
const hourCell = `
${row === "extra" ? "+" : esc(row)}
${esc(timeLabel)}
`;
const cells = columns
.map((day) => {
const entries = grid[day].get(row) || [];
if (!entries.length) {
return ``;
}
return `${entries.map(renderTimetableEntry).join("")}
`;
})
.join("");
return hourCell + cells;
})
.join("");
$("#view").innerHTML = `
${esc(weekLabel)}
${esc(fmtDate(weekStartIso))} → ${esc(fmtDate(weekEndIso))}
[ ] switch week
SUB substitution ·
CNL cancelled ·
EXTRA additional activity
`;
}
function renderTimetableEntry(entry) {
const flags = [];
if (entry.isSubstitution) flags.push(`SUB`);
if (entry.isCancelled) flags.push(`CNL`);
if (entry.isAdditional) flags.push(``);
const teacher = personName(entry.Teacher);
const title = [
entry.Subject,
timeRange(entry.HourFrom, entry.HourTo),
entry.Classroom ? `room ${entry.Classroom}` : "",
teacher,
]
.filter(Boolean)
.join(" · ");
return `
${esc(entry.Subject || "lesson")}
${esc(timeRange(entry.HourFrom, entry.HourTo))}
${esc(entry.Classroom || "?")}${teacher ? ` · ${esc(teacher)}` : ""}
${flags.length ? `${flags.join("")}` : ""}
`;
}
function mondayOfCurrentWeek(week = 0) {
const today = new Date();
const monday = addDays(today, -((today.getDay() + 6) % 7) + week * 7);
monday.setHours(0, 0, 0, 0);
return monday;
}
function addDays(date, days) {
const copy = new Date(date.getTime());
copy.setDate(copy.getDate() + days);
return copy;
}
function changeTimetableWeek(delta) {
const next = Math.min(3, Math.max(0, state.timetableWeek + delta));
if (next === state.timetableWeek) return;
state.timetableWeek = next;
navigate("timetable");
}