Fix 7 UX pain points: grading filter, next-item nav, lesson prose, gradebook calc, auto-schedule skip, lesson item defaults, planner improvements
- Grading: add student filter dropdown, Enter-to-save, and point max always defaults to 100 - Student side: show next-item/back-to-course actions after completing a lesson or assignment - Lesson body: apply prose typography styles (headings, lists, blockquotes, links, hr) - Gradebook: fix % display when points_possible is 0 (default to 100 on grade save) - Auto-schedule: skip items that already have a submission - Item editor: default new/existing item point values to 100 instead of 10 - Planner: friendly week date header, per-day short date, today highlight, course color on task pills, done toggle, within-day drag reorder, Today button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
60a2a597c5
commit
538522cfe7
6 files changed
+90
-21
No files matched your search
@@ -357,6 +357,9 @@
|
|||||||
<!-- Grading queue -->
|
<!-- Grading queue -->
|
||||||
<section id="panel-grading" class="panel">
|
<section id="panel-grading" class="panel">
|
||||||
<h1>Grading Queue</h1>
|
<h1>Grading Queue</h1>
|
||||||
|
<div class="inline-form" style="margin-bottom:1rem">
|
||||||
|
<label>Student <select id="grading-student-filter"><option value="">All students</option></select></label>
|
||||||
|
</div>
|
||||||
<div id="grading-rows"></div>
|
<div id="grading-rows"></div>
|
||||||
<p id="grading-empty" class="hint" hidden>Nothing waiting to be graded 🎉</p>
|
<p id="grading-empty" class="hint" hidden>Nothing waiting to be graded 🎉</p>
|
||||||
<!-- Evidence lightbox -->
|
<!-- Evidence lightbox -->
|
||||||
|
|||||||
+31
-12
@@ -1391,8 +1391,9 @@ function renderDayCell(day, tasks, compact = false, spellingTests = [], today =
|
|||||||
<button class="danger tiny" data-del-schedule="${t.id}">✕</button>
|
<button class="danger tiny" data-del-schedule="${t.id}">✕</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
const fmtDate = (d) => { const dt = new Date(d + 'T00:00:00'); return dt.toLocaleString('default', { month: 'short', day: 'numeric' }); };
|
||||||
return `<div class="planner-day${compact ? ' planner-day-compact' : ''}${isToday ? ' today' : ''}" data-drop-date="${day.date}">
|
return `<div class="planner-day${compact ? ' planner-day-compact' : ''}${isToday ? ' today' : ''}" data-drop-date="${day.date}">
|
||||||
<h3>${compact ? day.name.slice(0,3) : day.name}<small>${compact ? day.date.slice(5) : day.date}</small></h3>
|
<h3>${compact ? day.name.slice(0,3) : day.name}<small>${fmtDate(day.date)}</small></h3>
|
||||||
<div class="planner-tasks" data-drop-date="${day.date}">
|
<div class="planner-tasks" data-drop-date="${day.date}">
|
||||||
${testBadges}
|
${testBadges}
|
||||||
${taskHtml || (!testBadges ? `<p class="hint tiny">—</p>` : '')}
|
${taskHtml || (!testBadges ? `<p class="hint tiny">—</p>` : '')}
|
||||||
@@ -1782,7 +1783,19 @@ function showEvidenceLightbox(notes, photo) {
|
|||||||
const STUDENT_NOTE_LABEL = { needs_help: '🙋 Needs help', not_sure: '🤔 Not sure' };
|
const STUDENT_NOTE_LABEL = { needs_help: '🙋 Needs help', not_sure: '🤔 Not sure' };
|
||||||
|
|
||||||
async function loadGrading() {
|
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 = '<option value="">All students</option>' +
|
||||||
|
allRows.filter((r) => { if (seen.has(r.studentId)) return false; seen.add(r.studentId); return true; })
|
||||||
|
.map((r) => `<option value="${r.studentId}">${esc(r.emoji)} ${esc(r.studentName)}</option>`).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-empty').hidden = rows.length > 0;
|
||||||
$('#grading-rows').innerHTML = rows
|
$('#grading-rows').innerHTML = rows
|
||||||
.map((r) => {
|
.map((r) => {
|
||||||
@@ -1814,7 +1827,6 @@ async function loadGrading() {
|
|||||||
|
|
||||||
document.querySelectorAll('[data-evidence-notes]').forEach((btn) => {
|
document.querySelectorAll('[data-evidence-notes]').forEach((btn) => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
// photo may be truncated in the attribute — fetch full evidence via API
|
|
||||||
const subId = btn.dataset.subId;
|
const subId = btn.dataset.subId;
|
||||||
let photo = null;
|
let photo = null;
|
||||||
if (btn.dataset.evidencePhoto === 'yes') {
|
if (btn.dataset.evidencePhoto === 'yes') {
|
||||||
@@ -1825,22 +1837,29 @@ async function loadGrading() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('[data-save-grade]').forEach((btn) => {
|
async function saveGrade(subId) {
|
||||||
btn.addEventListener('click', async () => {
|
const input = document.querySelector(`.grade-input[data-sub="${subId}"]`);
|
||||||
const input = document.querySelector(`.grade-input[data-sub="${btn.dataset.saveGrade}"]`);
|
const commentInput = document.querySelector(`.comment-input[data-comment-sub="${subId}"]`);
|
||||||
const commentInput = document.querySelector(`.comment-input[data-comment-sub="${btn.dataset.saveGrade}"]`);
|
if (!input || input.value === '') return;
|
||||||
if (input.value === '') return;
|
await api(`/api/submissions/${subId}/grade`, {
|
||||||
await api(`/api/submissions/${btn.dataset.saveGrade}/grade`, {
|
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: { score: Number(input.value), parentComment: commentInput?.value || '' },
|
body: { score: Number(input.value), parentComment: commentInput?.value || '', pointsPossible: Number(input.max) || 100 },
|
||||||
});
|
});
|
||||||
msg('Grade saved.');
|
msg('Grade saved.');
|
||||||
loadGrading();
|
loadGrading();
|
||||||
loadGradingBadge();
|
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
|
// Gradebook
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
+33
-2
@@ -258,6 +258,28 @@ async function loadHome() {
|
|||||||
|
|
||||||
// ---------- kid home: Today / Courses / Spelling ----------
|
// ---------- 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 = `
|
||||||
|
<button class="check-btn" id="next-item-btn">Next: ${esc(next.title)} →</button>
|
||||||
|
<button class="check-btn secondary" id="back-course-btn">← Back to course</button>
|
||||||
|
`;
|
||||||
|
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) =>
|
document.querySelectorAll('.nav-pill').forEach((pill) =>
|
||||||
pill.addEventListener('click', () => switchTab(pill.dataset.tab))
|
pill.addEventListener('click', () => switchTab(pill.dataset.tab))
|
||||||
);
|
);
|
||||||
@@ -902,6 +924,7 @@ function activeUnitIndex(units) {
|
|||||||
|
|
||||||
async function openCourse(courseId) {
|
async function openCourse(courseId) {
|
||||||
const course = await api(`/api/courses/${courseId}/detail?studentId=${currentStudent.id}`);
|
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;
|
$('#course-title').textContent = course.name;
|
||||||
const activeIdx = activeUnitIndex(course.units);
|
const activeIdx = activeUnitIndex(course.units);
|
||||||
$('#course-units').innerHTML = course.units
|
$('#course-units').innerHTML = course.units
|
||||||
@@ -992,12 +1015,15 @@ async function openLesson(itemId) {
|
|||||||
show('lesson');
|
show('lesson');
|
||||||
|
|
||||||
const btn = $('#lesson-done-btn');
|
const btn = $('#lesson-done-btn');
|
||||||
|
const nextActions = $('#lesson-next-actions');
|
||||||
|
nextActions.hidden = true;
|
||||||
btn.textContent = item.submission ? '✅ Read' : 'Done reading ✓';
|
btn.textContent = item.submission ? '✅ Read' : 'Done reading ✓';
|
||||||
btn.onclick = async () => {
|
btn.onclick = async () => {
|
||||||
await api(`/api/items/${itemId}/complete`, { method: 'POST', body: { studentId: currentStudent.id, date: todayStr() } });
|
await api(`/api/items/${itemId}/complete`, { method: 'POST', body: { studentId: currentStudent.id, date: todayStr() } });
|
||||||
showMiniCelebration('📖 Lesson complete!');
|
showMiniCelebration('📖 Lesson complete!');
|
||||||
await showDoneStamp();
|
await showDoneStamp();
|
||||||
backTarget();
|
btn.disabled = true;
|
||||||
|
showNextActions('lesson-next-actions', itemId);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,6 +1035,7 @@ async function openAssignment(itemId) {
|
|||||||
$('#assignment-photo-input').value = '';
|
$('#assignment-photo-input').value = '';
|
||||||
$('#assignment-evidence-notes').value = '';
|
$('#assignment-evidence-notes').value = '';
|
||||||
$('#assignment-student-note').value = '';
|
$('#assignment-student-note').value = '';
|
||||||
|
$('#assignment-next-actions').hidden = true;
|
||||||
|
|
||||||
const item = await api(`/api/items/${itemId}?studentId=${currentStudent.id}`);
|
const item = await api(`/api/items/${itemId}?studentId=${currentStudent.id}`);
|
||||||
$('#assignment-kicker').textContent = `${item.course_name} · ${item.unit_name}`;
|
$('#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 });
|
await api(`/api/items/${itemId}/complete`, { method: 'POST', body });
|
||||||
showMiniCelebration('📝 Turned in!');
|
showMiniCelebration('📝 Turned in!');
|
||||||
await showDoneStamp();
|
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');
|
show('assignment');
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
<h1 id="lesson-title"></h1>
|
<h1 id="lesson-title"></h1>
|
||||||
<div id="lesson-body" class="lesson-body"></div>
|
<div id="lesson-body" class="lesson-body"></div>
|
||||||
<button id="lesson-done-btn" class="check-btn">Done reading ✓</button>
|
<button id="lesson-done-btn" class="check-btn">Done reading ✓</button>
|
||||||
|
<div id="lesson-next-actions" class="next-actions" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -135,6 +136,7 @@
|
|||||||
<button id="assignment-done-btn" class="check-btn">Mark as done ✓</button>
|
<button id="assignment-done-btn" class="check-btn">Mark as done ✓</button>
|
||||||
<div id="assignment-status" class="status-banner" hidden></div>
|
<div id="assignment-status" class="status-banner" hidden></div>
|
||||||
<div id="assignment-comment" class="parent-comment" hidden></div>
|
<div id="assignment-comment" class="parent-comment" hidden></div>
|
||||||
|
<div id="assignment-next-actions" class="next-actions" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -439,6 +439,17 @@ table.results tr:nth-child(even) td { background: #faf7ee; }
|
|||||||
.item-card h1 { font-size: 1.5rem; margin-bottom: 1rem; }
|
.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 { 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 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; }
|
.points-label { color: var(--muted); font-weight: 700; margin-bottom: 0.75rem; }
|
||||||
.status-banner {
|
.status-banner {
|
||||||
margin-top: 1rem; padding: 0.75rem 1rem; border-radius: 12px;
|
margin-top: 1rem; padding: 0.75rem 1rem; border-radius: 12px;
|
||||||
|
|||||||
@@ -1569,11 +1569,13 @@ app.put('/api/submissions/:id/grade', requirePin, (req, res) => {
|
|||||||
const score = Number(req.body.score);
|
const score = Number(req.body.score);
|
||||||
if (Number.isNaN(score)) return res.status(400).json({ error: 'Score must be a number' });
|
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;
|
const comment = req.body.parentComment !== undefined ? String(req.body.parentComment).trim() || null : undefined;
|
||||||
db.prepare(`
|
const pp = req.body.pointsPossible !== undefined ? Number(req.body.pointsPossible) : null;
|
||||||
UPDATE submissions SET score = ?, status = 'graded', graded_at = datetime('now')
|
const sets = [`score = ?`, `status = 'graded'`, `graded_at = datetime('now')`];
|
||||||
${comment !== undefined ? ', parent_comment = ?' : ''}
|
const vals = [score];
|
||||||
WHERE id = ?
|
if (comment !== undefined) { sets.push('parent_comment = ?'); vals.push(comment); }
|
||||||
`).run(...(comment !== undefined ? [score, comment, req.params.id] : [score, req.params.id]));
|
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 });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1839,9 +1841,10 @@ app.post('/api/schedule/auto', requirePin, (req, res) => {
|
|||||||
const items = db.prepare(`
|
const items = db.prepare(`
|
||||||
SELECT i.id, i.title FROM items i
|
SELECT i.id, i.title FROM items i
|
||||||
JOIN units u ON u.id = i.unit_id
|
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
|
ORDER BY u.sort, i.sort
|
||||||
`).all(courseId);
|
`).all(studentId, courseId);
|
||||||
if (!items.length) return res.json({ scheduled: 0 });
|
if (!items.length) return res.json({ scheduled: 0 });
|
||||||
|
|
||||||
const perDay = Math.max(1, Math.min(20, Number(itemsPerDay)));
|
const perDay = Math.max(1, Math.min(20, Number(itemsPerDay)));
|
||||||
|
|||||||
Reference in new issue
Block a user