File size: 11,068 Bytes
b6f5bc4 a7ffb22 b6f5bc4 a7ffb22 b6f5bc4 a7ffb22 b6f5bc4 a7ffb22 c5754b1 a7ffb22 c5754b1 a7ffb22 0815e01 b6f5bc4 a7ffb22 0815e01 b6f5bc4 0815e01 a7ffb22 0815e01 a7ffb22 0815e01 a7ffb22 b6f5bc4 a7ffb22 b6f5bc4 a7ffb22 b6f5bc4 a7ffb22 b6f5bc4 a7ffb22 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | // The manager's screens: every class (make, change, delete), every student, the curriculum and the subjects. A manager sees no learning data.
import { api } from "./api.js";
import { esc } from "./math.js";
import * as subjectsTab from "./subjects_tab.js";
let draw, tab = "classes", editing = null, draft = null, flash = { message: "", error: "" }, lessonId = null;
// draft: what was typed in the editor when a save was refused, so the form keeps it and the person can fix one thing
const note = () => `${flash.error && tab !== "edit" ? `<p class="error" role="alert">${esc(flash.error)}</p>` : ""}${flash.message ? `<p role="status" id="msg">${esc(flash.message)}</p>` : ""}`;
const tabs = () => `<div class="subtabs" id="mgr-tabs" role="group" aria-label="诪谞讛诇 讻讬转讜转">
${[["classes", "讻讬转讜转"], ["edit", editing ? "注专讬讻转 讻讬转讛" : "讻讬转讛 讞讚砖讛"], ["students", "转诇诪讬讚讬诐"], ["curriculum", "转讻谞讬转 诇讬诪讜讚讬诐"], ["subjects", "诪拽爪讜注讜转"]].map(([id, label]) =>
`<button type="button" data-tab="${id}" aria-pressed="${tab === id || (tab === "lesson" && id === "subjects")}">${label}</button>`).join("")}</div>`;
async function render() {
let body, failed = false;
try { // a tab whose data cannot be read says so, instead of the click doing nothing
if (tab === "edit") subjects = (await api("/api/subjects")).subjects;
body = tab === "classes" ? await classesView() : tab === "students" ? await studentsView() : tab === "curriculum" ? await curriculumView()
: tab === "subjects" ? await subjectsTab.view() : tab === "lesson" ? `<div id="lesson-slot"></div>` : editorView();
} catch (e) {
failed = true;
body = `<p class="error" role="alert" id="tab-error">诇讗 讛爪诇讞谞讜 诇驻转讜讞 讗转 讛诇砖讜谞讬转: ${esc(e.message)}</p>`;
}
draw(`<section id="mgr"><h2>诪谞讛诇 讻讬转讜转</h2>${tabs()}${note()}${body}</section>`);
document.getElementById("mgr-tabs").addEventListener("click", event => {
const next = event.target.closest("[data-tab]")?.dataset.tab;
if (!next) return;
if (next === "edit" && tab !== "edit") editing = null;
draft = null;
go(next);
});
if (failed) { flash = { message: "", error: "" }; return; }
if (tab === "classes") wireClasses();
if (tab === "curriculum") wireCurriculum();
const openLesson = id => { lessonId = id; go("lesson"); };
if (tab === "subjects") subjectsTab.wire(go, (message, error) => { flash = { message, error }; }, openLesson);
if (tab === "lesson") (await import("./lesson_editor.js")).open(document.getElementById("lesson-slot"), lessonId, () => go("subjects"));
if (tab === "edit") wireEditor();
if (tab === "edit" && flash.error) { // the form is long: bring the reason into view, next to the buttons that were pressed
const reason = document.getElementById("form-error");
reason.scrollIntoView({ block: "center" });
reason.focus();
}
flash = { message: "", error: "" };
}
/** The curriculum: the date by which each lesson should be completed, one for the whole school. */
async function curriculumView() {
const lessons = await api("/api/curriculum");
return `<form class="card" id="curriculum-form"><h3>转讻谞讬转 诇讬诪讜讚讬诐</h3>
<p class="muted">讛转讗专讬讱 砖注讚 讗诇讬讜 讻诇 砖讬注讜专 讗诪讜专 诇讛住转讬讬诐. 转诇诪讬讚 砖诇讗 住讬讬诐 砖讬注讜专 讗讞专讬 讛转讗专讬讱 专讜讗讛 讗讜转讜 诪住讜诪谉 讘驻讬讙讜专. 讗驻砖专 诇讛砖讗讬专 转讗专讬讱 专讬拽.</p>
${[...new Set(lessons.map(l => l.subject?.id || "math"))].map(sid => {
const mine = lessons.filter(l => (l.subject?.id || "math") === sid);
return `<section class="curriculum-subject" data-subject="${esc(sid)}"><h4>${esc(mine[0].subject?.name || "诪转诪讟讬拽讛")}</h4>
<ul class="plain curriculum">${mine.map(l => `<li><label for="due-${esc(l.id)}">${esc(l.name)} <span class="muted">${esc(l.month)}</span></label>
<input id="due-${esc(l.id)}" type="date" data-lesson="${esc(l.id)}" value="${esc(l.due_date || "")}"></li>`).join("")}</ul></section>`;
}).join("")}
<p><button class="primary" type="submit" id="save-curriculum">砖诪讬专讛</button></p></form>`;
}
function wireCurriculum() {
document.getElementById("curriculum-form").addEventListener("submit", async event => {
event.preventDefault();
const dates = Object.fromEntries([...document.querySelectorAll("#curriculum-form [data-lesson]")].map(i => [i.dataset.lesson, i.value || null]));
try { await api("/api/curriculum", { method: "PUT", body: { dates } }); flash = { message: "讛转讻谞讬转 谞砖诪专讛.", error: "" }; }
catch (e) { flash = { message: "", error: e.message }; }
go("curriculum");
});
}
const go = next => { tab = next; return render(); };
async function classesView() {
const classes = await api("/api/classes");
return `<div class="hrow" style="margin-bottom:10px"><button class="primary" type="button" id="new-class">+ 讻讬转讛 讞讚砖讛</button>
<span class="muted">${classes.length} 讻讬转讜转</span></div>
${classes.length ? `<div class="cards" id="classes">${classes.map(c => `<article class="card" data-id="${c.id}">
<div class="hrow"><h3 class="grow">${esc(c.name)}</h3><button type="button" data-edit="${c.id}">注专讬讻讛</button><button type="button" class="danger" data-delete="${c.id}">诪讞讬拽讛</button></div>
<p style="margin:0;font-size:14px">诪讜专讛: <span dir="ltr">${esc(c.teacher_email || "注讜讚 诇讗 谞讘讞专")}</span><br>
${c.students} 转诇诪讬讚讬诐 路 AI: <b>${c.ai_enabled ? "驻讜注诇" : "讻讘讜讬"}</b> 路 ${c.ai_daily_checks} 讘讚讬拽讜转 讘讬讜诐</p></article>`).join("")}</div>`
: `<section class="card"><p class="muted">注讜讚 讗讬谉 讻讬转讜转. 驻转讞讜 讻讬转讛 专讗砖讜谞讛 讜讛讜住讬驻讜 讗转 讛转诇诪讬讚讬诐.</p></section>`}`;
}
function wireClasses() {
document.getElementById("new-class").addEventListener("click", () => { editing = null; draft = null; go("edit"); });
document.getElementById("classes")?.addEventListener("click", async event => {
const edit = event.target.closest("[data-edit]")?.dataset.edit, del = event.target.closest("[data-delete]")?.dataset.delete;
if (edit) { editing = await api(`/api/classes/${edit}`); draft = null; return go("edit"); }
if (del) {
const card = event.target.closest("article");
if (!confirm(`诇诪讞讜拽 讗转 讛讻讬转讛 "${card.querySelector("h3").textContent}"? 讛讛转拽讚诪讜转 砖诇 讛转诇诪讬讚讬诐 谞砖诪专转.`)) return;
try { await api(`/api/classes/${del}`, { method: "DELETE" }); flash.message = "讛讻讬转讛 谞诪讞拽讛."; }
catch (e) { flash.error = e.message; }
go("classes");
}
});
}
let subjects = []; // every subject, for the class editor's choice (loaded when the editor opens)
function editorView() {
const c = editing, d = draft; // a refused save keeps what was typed
const value = (typed, saved, fallback = "") => d ? (typed ?? fallback) : (saved ?? fallback);
const studies = d ? d.subjects : (c?.subjects || ["math"]);
return `<form class="card" id="class-form"><h3>${c ? `注专讬讻转 ${esc(c.name)}` : "讻讬转讛 讞讚砖讛"}</h3>
<label for="c-name">砖诐 讛讻讬转讛</label><input id="c-name" required maxlength="80" autocomplete="off" value="${esc(value(d?.name, c?.name))}">
<label for="c-teacher">诪讜专讛 讛讻讬转讛 (讻转讜讘转 讗讞转; 诇讗 讗讞讚 讛转诇诪讬讚讬诐 砖诇 讛讻讬转讛)</label>
<input id="c-teacher" type="text" dir="ltr" autocomplete="off" value="${esc(value(d?.teacher_email, c?.teacher_email))}">
<label for="c-students">转诇诪讬讚讬诐 (讻转讜讘转 讘讻诇 砖讜专讛; 转诇诪讬讚 讬讻讜诇 诇讛讬讜转 讘讻诪讛 讻讬转讜转)</label>
<textarea id="c-students" dir="ltr" spellcheck="false">${esc(d ? d.students : (c?.student_emails || []).join("\n"))}</textarea>
<div class="hrow" style="margin-top:12px"><label class="switch"><input id="c-ai" type="checkbox" ${value(d?.ai_enabled, c?.ai_enabled, false) ? "checked" : ""}> AI 诇讻讬转讛</label>
<label for="c-budget" style="margin:0">讘讚讬拽讜转 AI 讘讬讜诐</label><input id="c-budget" type="number" min="0" max="10000" style="width:110px" value="${value(d?.ai_daily_checks, c?.ai_daily_checks, 150)}"></div>
<p class="muted">讛志AI 讻讘讜讬 注讚 砖讘讬转 讛住驻专 诪讗砖专 砖讛诪砖驻讞讜转 诪住讻讬诪讜转. 专拽 讗讞专讬 讝讛 诪驻注讬诇讬诐 讗讜转讜.</p>
<fieldset class="c-subjects" id="c-subjects"><legend>诪拽爪讜注讜转 砖讛讻讬转讛 诇讜诪讚转</legend>
${subjects.map(s => `<label class="switch"><input type="checkbox" value="${esc(s.id)}" ${studies.includes(s.id) ? "checked" : ""}> <span aria-hidden="true">${esc(s.icon)}</span> ${esc(s.name)}</label>`).join("")}
<p class="muted">讛讻讬转讛 诇讜诪讚转 讗转 讻诇 讛砖讬注讜专讬诐 砖驻讜专住诪讜 讘诪拽爪讜注讜转 砖谞讘讞专讜. 讛住专转 诪拽爪讜注 诇讗 诪讜讞拽转 讗转 讛讛转拽讚诪讜转 砖诇 讛转诇诪讬讚讬诐.</p></fieldset>
${flash.error ? `<p class="error" role="alert" id="form-error" tabindex="-1"><b>诇讗 谞砖诪专:</b> ${esc(flash.error)}</p>` : ""}
<div class="hrow"><button class="primary" type="submit" id="c-save">${c ? "砖诪讬专转 讛砖讬谞讜讬讬诐" : "驻转讬讞转 讛讻讬转讛"}</button><button type="button" id="c-cancel">讘讬讟讜诇</button></div></form>`;
}
function wireEditor() {
document.getElementById("c-cancel").addEventListener("click", () => { editing = null; draft = null; go("classes"); });
document.getElementById("class-form").addEventListener("submit", async event => {
event.preventDefault();
const body = {
name: document.getElementById("c-name").value,
teacher_email: document.getElementById("c-teacher").value.trim() || null,
students: document.getElementById("c-students").value,
ai_enabled: document.getElementById("c-ai").checked,
ai_daily_checks: Number(document.getElementById("c-budget").value),
subjects: [...document.querySelectorAll("#c-subjects input:checked")].map(i => i.value),
};
try {
const saved = editing ? await api(`/api/classes/${editing.id}`, { method: "PUT", body }) : await api("/api/classes", { method: "POST", body });
flash.message = `${editing ? "讛砖讬谞讜讬讬诐 谞砖诪专讜" : "讛讻讬转讛 谞驻转讞讛"}: ${saved.name}.${saved.invalid?.length ? ` 诇讗 谞专讗讛 讻诪讜 诪讬讬诇: ${saved.invalid.join(", ")}` : ""}`;
editing = null; draft = null;
go("classes");
} catch (e) { draft = body; flash.error = e.message; go("edit"); }
});
}
async function studentsView() {
const students = await api("/api/students");
return `<div class="tablewrap" id="students-list"><table class="data"><thead><tr><th>转诇诪讬讚</th><th>讻讬转讜转</th></tr></thead>
<tbody>${students.length ? students.map(s => `<tr><td dir="ltr">${esc(s.email)}</td><td>${s.classes.map(esc).join(" 路 ")}</td></tr>`).join("")
: `<tr><td colspan="2" class="muted">注讚讬讬谉 诇讗 谞讜住驻讜 转诇诪讬讚讬诐 诇讻讬转讜转.</td></tr>`}</tbody></table></div>`;
}
export async function show(me, view) {
draw = view;
tab = "classes"; editing = null;
await render();
}
|