/* ========================================================================== 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
lesson
${header} ${body}
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(`EXTRA`); 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"); }