Move spelling test results from Spelling panel into Gradebook
Adds a GET /api/spelling-tests endpoint that returns all standalone test results grouped by student. The Gradebook panel now shows a "Spelling Test History" section below the course gradebook table. Removed the "Test results" section from the Spelling panel left column. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
774979f2e4
commit
183b43b452
3 files changed
+46
-25
No files matched your search
+2
-2
@@ -283,6 +283,8 @@
|
|||||||
<button id="gradebook-csv-btn" class="secondary">⬇ Export CSV</button>
|
<button id="gradebook-csv-btn" class="secondary">⬇ Export CSV</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="gradebook-table"></div>
|
<div id="gradebook-table"></div>
|
||||||
|
<h2>Spelling Test History</h2>
|
||||||
|
<div id="spelling-tests-table"></div>
|
||||||
<div id="history-modal" class="modal" hidden>
|
<div id="history-modal" class="modal" hidden>
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3 id="history-modal-title">Attempt History</h3>
|
<h3 id="history-modal-title">Attempt History</h3>
|
||||||
@@ -305,8 +307,6 @@
|
|||||||
<button type="button" id="list-show-hidden" class="secondary small" hidden>Show hidden</button>
|
<button type="button" id="list-show-hidden" class="secondary small" hidden>Show hidden</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="list-rows"></div>
|
<div id="list-rows"></div>
|
||||||
<h2>Test results</h2>
|
|
||||||
<div id="results-area"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-col">
|
<div class="panel-col">
|
||||||
<div class="toggle-btns">
|
<div class="toggle-btns">
|
||||||
|
|||||||
+31
-23
@@ -1211,6 +1211,37 @@ async function loadGradebookPanel() {
|
|||||||
const courses = await api('/api/admin/courses');
|
const courses = await api('/api/admin/courses');
|
||||||
$('#gradebook-course').innerHTML = courses.map((c) => `<option value="${c.id}">${esc(c.name)}</option>`).join('');
|
$('#gradebook-course').innerHTML = courses.map((c) => `<option value="${c.id}">${esc(c.name)}</option>`).join('');
|
||||||
if (courses.length) renderGradebook(courses[0].id);
|
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 = `<p class="hint">No spelling tests recorded yet.</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = active
|
||||||
|
.map((s) => {
|
||||||
|
const rows = s.tests
|
||||||
|
.map((t) => {
|
||||||
|
const pct = Math.round((t.score / t.total) * 100);
|
||||||
|
return `<tr>
|
||||||
|
<td>${new Date(t.at + 'Z').toLocaleDateString()}</td>
|
||||||
|
<td>${esc(t.list)}</td>
|
||||||
|
<td class="${pct >= 80 ? 'score-good' : 'score-bad'}">${t.score}/${t.total} (${pct}%)</td>
|
||||||
|
<td><button data-print="${t.id}">🖨 Print</button></td>
|
||||||
|
</tr>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
return `<div class="results-student"><h3>${esc(s.emoji)} ${esc(s.name)}</h3>
|
||||||
|
<table class="results"><tr><th>Date</th><th>List</th><th>Score</th><th></th></tr>${rows}</table></div>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
container.querySelectorAll('[data-print]').forEach((btn) =>
|
||||||
|
btn.addEventListener('click', () => printSpellingReport(btn.dataset.print))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
$('#gradebook-course').addEventListener('change', () => renderGradebook($('#gradebook-course').value));
|
$('#gradebook-course').addEventListener('change', () => renderGradebook($('#gradebook-course').value));
|
||||||
$('#gradebook-csv-btn').addEventListener('click', () => {
|
$('#gradebook-csv-btn').addEventListener('click', () => {
|
||||||
@@ -1281,7 +1312,6 @@ async function loadSpelling() {
|
|||||||
cachedStudents = overview.students;
|
cachedStudents = overview.students;
|
||||||
renderAssignRows(overview.students);
|
renderAssignRows(overview.students);
|
||||||
renderLists(lists);
|
renderLists(lists);
|
||||||
renderResults(overview.students);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAssignRows(students) {
|
function renderAssignRows(students) {
|
||||||
@@ -1546,28 +1576,6 @@ $('#list-bulk-clear').addEventListener('click', () => {
|
|||||||
$('#list-bulk-status').textContent = '';
|
$('#list-bulk-status').textContent = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderResults(students) {
|
|
||||||
$('#results-area').innerHTML = students
|
|
||||||
.map((s) => {
|
|
||||||
if (s.tests.length === 0) return `<div class="results-student"><h3>${esc(s.emoji)} ${esc(s.name)}</h3><p class="hint">No tests yet.</p></div>`;
|
|
||||||
const rows = s.tests
|
|
||||||
.map((t) => {
|
|
||||||
const pct = Math.round((t.score / t.total) * 100);
|
|
||||||
return `<tr>
|
|
||||||
<td>${new Date(t.at + 'Z').toLocaleDateString()}</td>
|
|
||||||
<td>${esc(t.list)}</td>
|
|
||||||
<td class="${pct >= 80 ? 'score-good' : 'score-bad'}">${t.score}/${t.total} (${pct}%)</td>
|
|
||||||
<td><button data-print="${t.id}">🖨 Print</button></td>
|
|
||||||
</tr>`;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
return `<div class="results-student"><h3>${esc(s.emoji)} ${esc(s.name)}</h3>
|
|
||||||
<table class="results"><tr><th>Date</th><th>List</th><th>Score</th><th></th></tr>${rows}</table></div>`;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-print]').forEach((btn) => btn.addEventListener('click', () => printSpellingReport(btn.dataset.print)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function printSpellingWorksheet(name, words) {
|
function printSpellingWorksheet(name, words) {
|
||||||
const rows = words
|
const rows = words
|
||||||
|
|||||||
@@ -968,6 +968,19 @@ app.get('/api/gradebook/:courseId/csv', requirePin, (req, res) => {
|
|||||||
res.send([header, ...rows].map((r) => r.join(',')).join('\n'));
|
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)
|
// Full evidence for a submission (photo can be large, not included in queue list)
|
||||||
app.get('/api/grading-queue/:id/evidence', requirePin, (req, res) => {
|
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);
|
const row = db.prepare(`SELECT evidence_notes, evidence_photo FROM submissions WHERE id = ?`).get(req.params.id);
|
||||||
|
|||||||
Reference in new issue
Block a user