diff --git a/public/admin.html b/public/admin.html index e9d04ab..ddbb877 100644 --- a/public/admin.html +++ b/public/admin.html @@ -357,6 +357,9 @@

Grading Queue

+
+ +
diff --git a/public/admin.js b/public/admin.js index 28e56de..5e3dbc1 100644 --- a/public/admin.js +++ b/public/admin.js @@ -1391,8 +1391,9 @@ function renderDayCell(day, tasks, compact = false, spellingTests = [], today = `; }).join(''); + const fmtDate = (d) => { const dt = new Date(d + 'T00:00:00'); return dt.toLocaleString('default', { month: 'short', day: 'numeric' }); }; return `
-

${compact ? day.name.slice(0,3) : day.name}${compact ? day.date.slice(5) : day.date}

+

${compact ? day.name.slice(0,3) : day.name}${fmtDate(day.date)}

${testBadges} ${taskHtml || (!testBadges ? `

โ€”

` : '')} @@ -1782,7 +1783,19 @@ function showEvidenceLightbox(notes, photo) { const STUDENT_NOTE_LABEL = { needs_help: '๐Ÿ™‹ Needs help', not_sure: '๐Ÿค” Not sure' }; async function loadGrading() { - const rows = await api('/api/grading-queue'); + const allRows = await api('/api/grading-queue'); + + // Populate student filter + const filterEl = $('#grading-student-filter'); + const prevFilter = filterEl.value; + const seen = new Set(); + filterEl.innerHTML = '' + + allRows.filter((r) => { if (seen.has(r.studentId)) return false; seen.add(r.studentId); return true; }) + .map((r) => ``).join(''); + if (prevFilter) filterEl.value = prevFilter; + + const rows = filterEl.value ? allRows.filter((r) => String(r.studentId) === filterEl.value) : allRows; + $('#grading-empty').hidden = rows.length > 0; $('#grading-rows').innerHTML = rows .map((r) => { @@ -1814,7 +1827,6 @@ async function loadGrading() { document.querySelectorAll('[data-evidence-notes]').forEach((btn) => { btn.addEventListener('click', async () => { - // photo may be truncated in the attribute โ€” fetch full evidence via API const subId = btn.dataset.subId; let photo = null; if (btn.dataset.evidencePhoto === 'yes') { @@ -1825,22 +1837,29 @@ async function loadGrading() { }); }); - document.querySelectorAll('[data-save-grade]').forEach((btn) => { - btn.addEventListener('click', async () => { - const input = document.querySelector(`.grade-input[data-sub="${btn.dataset.saveGrade}"]`); - const commentInput = document.querySelector(`.comment-input[data-comment-sub="${btn.dataset.saveGrade}"]`); - if (input.value === '') return; - await api(`/api/submissions/${btn.dataset.saveGrade}/grade`, { - method: 'PUT', - body: { score: Number(input.value), parentComment: commentInput?.value || '' }, - }); - msg('Grade saved.'); - loadGrading(); - loadGradingBadge(); + async function saveGrade(subId) { + const input = document.querySelector(`.grade-input[data-sub="${subId}"]`); + const commentInput = document.querySelector(`.comment-input[data-comment-sub="${subId}"]`); + if (!input || input.value === '') return; + await api(`/api/submissions/${subId}/grade`, { + method: 'PUT', + body: { score: Number(input.value), parentComment: commentInput?.value || '', pointsPossible: Number(input.max) || 100 }, }); - }); + msg('Grade saved.'); + loadGrading(); + loadGradingBadge(); + } + + document.querySelectorAll('[data-save-grade]').forEach((btn) => + btn.addEventListener('click', () => saveGrade(btn.dataset.saveGrade)) + ); + document.querySelectorAll('.grade-input').forEach((input) => + input.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveGrade(input.dataset.sub); }) + ); } +$('#grading-student-filter').addEventListener('change', loadGrading); + // ============================================================ // Gradebook // ============================================================ diff --git a/public/app.js b/public/app.js index 2929c7b..eb17b03 100644 --- a/public/app.js +++ b/public/app.js @@ -258,6 +258,28 @@ async function loadHome() { // ---------- kid home: Today / Courses / Spelling ---------- +let currentCourseItems = []; // flat [{id, type, title}] from most-recently opened course + +function findNextCourseItem(currentId) { + const idx = currentCourseItems.findIndex((it) => it.id === currentId); + if (idx === -1 || idx >= currentCourseItems.length - 1) return null; + return currentCourseItems[idx + 1]; +} + +function showNextActions(containerId, currentItemId) { + const next = findNextCourseItem(currentItemId); + const el = document.getElementById(containerId); + if (!el) return; + if (!next) { backTarget(); return; } + el.hidden = false; + el.innerHTML = ` + + + `; + document.getElementById('next-item-btn').addEventListener('click', () => { el.hidden = true; openItem(next.id, next.type); }); + document.getElementById('back-course-btn').addEventListener('click', () => { el.hidden = true; backTarget(); }); +} + document.querySelectorAll('.nav-pill').forEach((pill) => pill.addEventListener('click', () => switchTab(pill.dataset.tab)) ); @@ -902,6 +924,7 @@ function activeUnitIndex(units) { async function openCourse(courseId) { const course = await api(`/api/courses/${courseId}/detail?studentId=${currentStudent.id}`); + currentCourseItems = course.units.flatMap((u) => u.items.map((it) => ({ id: it.id, type: it.type, title: it.title }))); $('#course-title').textContent = course.name; const activeIdx = activeUnitIndex(course.units); $('#course-units').innerHTML = course.units @@ -992,12 +1015,15 @@ async function openLesson(itemId) { show('lesson'); const btn = $('#lesson-done-btn'); + const nextActions = $('#lesson-next-actions'); + nextActions.hidden = true; btn.textContent = item.submission ? 'โœ… Read' : 'Done reading โœ“'; btn.onclick = async () => { await api(`/api/items/${itemId}/complete`, { method: 'POST', body: { studentId: currentStudent.id, date: todayStr() } }); showMiniCelebration('๐Ÿ“– Lesson complete!'); await showDoneStamp(); - backTarget(); + btn.disabled = true; + showNextActions('lesson-next-actions', itemId); }; } @@ -1009,6 +1035,7 @@ async function openAssignment(itemId) { $('#assignment-photo-input').value = ''; $('#assignment-evidence-notes').value = ''; $('#assignment-student-note').value = ''; + $('#assignment-next-actions').hidden = true; const item = await api(`/api/items/${itemId}?studentId=${currentStudent.id}`); $('#assignment-kicker').textContent = `${item.course_name} ยท ${item.unit_name}`; @@ -1068,7 +1095,11 @@ async function openAssignment(itemId) { await api(`/api/items/${itemId}/complete`, { method: 'POST', body }); showMiniCelebration('๐Ÿ“ Turned in!'); await showDoneStamp(); - openAssignment(itemId); + btn.hidden = true; + status.hidden = false; + status.className = 'status-banner'; + status.textContent = 'โณ Turned in โ€” waiting for a parent to grade it.'; + showNextActions('assignment-next-actions', itemId); }; } show('assignment'); diff --git a/public/index.html b/public/index.html index db9d3fa..e5f7553 100644 --- a/public/index.html +++ b/public/index.html @@ -101,6 +101,7 @@

+
@@ -135,6 +136,7 @@ + diff --git a/public/styles.css b/public/styles.css index 2f2424f..6950b47 100644 --- a/public/styles.css +++ b/public/styles.css @@ -439,6 +439,17 @@ table.results tr:nth-child(even) td { background: #faf7ee; } .item-card h1 { font-size: 1.5rem; margin-bottom: 1rem; } .lesson-body { font-size: 1.05rem; line-height: 1.6; margin-bottom: 1.5rem; } .lesson-body img, .lesson-body iframe { max-width: 100%; border-radius: 10px; } +.lesson-body h1, .lesson-body h2, .lesson-body h3 { font-weight: 700; margin: 1.1rem 0 0.4rem; line-height: 1.3; } +.lesson-body h1 { font-size: 1.35rem; } +.lesson-body h2 { font-size: 1.15rem; } +.lesson-body h3 { font-size: 1rem; } +.lesson-body p { margin-bottom: 0.75rem; } +.lesson-body ul, .lesson-body ol { padding-left: 1.5rem; margin-bottom: 0.75rem; } +.lesson-body li { margin-bottom: 0.25rem; } +.lesson-body blockquote { border-left: 3px solid var(--accent); padding-left: 1rem; margin: 0.75rem 0; color: var(--muted); font-style: italic; } +.lesson-body a { color: var(--accent); text-decoration: underline; } +.lesson-body hr { border: none; border-top: 1px solid #ddd; margin: 1rem 0; } +.next-actions { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.75rem; } .points-label { color: var(--muted); font-weight: 700; margin-bottom: 0.75rem; } .status-banner { margin-top: 1rem; padding: 0.75rem 1rem; border-radius: 12px; diff --git a/server.js b/server.js index 5c8bedb..c279084 100644 --- a/server.js +++ b/server.js @@ -1569,11 +1569,13 @@ app.put('/api/submissions/:id/grade', requirePin, (req, res) => { const score = Number(req.body.score); if (Number.isNaN(score)) return res.status(400).json({ error: 'Score must be a number' }); const comment = req.body.parentComment !== undefined ? String(req.body.parentComment).trim() || null : undefined; - db.prepare(` - UPDATE submissions SET score = ?, status = 'graded', graded_at = datetime('now') - ${comment !== undefined ? ', parent_comment = ?' : ''} - WHERE id = ? - `).run(...(comment !== undefined ? [score, comment, req.params.id] : [score, req.params.id])); + const pp = req.body.pointsPossible !== undefined ? Number(req.body.pointsPossible) : null; + const sets = [`score = ?`, `status = 'graded'`, `graded_at = datetime('now')`]; + const vals = [score]; + if (comment !== undefined) { sets.push('parent_comment = ?'); vals.push(comment); } + if (pp && pp > 0) { sets.push('points_possible = CASE WHEN COALESCE(points_possible, 0) = 0 THEN ? ELSE points_possible END'); vals.push(pp); } + vals.push(req.params.id); + db.prepare(`UPDATE submissions SET ${sets.join(', ')} WHERE id = ?`).run(...vals); res.json({ ok: true }); }); @@ -1839,9 +1841,10 @@ app.post('/api/schedule/auto', requirePin, (req, res) => { const items = db.prepare(` SELECT i.id, i.title FROM items i JOIN units u ON u.id = i.unit_id - WHERE u.course_id = ? + LEFT JOIN submissions sub ON sub.item_id = i.id AND sub.student_id = ? + WHERE u.course_id = ? AND sub.id IS NULL ORDER BY u.sort, i.sort - `).all(courseId); + `).all(studentId, courseId); if (!items.length) return res.json({ scheduled: 0 }); const perDay = Math.max(1, Math.min(20, Number(itemsPerDay)));