Files
librustui/static/js/20-dashboard.js
T
2026-11-08 19:28:03 +01:00

221 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
librus-tui frontend - dashboard view
========================================================================== */
"use strict";
async function renderDashboard(token) {
const [bootstrap, notifications, timetable, freedays] = await Promise.all([
api("/api/bootstrap"),
api("/api/notifications").catch(() => null),
api("/api/timetable?week=0").catch(() => null),
api("/api/freedays").catch(() => null),
]);
if (token !== state.loadToken) return;
applyUserChrome(bootstrap);
const me = bootstrap.me || {};
const school = bootstrap.school || {};
const behaviour = bootstrap.behaviour;
const lucky = bootstrap.luckyNumber;
const notif = notifications || {};
const now = new Date();
const todayName = DAY_NAMES[now.getDay()];
const todayIndex = (now.getDay() + 6) % 7; // Monday = 0
const todayIso = isoDate(now);
const week = (timetable && timetable.Timetable) || {};
let dayName = todayName;
let lessons = week[todayName] || [];
let dayDate = (lessons[0] && lessons[0].Date) || todayIso;
if (!lessons.length) {
// Prefer the next day that still belongs to the current week.
for (let offset = 1; offset < 7 - todayIndex; offset += 1) {
const candidate = WEEK_ORDER[todayIndex + offset];
const candidateLessons = week[candidate] || [];
if (candidateLessons.length) {
dayName = candidate;
lessons = candidateLessons;
dayDate = candidateLessons[0].Date || todayIso;
break;
}
}
}
if (!lessons.length) {
// Weekends: peek at next week so the panel is not empty.
try {
const nextWeek = await api("/api/timetable?week=1");
if (token !== state.loadToken) return;
const nextData = (nextWeek && nextWeek.Timetable) || {};
const firstDay = WEEK_ORDER.find((day) => (nextData[day] || []).length);
if (firstDay) {
dayName = firstDay;
lessons = nextData[firstDay];
dayDate = (lessons[0] && lessons[0].Date) || todayIso;
}
} catch (error) {
/* dashboard works without a preview */
}
}
const isToday = dayDate === todayIso;
const schoolFree = ((freedays && freedays.school) || [])
.filter((item) => item && item.DateFrom)
.slice()
.sort((a, b) => (parseDate(a.DateFrom) || 0) - (parseDate(b.DateFrom) || 0));
const upcomingFree = schoolFree
.filter((item) => (daysUntil(item.DateFrom) ?? -1) >= 0)
.slice(0, 4);
const freeRows = upcomingFree.length
? upcomingFree
.map(
(item) => `
<div class="freeday-line" title="${esc(fmtDate(item.DateFrom))} → ${esc(fmtDate(item.DateTo))}">
<span class="name">${esc(item.Name || "free day")}</span>
<span class="countdown ${countdownClass(item.DateFrom)}">${esc(countdownLabel(item.DateFrom))} · ${esc(fmtDateShort(item.DateFrom))}</span>
</div>`
)
.join("")
: `<div class="empty">no upcoming free days</div>`;
const lessonsHtml = renderDashboardLessons(lessons, isToday);
const html = `
<div class="view-inner">
<div class="dash-grid">
<section class="panel span-2 identity-panel">
<div class="panel-title">
<span>student profile</span>
<span class="title-note">${esc(me.Type || "synergia")}</span>
</div>
<div class="panel-body">
<div class="identity-name">${esc(me.FirstName || "")} ${esc(me.LastName || "")}</div>
<div class="identity-sub">
<span class="class-chip">class ${esc(me.Class || "—")}</span>
<span class="dim"> · tutor ${esc(personName(me.Tutor) || "—")}</span>
</div>
<dl class="kv">
<dt>school</dt><dd>${esc(school.Name || "—")}</dd>
<dt>address</dt><dd>${esc(schoolAddress(school))}</dd>
<dt>year</dt><dd>${esc(fmtDate(me.SchoolYearStarts))} → ${esc(fmtDate(me.SchoolYearEnds))}</dd>
<dt>semester</dt><dd>ends ${esc(fmtDate(me.SchoolYearMiddles))}</dd>
</dl>
</div>
</section>
<section class="panel">
<div class="panel-title"><span>lucky number</span><span class="title-note">daily draw</span></div>
<div class="lucky">
<div class="lucky-number">${lucky === null || lucky === undefined ? "–" : esc(lucky)}</div>
<div class="lucky-label">${lucky === null || lucky === undefined ? "not drawn yet" : "today's number"}</div>
</div>
</section>
<section class="panel behaviour-panel">
<div class="panel-title"><span>behaviour</span><span class="title-note">semester 1</span></div>
<div class="behaviour">
<div class="behaviour-grade">${behaviour ? esc(titleCase(behaviour.Grade)) : "—"}</div>
<div class="behaviour-teacher">${behaviour ? esc(personName(behaviour.Teacher) || "—") : "no grade yet"}</div>
</div>
</section>
<section class="panel counters-panel">
<div class="panel-title">
<span>counters</span>
<span class="title-note">click to open a section</span>
</div>
<div class="stat-grid">
<button class="stat" type="button" data-view="grades">
<span class="stat-value">${notif.grades ?? "—"}</span>
<span class="stat-label">grades</span>
</button>
<button class="stat" type="button" data-view="homework">
<span class="stat-value">${notif.exams ?? "—"}</span>
<span class="stat-label">homework</span>
</button>
<button class="stat warn" type="button" data-view="attendance">
<span class="stat-value">${notif.absences ?? "—"}</span>
<span class="stat-label">absences</span>
</button>
<button class="stat" type="button" data-view="freedays">
<span class="stat-value">${notif.conferences ?? "—"}</span>
<span class="stat-label">conferences</span>
</button>
</div>
</section>
<section class="panel span-2 today-panel">
<div class="panel-title">
<span>${isToday ? "today" : "next school day"} · ${esc(dayName)}</span>
<span class="title-note">${esc(fmtDate(dayDate))}${isToday ? "" : " · coming up"}</span>
</div>
<div class="panel-body tight">${lessonsHtml}</div>
</section>
<section class="panel freedays-panel">
<div class="panel-title">
<span>upcoming breaks</span>
<span class="title-note">${upcomingFree.length} ahead</span>
</div>
<div class="panel-body tight">${freeRows}</div>
</section>
</div>
</div>
`;
$("#view").innerHTML = html;
}
function renderDashboardLessons(lessons, isToday) {
if (!lessons || !lessons.length) {
return `<div class="empty">no lessons</div>`;
}
const sorted = lessons
.slice()
.sort((a, b) => (timeToMinutes(a.HourFrom) ?? 9999) - (timeToMinutes(b.HourFrom) ?? 9999));
const now = new Date();
const nowMinutes = now.getHours() * 60 + now.getMinutes();
let currentIndex = -1;
let nextIndex = -1;
if (isToday) {
for (let index = 0; index < sorted.length; index += 1) {
const lesson = sorted[index];
const start = timeToMinutes(lesson.HourFrom);
const end = timeToMinutes(lesson.HourTo);
if (start !== null && end !== null && nowMinutes >= start && nowMinutes <= end) {
currentIndex = index;
} else if (nextIndex === -1 && start !== null && start > nowMinutes && !lesson.isCancelled) {
nextIndex = index;
}
}
}
return `<div class="lesson-list">${sorted
.map((lesson, index) => {
const classes = ["lesson-line"];
if (index === currentIndex) classes.push("now");
if (index === nextIndex) classes.push("next");
if (lesson.isCancelled) classes.push("cancelled");
const marker = index === currentIndex ? ">" : index === nextIndex ? "»" : "·";
const room = [lesson.Classroom, personName(lesson.Teacher)].filter(Boolean).join(" · ");
return `
<div class="${classes.join(" ")}" title="${esc(room)}">
<span class="time">${esc(shortTime(lesson.HourFrom) || "—")}</span>
<span class="marker">${marker}</span>
<span class="subject">${esc(lesson.Subject || "lesson")}</span>
<span class="room">${esc(lesson.Classroom || "")}${lesson.isCancelled ? " [cancelled]" : ""}</span>
</div>`;
})
.join("")}</div>`;
}