Crank dat!

This commit is contained in:
purplebored committed 2026-11-08 19:28:03 +01:00
commit fe13a4d989
17 files changed
+6624

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
/* ==========================================================================
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>`;
}