Files
librustui/static/js/26-more-views.js
T
2026-11-08 19:28:03 +01:00

386 lines
14 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 - homework, attendance, messages, free days
========================================================================== */
"use strict";
/* ------------------------------------------------------------------ homework */
async function renderHomework(token) {
const data = await api("/api/exams");
if (token !== state.loadToken) return;
const exams = (data.exams || []).slice().sort((a, b) => {
const left = parseDate(a.Date);
const right = parseDate(b.Date);
return (left ? left.getTime() : Number.MAX_SAFE_INTEGER) -
(right ? right.getTime() : Number.MAX_SAFE_INTEGER);
});
state.exams = exams;
const upcoming = exams.filter((exam) => (daysUntil(exam.Date) ?? -1) >= 0).length;
const rows = exams.length
? exams
.map((exam, index) => {
const distance = daysUntil(exam.Date);
const dateClass = distance === null ? "" : distance < 0 ? "soft" : distance <= 1 ? "urgent" : distance <= 3 ? "soon" : "";
const typeClass = examTagClass(exam.Type);
const content = exam.Content ? truncate(exam.Content, 84) : "—";
return `
<tr data-exam="${index}" title="click for details">
<td class="num">
<span class="date-badge ${dateClass}">${esc(fmtDate(exam.Date))}</span>
<div class="soft">${esc(weekdayShort(exam.Date))}</div>
</td>
<td class="strong">${esc(exam.Lesson || "—")}</td>
<td><span class="tag ${typeClass}">${esc(exam.Type || "—")}</span></td>
<td class="num">${esc(timeRange(exam.StartTime, exam.EndTime))}</td>
<td class="soft">${esc(personName(exam.AddedBy) || "—")}</td>
<td class="content-cell">${esc(content)}</td>
</tr>`;
})
.join("")
: "";
$("#view").innerHTML = `
<div class="view-inner">
<div class="toolbar">
<span><b>${upcoming}</b> upcoming</span>
<span class="dim">·</span>
<span><b>${exams.length}</b> total</span>
<span class="grow"></span>
<span class="dim">click a row for the full description</span>
</div>
<section class="panel">
<div class="panel-title"><span>homework &amp; tests</span><span class="title-note">sorted by date</span></div>
${
exams.length
? `<div class="table-wrap"><table class="data-table">
<thead>
<tr><th>date</th><th>subject</th><th>type</th><th>time</th><th>teacher</th><th>description</th></tr>
</thead>
<tbody>${rows}</tbody>
</table></div>`
: `<div class="empty">no homework or tests</div>`
}
</section>
</div>
`;
}
function openExamDetail(index) {
const exam = state.exams[index];
if (!exam) return;
const body = `
<dl class="detail-grid">
<dt>subject</dt><dd>${esc(exam.Lesson || "—")}</dd>
<dt>type</dt><dd>${esc(exam.Type || "—")}</dd>
<dt>date</dt><dd>${esc(fmtDate(exam.Date))} · ${esc(weekdayShort(exam.Date))}</dd>
<dt>time</dt><dd>${esc(timeRange(exam.StartTime, exam.EndTime))}</dd>
<dt>teacher</dt><dd>${esc(personName(exam.AddedBy) || "—")}</dd>
<dt>added</dt><dd>${esc(fmtDate(exam.AddDate))}</dd>
</dl>
<div class="help-section">description</div>
<p style="white-space:pre-wrap">${esc(exam.Content || "—")}</p>
`;
openModal(`homework · ${exam.Lesson || "—"}`, body, [{ label: "[ close ]", close: true }]);
}
/* ---------------------------------------------------------------- attendance */
async function renderAttendance(token) {
const data = await api("/api/attendances");
if (token !== state.loadToken) return;
const items = (data.attendances || []).slice().sort((a, b) => {
const left = parseDate(a.Date);
const right = parseDate(b.Date);
return (right ? right.getTime() : 0) - (left ? left.getTime() : 0);
});
const total = items.length;
const absent = items.filter((item) => !item.isPresence).length;
const late = items.filter((item) => /późn|spoźn|sp\./i.test(`${item.Type} ${item.Short}`)).length;
const excused = items.filter((item) => /zwoln|zw\./i.test(`${item.Type} ${item.Short}`)).length;
const rows = items.length
? items
.map((item) => {
const isLate = /późn|spoźn|sp\./i.test(`${item.Type} ${item.Short}`);
const tagClass = !item.isPresence ? "tag-red" : isLate ? "tag-amber" : "tag-green";
return `
<tr>
<td class="num">${esc(fmtDate(item.Date))}</td>
<td class="strong">${esc(item.Lesson || "—")}</td>
<td><span class="tag ${tagClass}">${esc(item.Short || "?")} ${esc(item.Type || "")}</span></td>
<td class="soft">${esc(personName(item.AddedBy) || "—")}</td>
<td class="num presence ${item.isPresence ? "yes" : "no"}">${item.isPresence ? "present" : "absent"}</td>
</tr>`;
})
.join("")
: "";
$("#view").innerHTML = `
<div class="view-inner">
<div class="att-summary">
<div class="att-tile"><div class="value">${total}</div><div class="label">entries</div></div>
<div class="att-tile"><div class="value">${Math.max(0, total - absent)}</div><div class="label">present</div></div>
<div class="att-tile absent"><div class="value">${absent}</div><div class="label">absences</div></div>
<div class="att-tile late"><div class="value">${late}</div><div class="label">late · ${excused} excused</div></div>
</div>
<section class="panel">
<div class="panel-title"><span>attendance register</span><span class="title-note">${total} entries</span></div>
${
items.length
? `<div class="table-wrap"><table class="data-table">
<thead><tr><th>date</th><th>subject</th><th>type</th><th>teacher</th><th>status</th></tr></thead>
<tbody>${rows}</tbody>
</table></div>`
: `<div class="empty">no attendance entries</div>`
}
</section>
</div>
`;
}
/* ------------------------------------------------------------------ messages */
async function renderMessages(token) {
const data = await api("/api/messages");
if (token !== state.loadToken) return;
state.messages = data.messages || [];
state.messageCache.clear();
state.messageIndex = state.messages.length ? 0 : -1;
$("#view").innerHTML = `
<div class="view-inner">
<div class="messages-layout">
<section class="panel">
<div class="panel-title">
<span>inbox</span>
<span class="title-note">${state.messages.length} messages</span>
</div>
<div class="message-list" id="message-list"></div>
</section>
<div class="panel" id="message-reader"></div>
</div>
</div>
`;
renderMessageList();
if (state.messageIndex >= 0) {
loadMessage(state.messageIndex);
} else {
$("#message-reader").innerHTML = `
<div class="panel-title"><span>reader</span></div>
<div class="empty">inbox empty</div>`;
}
}
function renderMessageList() {
const list = $("#message-list");
if (!list) return;
if (!state.messages.length) {
list.innerHTML = `<div class="empty">no messages</div>`;
return;
}
list.innerHTML = state.messages
.map(
(message, index) => `
<button class="message-item ${index === state.messageIndex ? "active" : ""}" type="button" data-message="${index}">
<div class="msg-top">
<span class="msg-from">${esc(message.from || "unknown")}</span>
<span class="msg-date">${esc(message.date || "")}</span>
</div>
<div class="msg-subject">${esc(message.subject || "(no subject)")}</div>
</button>`
)
.join("");
}
function highlightMessageList() {
$$("#message-list .message-item").forEach((element, index) => {
element.classList.toggle("active", index === state.messageIndex);
});
const active = $("#message-list .message-item.active");
if (active) active.scrollIntoView({ block: "nearest" });
}
function selectMessage(index) {
if (!state.messages.length) return;
const next = Math.max(0, Math.min(state.messages.length - 1, index));
if (next === state.messageIndex) return;
state.messageIndex = next;
highlightMessageList();
loadMessage(next);
}
function moveMessage(delta) {
if (!state.messages.length) return;
selectMessage(state.messageIndex + delta);
}
async function loadMessage(index, force = false) {
const message = state.messages[index];
const reader = $("#message-reader");
if (!message || !reader) return;
const token = ++state.messageToken;
const link = message.link;
if (!force && state.messageCache.has(link)) {
renderMessageDetail(state.messageCache.get(link), index);
return;
}
reader.innerHTML = `
<div class="panel-title"><span>reader</span><span class="title-note">loading…</span></div>
<div class="loading">[..] opening message…</div>`;
try {
const detail = await api(`/api/message?link=${encodeURIComponent(link)}`);
if (token !== state.messageToken) return;
state.messageCache.set(link, detail);
renderMessageDetail(detail, index);
setStatus(`[ok] message ${index + 1}/${state.messages.length}`, "ok");
} catch (error) {
if (token !== state.messageToken) return;
reader.innerHTML = `
<div class="panel-title"><span>reader</span><span class="title-note">error</span></div>
<div class="error-message">${esc(error.message)}</div>`;
toast(error.message, "err");
}
}
function renderMessageDetail(detail, index) {
const reader = $("#message-reader");
if (!reader) return;
const attachments = (detail.attachments || [])
.map((attachment) => {
const link = attachment.nice || attachment.source || "";
return `
<a class="attachment" href="/api/download?link=${encodeURIComponent(link)}" download>
<span>[&darr;]</span>
<span>${esc(attachment.name || link || "attachment")}</span>
<span class="size-hint">download</span>
</a>`;
})
.join("");
const readState = detail.read
? `<span class="tag tag-dim">read: ${esc(detail.read)}</span>`
: "";
reader.innerHTML = `
<div class="panel-title">
<span>reader</span>
<span class="title-note">${index + 1} / ${state.messages.length}</span>
</div>
<div class="reader-head">
<h2 class="reader-subject">${esc(detail.subject || "(no subject)")}</h2>
<div class="reader-meta">
<span>from <b>${esc(detail.from || "unknown")}</b></span>
<span>${esc(detail.date || "")}</span>
${readState}
</div>
</div>
<div class="message-body">${detail.content || `<div class="empty">message body is empty</div>`}</div>
${
attachments
? `<div class="attachments"><div class="attachments-title">attachments</div>${attachments}</div>`
: ""
}
`;
reader.scrollTop = 0;
}
/* ----------------------------------------------------------------- free days */
async function renderFreedays(token) {
const [free, conferences] = await Promise.all([
api("/api/freedays"),
api("/api/conferences").catch(() => null),
]);
if (token !== state.loadToken) return;
const school = (free.school || [])
.slice()
.sort((a, b) => (parseDate(a.DateFrom) || 0) - (parseDate(b.DateFrom) || 0));
const teachers = (free.teachers || [])
.slice()
.sort((a, b) => (parseDate(a.DateFrom) || 0) - (parseDate(b.DateFrom) || 0));
const conf = (conferences && conferences.conferences) || [];
const schoolRows = school.length
? school
.map((item) => {
const distance = daysUntil(item.DateFrom);
const past = distance !== null && distance < 0;
return `
<div class="free-item" ${past ? 'style="opacity:.55"' : ""}>
<div>
<div class="free-name">${esc(item.Name || "free day")}</div>
<div class="conf-meta">${esc(fmtDate(item.DateFrom))} → ${esc(fmtDate(item.DateTo))}</div>
</div>
<span class="countdown ${countdownClass(item.DateFrom)}">${esc(countdownLabel(item.DateFrom))}</span>
</div>`;
})
.join("")
: `<div class="empty">no free days</div>`;
const teacherRows = teachers.length
? teachers
.map((item) => {
const teacher = personName(item.Teacher) || "teacher";
const times = item.TimeFrom
? ` · ${esc(shortTime(item.TimeFrom))}–${esc(shortTime(item.TimeTo))}`
: "";
return `
<div class="free-item">
<div>
<div class="free-name">${esc(teacher)}</div>
<div class="conf-meta">${esc(fmtDate(item.DateFrom))} → ${esc(fmtDate(item.DateTo))}${times}</div>
</div>
<span class="countdown ${countdownClass(item.DateFrom)}">${esc(countdownLabel(item.DateFrom))}</span>
</div>`;
})
.join("")
: `<div class="empty">no teacher absences</div>`;
const confRows = conf.length
? conf
.map(
(item) => `
<div class="conf-item">
<div class="conf-name">${esc(item.Name || "conference")}</div>
<div class="conf-meta">${esc(item.Topic || "")}</div>
<div class="conf-meta">${esc(fmtDate(item.Date))} · ${esc(shortTime(item.Time))}${item.Room ? ` · room ${esc(item.Room)}` : ""}</div>
</div>`
)
.join("")
: `<div class="empty">no scheduled conferences</div>`;
$("#view").innerHTML = `
<div class="view-inner">
<div class="split-2">
<section class="panel">
<div class="panel-title"><span>school free days</span><span class="title-note">${school.length}</span></div>
<div class="panel-body tight">${schoolRows}</div>
</section>
<section class="panel">
<div class="panel-title"><span>teacher free days</span><span class="title-note">${teachers.length}</span></div>
<div class="panel-body tight">${teacherRows}</div>
</section>
<section class="panel span-2">
<div class="panel-title"><span>parent-teacher conferences</span><span class="title-note">${conf.length}</span></div>
<div class="panel-body tight">${confRows}</div>
</section>
</div>
</div>
`;
}