221 lines
8.5 KiB
JavaScript
221 lines
8.5 KiB
JavaScript
/* ==========================================================================
|
||
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>`;
|
||
}
|