/* ========================================================================== 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 ` ${esc(fmtDate(exam.Date))}
${esc(weekdayShort(exam.Date))}
${esc(exam.Lesson || "—")} ${esc(exam.Type || "—")} ${esc(timeRange(exam.StartTime, exam.EndTime))} ${esc(personName(exam.AddedBy) || "—")} ${esc(content)} `; }) .join("") : ""; $("#view").innerHTML = `
${upcoming} upcoming · ${exams.length} total click a row for the full description
homework & testssorted by date
${ exams.length ? `
${rows}
datesubjecttypetimeteacherdescription
` : `
no homework or tests
` }
`; } function openExamDetail(index) { const exam = state.exams[index]; if (!exam) return; const body = `
subject
${esc(exam.Lesson || "—")}
type
${esc(exam.Type || "—")}
date
${esc(fmtDate(exam.Date))} · ${esc(weekdayShort(exam.Date))}
time
${esc(timeRange(exam.StartTime, exam.EndTime))}
teacher
${esc(personName(exam.AddedBy) || "—")}
added
${esc(fmtDate(exam.AddDate))}
description

${esc(exam.Content || "—")}

`; 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 ` ${esc(fmtDate(item.Date))} ${esc(item.Lesson || "—")} ${esc(item.Short || "?")} ${esc(item.Type || "")} ${esc(personName(item.AddedBy) || "—")} ${item.isPresence ? "present" : "absent"} `; }) .join("") : ""; $("#view").innerHTML = `
${total}
entries
${Math.max(0, total - absent)}
present
${absent}
absences
${late}
late · ${excused} excused
attendance register${total} entries
${ items.length ? `
${rows}
datesubjecttypeteacherstatus
` : `
no attendance entries
` }
`; } /* ------------------------------------------------------------------ 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 = `
inbox ${state.messages.length} messages
`; renderMessageList(); if (state.messageIndex >= 0) { loadMessage(state.messageIndex); } else { $("#message-reader").innerHTML = `
reader
inbox empty
`; } } function renderMessageList() { const list = $("#message-list"); if (!list) return; if (!state.messages.length) { list.innerHTML = `
no messages
`; return; } list.innerHTML = state.messages .map( (message, index) => ` ` ) .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 = `
readerloading…
[..] opening message…
`; 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 = `
readererror
${esc(error.message)}
`; 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 ` [↓] ${esc(attachment.name || link || "attachment")} download `; }) .join(""); const readState = detail.read ? `read: ${esc(detail.read)}` : ""; reader.innerHTML = `
reader ${index + 1} / ${state.messages.length}

${esc(detail.subject || "(no subject)")}

from ${esc(detail.from || "unknown")} ${esc(detail.date || "")} ${readState}
${detail.content || `
message body is empty
`}
${ attachments ? `
attachments
${attachments}
` : "" } `; 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 `
${esc(item.Name || "free day")}
${esc(fmtDate(item.DateFrom))} → ${esc(fmtDate(item.DateTo))}
${esc(countdownLabel(item.DateFrom))}
`; }) .join("") : `
no free days
`; 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 `
${esc(teacher)}
${esc(fmtDate(item.DateFrom))} → ${esc(fmtDate(item.DateTo))}${times}
${esc(countdownLabel(item.DateFrom))}
`; }) .join("") : `
no teacher absences
`; const confRows = conf.length ? conf .map( (item) => `
${esc(item.Name || "conference")}
${esc(item.Topic || "")}
${esc(fmtDate(item.Date))} · ${esc(shortTime(item.Time))}${item.Room ? ` · room ${esc(item.Room)}` : ""}
` ) .join("") : `
no scheduled conferences
`; $("#view").innerHTML = `
school free days${school.length}
${schoolRows}
teacher free days${teachers.length}
${teacherRows}
parent-teacher conferences${conf.length}
${confRows}
`; }