Files
2026-11-08 19:28:03 +01:00

183 lines
6.0 KiB
JavaScript

/* ==========================================================================
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 `
<div class="tt-head ${dateIso === today ? "today" : ""}">
<span>${esc(day.slice(0, 3))}</span>
<span class="date">${esc(fmtDate(dateIso, false))}</span>
</div>`;
})
.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 = `
<div class="tt-hour">
<b>${row === "extra" ? "+" : esc(row)}</b>
<span>${esc(timeLabel)}</span>
</div>`;
const cells = columns
.map((day) => {
const entries = grid[day].get(row) || [];
if (!entries.length) {
return `<div class="tt-cell"><div class="tt-free">·</div></div>`;
}
return `<div class="tt-cell">${entries.map(renderTimetableEntry).join("")}</div>`;
})
.join("");
return hourCell + cells;
})
.join("");
$("#view").innerHTML = `
<div class="view-inner">
<div class="toolbar">
<button class="btn btn-sm" type="button" data-action="tt-prev" ${state.timetableWeek <= 0 ? "disabled" : ""}>[ &lt; prev ]</button>
<button class="btn btn-sm" type="button" data-action="tt-next" ${state.timetableWeek >= 3 ? "disabled" : ""}>[ next &gt; ]</button>
<span><b>${esc(weekLabel)}</b></span>
<span class="dim">${esc(fmtDate(weekStartIso))} → ${esc(fmtDate(weekEndIso))}</span>
<span class="grow"></span>
<span class="dim"><kbd>[</kbd> <kbd>]</kbd> switch week</span>
</div>
<div class="timetable-scroll">
<div class="timetable" style="grid-template-columns: 60px repeat(${columns.length}, minmax(150px, 1fr))">
<div class="tt-hour corner">lesson</div>
${header}
${body}
</div>
</div>
<div class="toolbar" style="margin-top:10px">
<span class="dim">
<span class="tag tag-amber">SUB</span> substitution ·
<span class="tag tag-red">CNL</span> cancelled ·
<span class="tag tag-cyan">EXTRA</span> additional activity
</span>
</div>
</div>
`;
}
function renderTimetableEntry(entry) {
const flags = [];
if (entry.isSubstitution) flags.push(`<span class="tt-flag sub">SUB</span>`);
if (entry.isCancelled) flags.push(`<span class="tt-flag cancel">CNL</span>`);
if (entry.isAdditional) flags.push(`<span class="tt-flag extra">EXTRA</span>`);
const teacher = personName(entry.Teacher);
const title = [
entry.Subject,
timeRange(entry.HourFrom, entry.HourTo),
entry.Classroom ? `room ${entry.Classroom}` : "",
teacher,
]
.filter(Boolean)
.join(" · ");
return `
<div class="tt-lesson ${entry.isCancelled ? "cancelled" : ""}" title="${esc(title)}">
<span class="tt-subject">${esc(entry.Subject || "lesson")}</span>
<span class="tt-meta">${esc(timeRange(entry.HourFrom, entry.HourTo))}</span>
<span class="tt-meta"><span class="tt-room">${esc(entry.Classroom || "?")}</span>${teacher ? ` · ${esc(teacher)}` : ""}</span>
${flags.length ? `<span class="tt-flags">${flags.join("")}</span>` : ""}
</div>`;
}
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");
}