168 lines
5.6 KiB
JavaScript
168 lines
5.6 KiB
JavaScript
/* ==========================================================================
|
|
librus-tui frontend - grades view
|
|
========================================================================== */
|
|
|
|
"use strict";
|
|
|
|
async function renderGrades(token) {
|
|
const data = await api("/api/grades");
|
|
if (token !== state.loadToken) return;
|
|
|
|
const subjects = data.subjects || {};
|
|
const sorted = Object.entries(subjects).sort((a, b) =>
|
|
String(a[0]).localeCompare(String(b[0]), "pl")
|
|
);
|
|
|
|
state.gradesFlat = [];
|
|
state.gradeGroups = sorted.map(([subject, grades]) => {
|
|
const items = (grades || []).map((grade) => {
|
|
const item = { ...grade, _subject: subject, _index: state.gradesFlat.length };
|
|
state.gradesFlat.push(item);
|
|
return item;
|
|
});
|
|
return { subject, grades: items };
|
|
});
|
|
|
|
const overall = averageLabel(state.gradesFlat);
|
|
const gradeCount = state.gradesFlat.length;
|
|
|
|
$("#view").innerHTML = `
|
|
<div class="view-inner">
|
|
<div class="toolbar">
|
|
<span>filter</span>
|
|
<input class="tool-input" id="grade-filter" type="text"
|
|
placeholder="subject or category…" spellcheck="false" autocomplete="off">
|
|
<button class="btn btn-sm" type="button" data-action="grades-clear">[ clear ]</button>
|
|
<span class="grow"></span>
|
|
<span class="grades-summary">
|
|
<span>average <b>${esc(overall)}</b></span>
|
|
<span><b>${gradeCount}</b> grades</span>
|
|
<span><b>${state.gradeGroups.length}</b> subjects</span>
|
|
</span>
|
|
</div>
|
|
<div class="grades-grid" id="grades-list"></div>
|
|
<div class="toolbar" style="margin-top:12px">
|
|
<div class="legend">
|
|
<span><span class="swatch green"></span>5 / 6</span>
|
|
<span><span class="swatch cyan"></span>4</span>
|
|
<span><span class="swatch amber"></span>3</span>
|
|
<span><span class="swatch red"></span>1 / 2</span>
|
|
<span class="dim">dashed chip = final · outlined = semester</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const input = $("#grade-filter");
|
|
if (input) {
|
|
input.value = state.gradeFilter;
|
|
input.addEventListener("input", () => {
|
|
state.gradeFilter = input.value;
|
|
renderGradesList();
|
|
});
|
|
}
|
|
|
|
renderGradesList();
|
|
}
|
|
|
|
function gradeWeightLabel(weight) {
|
|
const raw = String(weight ?? "").toLowerCase();
|
|
if (!raw || raw === "none" || raw === "null") return "";
|
|
return `w${raw}`;
|
|
}
|
|
|
|
function renderGradesList() {
|
|
const container = $("#grades-list");
|
|
if (!container) return;
|
|
|
|
const query = state.gradeFilter.trim().toLowerCase();
|
|
const groups = state.gradeGroups.filter((group) => {
|
|
if (!query) return true;
|
|
if (group.subject.toLowerCase().includes(query)) return true;
|
|
return group.grades.some((grade) =>
|
|
`${grade.Category || ""} ${grade.Grade || ""}`.toLowerCase().includes(query)
|
|
);
|
|
});
|
|
|
|
if (!groups.length) {
|
|
container.innerHTML = `<div class="empty" style="grid-column:1/-1">no matching grades</div>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = groups
|
|
.map((group) => {
|
|
const average = averageLabel(group.grades);
|
|
const chips = group.grades.length
|
|
? group.grades
|
|
.map((grade) => {
|
|
const weight = gradeWeightLabel(grade.Weight);
|
|
const classes = ["grade-chip", gradeClass(grade.Grade)];
|
|
if (grade.isFinal) classes.push("final");
|
|
if (grade.isSemester) classes.push("semester");
|
|
const label = `${group.subject} · ${grade.Grade} · ${grade.Category || "grade"} · ${fmtDate(grade.AddDate)}`;
|
|
return `
|
|
<button class="${classes.join(" ")}" type="button" data-grade="${grade._index}" title="${esc(label)}">
|
|
${weight ? `<span class="g-weight">${esc(weight)}</span>` : ""}
|
|
<span class="g">${esc(grade.Grade)}</span>
|
|
<span class="g-meta">${esc(grade.Category || "—")}</span>
|
|
</button>`;
|
|
})
|
|
.join("")
|
|
: `<span class="dim" style="font-size:10.5px">— no grades yet —</span>`;
|
|
|
|
return `
|
|
<section class="panel subject-card">
|
|
<div class="panel-title">
|
|
<span>${esc(group.subject)}</span>
|
|
<span class="subject-average">avg ${esc(average)}</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="grade-chips">${chips}</div>
|
|
</div>
|
|
</section>`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function focusGradeFilter() {
|
|
const input = $("#grade-filter");
|
|
if (input) {
|
|
input.focus();
|
|
input.select();
|
|
}
|
|
}
|
|
|
|
function openGradeDetail(index) {
|
|
const grade = state.gradesFlat[index];
|
|
if (!grade) return;
|
|
|
|
const flags = [];
|
|
if (grade.isFinal) flags.push("final");
|
|
if (grade.isSemester) flags.push("semester");
|
|
if (grade.Weight && String(grade.Weight).toLowerCase() !== "none") {
|
|
flags.push(`weight ${grade.Weight}`);
|
|
}
|
|
|
|
const comment =
|
|
grade.Comment && grade.Comment !== "No comment"
|
|
? `<div class="detail-comment">${esc(grade.Comment)}</div>`
|
|
: "";
|
|
|
|
const body = `
|
|
<dl class="detail-grid">
|
|
<dt>subject</dt><dd>${esc(grade._subject)}</dd>
|
|
<dt>grade</dt><dd>${esc(grade.Grade)}</dd>
|
|
<dt>category</dt><dd>${esc(grade.Category || "—")}</dd>
|
|
<dt>weight</dt><dd>${esc(grade.Weight ?? "—")}</dd>
|
|
<dt>added</dt><dd>${esc(fmtDate(grade.AddDate))}</dd>
|
|
<dt>teacher</dt><dd>${esc(personName(grade.AddedBy) || "—")}</dd>
|
|
<dt>semester</dt><dd>${esc(grade.Semester ?? "—")}${flags.length ? ` · ${esc(flags.join(" · "))}` : ""}</dd>
|
|
</dl>
|
|
${comment}
|
|
`;
|
|
|
|
openModal(`grade ${grade.Grade} · ${grade._subject}`, body, [
|
|
{ label: "[ close ]", close: true },
|
|
]);
|
|
}
|