diff --git a/public/admin.html b/public/admin.html index d880fdb..d5bc259 100644 --- a/public/admin.html +++ b/public/admin.html @@ -396,6 +396,28 @@
+ +
+ 📝 Record Paper Score +

Use this to log a score from a paper quiz, assignment, or other item done off-screen.

+
+
+ + +
+ +
+ + out of + + +
+
+ + +
+
+

Spelling Test History

+ +
+
+ + out of + + +
+
+ + +
+ + +

Practice Sessions

+ +
+ + + +
+

+ + diff --git a/public/admin.js b/public/admin.js index 6aca485..95bd517 100644 --- a/public/admin.js +++ b/public/admin.js @@ -2070,10 +2070,11 @@ function renderAdminMsgs(msgs) { let gradebookCourseId = null; async function loadGradebookPanel() { - const courses = await api('/api/admin/courses'); + const [courses, students] = await Promise.all([api('/api/admin/courses'), api('/api/students')]); $('#gradebook-course').innerHTML = courses.map((c) => ``).join(''); if (courses.length) renderGradebook(courses[0].id); loadSpellingTests(); + initPaperScoreForm(students, courses); } async function loadSpellingTests() { @@ -2275,6 +2276,7 @@ async function loadSpelling() { renderLists(lists); loadPracticeSessions(); loadPracticeActivity(); + initPaperTestForm(); } async function loadPracticeSessions() { @@ -2477,6 +2479,7 @@ function renderLists(lists, filterText = '') { ${l.wordCount} words${l.builtin ? ' · built-in' : ''} ${l.builtin ? '' : ``} + ${l.builtin ? '' : ``} ${l.builtin ? `` @@ -2495,6 +2498,12 @@ function renderLists(lists, filterText = '') { printSpellingWorksheet(list.name, list.words); }) ); + document.querySelectorAll('[data-blank-test]').forEach((btn) => + btn.addEventListener('click', async () => { + const list = await api(`/api/lists/${btn.dataset.blankTest}`); + printBlankSpellingTest(list.name, list.words); + }) + ); document.querySelectorAll('[data-edit-list]').forEach((btn) => btn.addEventListener('click', async () => { const list = await api(`/api/lists/${btn.dataset.editList}`); @@ -2595,14 +2604,22 @@ function clearListForm() { function showSection(type, which) { $(`#${type}-editor-section`).hidden = which !== 'editor'; $(`#${type}-bulk-section`).hidden = which !== 'bulk'; + const csvSec = $(`#${type}-csv-section`); + if (csvSec) csvSec.hidden = which !== 'csv'; $(`#${type}-new-btn`).classList.toggle('active', which === 'editor'); $(`#${type}-bulk-btn`).classList.toggle('active', which === 'bulk'); + const csvBtn = $(`#${type}-csv-btn`); + if (csvBtn) csvBtn.classList.toggle('active', which === 'csv'); } function hideSections(type) { $(`#${type}-editor-section`).hidden = true; $(`#${type}-bulk-section`).hidden = true; + const csvSec = $(`#${type}-csv-section`); + if (csvSec) csvSec.hidden = true; $(`#${type}-new-btn`).classList.remove('active'); $(`#${type}-bulk-btn`).classList.remove('active'); + const csvBtn = $(`#${type}-csv-btn`); + if (csvBtn) csvBtn.classList.remove('active'); } $('#list-new-btn').addEventListener('click', () => { @@ -2613,6 +2630,55 @@ $('#list-bulk-btn').addEventListener('click', () => { if ($('#list-bulk-section').hidden) showSection('list', 'bulk'); else hideSections('list'); }); +$('#list-csv-btn').addEventListener('click', () => { + if ($('#list-csv-section').hidden) showSection('list', 'csv'); + else hideSections('list'); +}); + +$('#list-csv-browse').addEventListener('click', () => $('#list-csv-file').click()); +$('#list-csv-file').addEventListener('change', () => { + const f = $('#list-csv-file').files[0]; + $('#list-csv-filename').textContent = f ? f.name : ''; + $('#list-csv-go').disabled = !f; + if (f && !$('#list-csv-name').value.trim()) { + $('#list-csv-name').value = f.name.replace(/\.[^.]+$/, '').replace(/[_-]/g, ' '); + } + $('#list-csv-status').textContent = ''; +}); +$('#list-csv-go').addEventListener('click', async () => { + const f = $('#list-csv-file').files[0]; + const name = $('#list-csv-name').value.trim(); + const groupName = $('#list-csv-group').value.trim(); + if (!f || !name) { $('#list-csv-status').textContent = 'Choose a file and enter a list name.'; return; } + $('#list-csv-status').textContent = 'Parsing…'; + const text = await f.text(); + const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean); + const words = []; + for (const line of lines) { + const parts = line.split(',').map((p) => p.trim().replace(/^["']|["']$/g, '')); + const word = parts[0]; + if (!word || word.toLowerCase() === 'word') continue; // skip header + const entry = { word }; + if (parts[1]) entry.sentence = parts[1]; + if (parts[2]) entry.definition = parts[2]; + words.push(entry); + } + if (words.length === 0) { $('#list-csv-status').textContent = 'No words found in file.'; return; } + try { + await api('/api/lists', { method: 'POST', body: { name, words, groupName } }); + msg(`Imported "${name}" (${words.length} words).`); + $('#list-csv-file').value = ''; + $('#list-csv-filename').textContent = ''; + $('#list-csv-go').disabled = true; + $('#list-csv-name').value = ''; + $('#list-csv-group').value = ''; + $('#list-csv-status').textContent = ''; + hideSections('list'); + loadSpelling(); + } catch (e) { + $('#list-csv-status').textContent = `Error: ${e.message}`; + } +}); function parseBulkSections(text) { const sections = []; @@ -2715,6 +2781,82 @@ async function printSpellingReport(testId) { win.document.close(); } +function printBlankSpellingTest(name, words) { + const today = new Date().toLocaleDateString(); + const rows = words + .map((w, i) => { + const hint = w.sentence ? `
${esc(w.sentence)}
` : ''; + return ` + ${i + 1}. + ${hint}
+ `; + }) + .join(''); + const win = window.open('', '_blank'); + win.document.write(`Spelling Test: ${esc(name)} + +

⭐ Spelling Test: ${esc(name)}

+
+ + +
+ ${rows}
+