diff --git a/public/admin.html b/public/admin.html index 4e34b1a..6f7c421 100644 --- a/public/admin.html +++ b/public/admin.html @@ -283,6 +283,8 @@
+

Spelling Test History

+
diff --git a/public/admin.js b/public/admin.js index 2a27b08..bbc7538 100644 --- a/public/admin.js +++ b/public/admin.js @@ -1211,6 +1211,37 @@ async function loadGradebookPanel() { const courses = await api('/api/admin/courses'); $('#gradebook-course').innerHTML = courses.map((c) => ``).join(''); if (courses.length) renderGradebook(courses[0].id); + loadSpellingTests(); +} + +async function loadSpellingTests() { + const { students } = await api('/api/spelling-tests'); + const container = $('#spelling-tests-table'); + const active = students.filter((s) => s.tests.length > 0); + if (!active.length) { + container.innerHTML = `

No spelling tests recorded yet.

`; + return; + } + container.innerHTML = active + .map((s) => { + const rows = s.tests + .map((t) => { + const pct = Math.round((t.score / t.total) * 100); + return ` + ${new Date(t.at + 'Z').toLocaleDateString()} + ${esc(t.list)} + ${t.score}/${t.total} (${pct}%) + + `; + }) + .join(''); + return `

${esc(s.emoji)} ${esc(s.name)}

+ ${rows}
DateListScore
`; + }) + .join(''); + container.querySelectorAll('[data-print]').forEach((btn) => + btn.addEventListener('click', () => printSpellingReport(btn.dataset.print)) + ); } $('#gradebook-course').addEventListener('change', () => renderGradebook($('#gradebook-course').value)); $('#gradebook-csv-btn').addEventListener('click', () => { @@ -1281,7 +1312,6 @@ async function loadSpelling() { cachedStudents = overview.students; renderAssignRows(overview.students); renderLists(lists); - renderResults(overview.students); } function renderAssignRows(students) { @@ -1546,28 +1576,6 @@ $('#list-bulk-clear').addEventListener('click', () => { $('#list-bulk-status').textContent = ''; }); -function renderResults(students) { - $('#results-area').innerHTML = students - .map((s) => { - if (s.tests.length === 0) return `

${esc(s.emoji)} ${esc(s.name)}

No tests yet.

`; - const rows = s.tests - .map((t) => { - const pct = Math.round((t.score / t.total) * 100); - return ` - ${new Date(t.at + 'Z').toLocaleDateString()} - ${esc(t.list)} - ${t.score}/${t.total} (${pct}%) - - `; - }) - .join(''); - return `

${esc(s.emoji)} ${esc(s.name)}

- ${rows}
DateListScore
`; - }) - .join(''); - - document.querySelectorAll('[data-print]').forEach((btn) => btn.addEventListener('click', () => printSpellingReport(btn.dataset.print))); -} function printSpellingWorksheet(name, words) { const rows = words diff --git a/server.js b/server.js index 8de9c06..24e51ec 100644 --- a/server.js +++ b/server.js @@ -968,6 +968,19 @@ app.get('/api/gradebook/:courseId/csv', requirePin, (req, res) => { res.send([header, ...rows].map((r) => r.join(',')).join('\n')); }); +// All standalone spelling test results grouped by student +app.get('/api/spelling-tests', requirePin, (req, res) => { + const students = db.prepare(`SELECT id, name, emoji FROM students ORDER BY name`).all(); + for (const s of students) { + s.tests = db.prepare(` + SELECT t.id, t.score, t.total, t.at, l.name AS list + FROM tests t JOIN lists l ON l.id = t.list_id + WHERE t.student_id = ? ORDER BY t.at DESC LIMIT 20 + `).all(s.id); + } + res.json({ students }); +}); + // Full evidence for a submission (photo can be large, not included in queue list) app.get('/api/grading-queue/:id/evidence', requirePin, (req, res) => { const row = db.prepare(`SELECT evidence_notes, evidence_photo FROM submissions WHERE id = ?`).get(req.params.id);