Planner UX improvements and grading max-points display

- Today button jumps planner to current week; today column highlighted with accent outline
- Week label now shows friendly format (Aug 3–7, 2026) instead of ISO dates
- Course color shown as left border on scheduled course items
- Parent-side done toggle on every planner task (works for offline + course items)
- Within-day drag-to-reorder tasks via new /api/schedule/reorder endpoint
- Grading queue shows visible "/ N pts" max-points label (defaults to 100 when unset)
- New item default points changed from 10 to 100

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-04 15:16:49 -04:00
1 parent 7580257ba0
commit 60a2a597c5
4 files changed
+112 -21

No files matched your search

+1
View File
@@ -334,6 +334,7 @@
<button id="planner-prev" class="secondary">← Prev</button>
<span id="planner-week-label" class="week-label"></span>
<button id="planner-next" class="secondary">Next →</button>
<button id="planner-today" class="secondary">Today</button>
<button id="planner-copy" class="secondary">Copy last week</button>
<button id="planner-print">🖨 Print</button>
<button id="planner-add-btn">+ Add task</button>
+86 -16
View File
@@ -533,7 +533,7 @@ async function openItemEditor(unitId, itemId) {
$('#ie-title').value = item.title;
$('#ie-body-lesson').innerHTML = item.body || '';
$('#ie-body-assignment').innerHTML = item.body || '';
$('#ie-points').value = item.points || 10;
$('#ie-points').value = item.points || 100;
$('#ie-due-date').value = item.due_date || '';
$('#ie-allow-retakes').checked = !!item.allow_retakes;
$('#ie-prereq').value = item.prereq_item_id || '';
@@ -582,7 +582,7 @@ async function openItemEditor(unitId, itemId) {
$('#ie-ws-paste').value = '';
$('#ie-ws-editor').innerHTML = '';
$('#ie-ws-parse-status').hidden = true;
$('#ie-points').value = 10;
$('#ie-points').value = 100;
$('#ie-due-date').value = '';
$('#ie-allow-retakes').checked = false;
$('#ie-prereq').value = '';
@@ -1161,6 +1161,15 @@ function weekStartOf(d) {
date.setDate(date.getDate() - diff);
return date.toISOString().slice(0, 10);
}
function formatWeekLabel(start, end) {
const s = new Date(start + 'T00:00:00');
const e = new Date(end + 'T00:00:00');
const sM = s.toLocaleString('default', { month: 'short' });
const eM = e.toLocaleString('default', { month: 'short' });
const y = s.getFullYear();
if (s.getMonth() === e.getMonth()) return `${sM} ${s.getDate()}–${e.getDate()}, ${y}`;
return `${sM} ${s.getDate()} – ${eM} ${e.getDate()}, ${y}`;
}
function addDays(dateStr, n) {
const d = new Date(dateStr + 'T00:00:00');
d.setDate(d.getDate() + n);
@@ -1266,6 +1275,11 @@ $('#planner-copy').addEventListener('click', async () => {
msg(`Copied ${copied} task${copied === 1 ? '' : 's'} from last week.`);
renderPlanner();
});
$('#planner-today').addEventListener('click', () => {
plannerWeekStart = weekStartOf(new Date());
plannerMonthRef = plannerWeekStart;
renderPlanner();
});
$('#planner-print').addEventListener('click', () => {
if (plannerMode === 'month') printMonthReport(plannerStudentId, plannerWeekStart, plannerMonthRef);
@@ -1278,18 +1292,51 @@ const DAY_NAMES = () => appSettings.weekStartDay === 'sunday' ? DAY_NAMES_SUN :
function attachPlannerEvents() {
let draggingId = null;
let draggingDate = null;
document.querySelectorAll('.planner-task[draggable]').forEach((el) => {
el.addEventListener('dragstart', (e) => {
draggingId = el.dataset.taskId;
draggingDate = el.closest('.planner-tasks')?.dataset.dropDate;
el.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
el.addEventListener('dragend', () => {
draggingId = null;
draggingDate = null;
el.classList.remove('dragging');
document.querySelectorAll('.planner-tasks').forEach((col) => col.classList.remove('drop-target'));
document.querySelectorAll('.planner-task').forEach((t) => t.classList.remove('insert-before'));
});
el.addEventListener('dragover', (e) => {
if (!draggingId || el.dataset.taskId === draggingId) return;
e.preventDefault();
e.stopPropagation();
document.querySelectorAll('.planner-task').forEach((t) => t.classList.remove('insert-before'));
el.classList.add('insert-before');
});
el.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
document.querySelectorAll('.planner-task').forEach((t) => t.classList.remove('insert-before'));
if (!draggingId || el.dataset.taskId === draggingId) return;
const targetDate = el.closest('.planner-tasks')?.dataset.dropDate;
if (draggingDate === targetDate) {
const colEl = el.closest('.planner-tasks');
const ids = [...colEl.querySelectorAll('.planner-task[data-task-id]')].map((t) => t.dataset.taskId);
const fromIdx = ids.indexOf(draggingId);
const toIdx = ids.indexOf(el.dataset.taskId);
if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) return;
ids.splice(fromIdx, 1);
ids.splice(toIdx, 0, draggingId);
await api('/api/schedule/reorder', { method: 'POST', body: { tasks: ids.map((id, sort) => ({ id: Number(id), sort })) } });
} else {
await api(`/api/schedule/${draggingId}`, { method: 'PATCH', body: { date: targetDate } });
}
renderPlanner();
});
});
document.querySelectorAll('.planner-tasks').forEach((col) => {
col.addEventListener('dragover', (e) => {
e.preventDefault();
@@ -1301,36 +1348,54 @@ function attachPlannerEvents() {
e.preventDefault();
col.classList.remove('drop-target');
if (!draggingId) return;
await api(`/api/schedule/${draggingId}`, { method: 'PATCH', body: { date: col.dataset.dropDate } });
renderPlanner();
const targetDate = col.dataset.dropDate;
if (targetDate !== draggingDate) {
await api(`/api/schedule/${draggingId}`, { method: 'PATCH', body: { date: targetDate } });
renderPlanner();
}
});
});
document.querySelectorAll('[data-del-schedule]').forEach((btn) =>
btn.addEventListener('click', async () => {
await api(`/api/schedule/${btn.dataset.delSchedule}`, { method: 'DELETE' });
renderPlanner();
})
);
document.querySelectorAll('[data-done-schedule]').forEach((btn) => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const isDone = btn.dataset.isDone === '1';
await api(`/api/schedule/${btn.dataset.doneSchedule}`, { method: 'PATCH', body: { done: !isDone } });
renderPlanner();
});
});
document.querySelectorAll('[data-add-task]').forEach((btn) =>
btn.addEventListener('click', () => openPlannerAddModal(btn.dataset.addTask))
);
}
function renderDayCell(day, tasks, compact = false, spellingTests = []) {
function renderDayCell(day, tasks, compact = false, spellingTests = [], today = '') {
const dayTasks = tasks.filter((t) => t.date === day.date);
const dayTests = spellingTests.filter((t) => t.date === day.date);
const isToday = day.date === today;
const testBadges = dayTests.map((t) => {
const pct = Math.round((t.score / t.total) * 100);
return `<div class="planner-task planner-test-badge" title="${esc(t.list)}">⭐ Spelling ${t.score}/${t.total} (${pct}%)</div>`;
}).join('');
return `<div class="planner-day${compact ? ' planner-day-compact' : ''}" data-drop-date="${day.date}">
const taskHtml = dayTasks.map((t) => {
const colorStyle = t.courseColor ? ` style="border-left:3px solid ${esc(t.courseColor)}"` : '';
return `<div class="planner-task${t.done ? ' done' : ''}" draggable="true" data-task-id="${t.id}"${colorStyle}>
<button class="planner-done-btn${t.done ? ' is-done' : ''}" data-done-schedule="${t.id}" data-is-done="${t.done ? '1' : '0'}" title="${t.done ? 'Mark not done' : 'Mark done'}">✓</button>
<span>${t.itemId ? TYPE_ICON[t.type] : '📌'} ${esc(t.itemTitle || t.offlineTitle)}</span>
<button class="danger tiny" data-del-schedule="${t.id}">✕</button>
</div>`;
}).join('');
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>
<div class="planner-tasks" data-drop-date="${day.date}">
${testBadges}
${dayTasks.map((t) => `<div class="planner-task ${t.done ? 'done' : ''}" draggable="true" data-task-id="${t.id}">
<span>${t.itemId ? TYPE_ICON[t.type] : '📌'} ${esc(t.itemTitle || t.offlineTitle)}</span>
<button class="danger tiny" data-del-schedule="${t.id}">✕</button>
</div>`).join('') || (!testBadges ? `<p class="hint tiny">—</p>` : '')}
${taskHtml || (!testBadges ? `<p class="hint tiny">—</p>` : '')}
</div>
<button class="secondary small" data-add-task="${day.date}">+ Add</button>
</div>`;
@@ -1360,23 +1425,25 @@ async function renderPlanner() {
api(`/api/admin/students/${plannerStudentId}/tests-range?from=${rangeStart}&to=${rangeEnd}`),
]);
const today = new Date().toISOString().slice(0, 10);
$('#planner-week-label').textContent = monthLabel(plannerMonthRef);
$('#planner-grid').className = 'planner-grid planner-month';
$('#planner-grid').innerHTML =
`<div class="planner-month-header">${DAY_NAMES().map((d) => `<div>${d.slice(0,3)}</div>`).join('')}</div>` +
weeks.map((week) =>
`<div class="planner-month-row">${week.map((day) => renderDayCell(day, tasks, true, spellingTests)).join('')}</div>`
`<div class="planner-month-row">${week.map((day) => renderDayCell(day, tasks, true, spellingTests, today)).join('')}</div>`
).join('');
} else {
const end = addDays(plannerWeekStart, 4);
$('#planner-week-label').textContent = `${plannerWeekStart} → ${end}`;
$('#planner-week-label').textContent = formatWeekLabel(plannerWeekStart, end);
const [{ tasks }, { tests: spellingTests }] = await Promise.all([
api(`/api/schedule-week/${plannerStudentId}?start=${plannerWeekStart}`),
api(`/api/admin/students/${plannerStudentId}/tests-range?from=${plannerWeekStart}&to=${end}`),
]);
const today = new Date().toISOString().slice(0, 10);
const days = DAY_NAMES().map((name, i) => ({ name, date: addDays(plannerWeekStart, i) }));
$('#planner-grid').className = 'planner-grid';
$('#planner-grid').innerHTML = days.map((day) => renderDayCell(day, tasks, false, spellingTests)).join('');
$('#planner-grid').innerHTML = days.map((day) => renderDayCell(day, tasks, false, spellingTests, today)).join('');
}
attachPlannerEvents();
@@ -1384,7 +1451,7 @@ async function renderPlanner() {
// ---------- planner add-task modal ----------
let allCourseItems = []; // {id, type, title, courseName}
let allCourseItems = []; // {id, type, title, courseName, courseColor}
let plannerAddDate = null;
async function loadAllCourseItems() {
@@ -1394,7 +1461,7 @@ async function loadAllCourseItems() {
for (const course of details) {
for (const unit of course.units) {
for (const it of unit.items) {
allCourseItems.push({ id: it.id, type: it.type, title: it.title, courseName: course.name });
allCourseItems.push({ id: it.id, type: it.type, title: it.title, courseName: course.name, courseColor: course.color });
}
}
}
@@ -1734,7 +1801,10 @@ async function loadGrading() {
</div>
${evidenceBtn}
<div class="grading-footer">
<input type="number" class="grade-input" min="0" max="${r.points_possible}" placeholder="Score / ${r.points_possible || '?'}" data-sub="${r.submissionId}">
<div class="grade-score-row">
<input type="number" class="grade-input" min="0" max="${r.points_possible || 100}" placeholder="Score" data-sub="${r.submissionId}">
<span class="grade-max-label">/ ${r.points_possible || 100} pts</span>
</div>
<input type="text" class="comment-input" placeholder="Parent comment (optional)" data-comment-sub="${r.submissionId}">
<button data-save-grade="${r.submissionId}">Save grade</button>
</div>
+7
View File
@@ -653,6 +653,11 @@ button.tiny { font-size: 0.75rem; padding: 0.15rem 0.5rem; }
.planner-task[draggable] { cursor: grab; }
.planner-task[draggable]:active { cursor: grabbing; }
.planner-task.dragging { opacity: 0.35; }
.planner-task.insert-before { border-top: 2px solid var(--accent); }
.planner-done-btn { flex-shrink: 0; width: 20px; height: 20px; padding: 0; border: 1.5px solid #bbb; border-radius: 4px; background: transparent; color: transparent; font-size: 0.72rem; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: border-color 0.15s, color 0.15s, background 0.15s; }
.planner-done-btn.is-done { border-color: var(--good); color: var(--good); background: var(--good-bg); }
.planner-day.today { outline: 2px solid var(--accent); outline-offset: 2px; }
.planner-day.today > h3 { color: var(--accent); }
.planner-test-badge { background: color-mix(in srgb, var(--accent) 18%, var(--card)); border-left: 3px solid var(--accent); font-size: 0.78rem; cursor: default; pointer-events: none; }
.planner-tasks.drop-target {
background: color-mix(in srgb, var(--accent) 12%, transparent);
@@ -665,6 +670,8 @@ button.tiny { font-size: 0.75rem; padding: 0.15rem 0.5rem; }
.task-save-btn { font-size: 0.85rem; }
.grade-input { width: 4.5rem; padding: 0.4rem; border: 2px solid #d8d2c0; border-radius: 8px; font-family: inherit; }
.grade-score-row { display: flex; align-items: center; gap: 0.4rem; }
.grade-max-label { font-size: 0.9rem; font-weight: 600; color: var(--ink); white-space: nowrap; }
@media (max-width: 700px) {
.planner-grid { grid-template-columns: 1fr; }
+18 -5
View File
@@ -1912,9 +1912,22 @@ app.post('/api/schedule', requirePin, (req, res) => {
});
app.patch('/api/schedule/:id', requirePin, (req, res) => {
const { date } = req.body;
if (!isDateStr(date)) return res.status(400).json({ error: 'date=YYYY-MM-DD required' });
db.prepare(`UPDATE schedule SET date = ? WHERE id = ?`).run(date, req.params.id);
const { date, done } = req.body;
if (date !== undefined) {
if (!isDateStr(date)) return res.status(400).json({ error: 'date=YYYY-MM-DD required' });
db.prepare(`UPDATE schedule SET date = ? WHERE id = ?`).run(date, req.params.id);
}
if (done !== undefined) {
const d = done ? 1 : 0;
db.prepare(`UPDATE schedule SET done = ?, done_at = CASE WHEN ? THEN datetime('now') ELSE NULL END WHERE id = ?`).run(d, d, req.params.id);
}
res.json({ ok: true });
});
app.post('/api/schedule/reorder', requirePin, (req, res) => {
const stmt = db.prepare(`UPDATE schedule SET sort = ? WHERE id = ?`);
const run = db.transaction((tasks) => { for (const t of tasks) stmt.run(t.sort, t.id); });
run(req.body.tasks || []);
res.json({ ok: true });
});
@@ -1929,7 +1942,7 @@ app.get('/api/schedule-week/:studentId', requirePin, (req, res) => {
if (!start) return res.status(400).json({ error: 'start=YYYY-MM-DD required' });
const tasks = db.prepare(`
SELECT sc.id, sc.date, sc.title AS offlineTitle, sc.done,
i.id AS itemId, i.type, i.title AS itemTitle, c.name AS courseName
i.id AS itemId, i.type, i.title AS itemTitle, c.name AS courseName, c.color AS courseColor
FROM schedule sc
LEFT JOIN items i ON i.id = sc.item_id
LEFT JOIN units u ON u.id = i.unit_id
@@ -1946,7 +1959,7 @@ app.get('/api/schedule-range/:studentId', requirePin, (req, res) => {
if (!start || !end) return res.status(400).json({ error: 'start and end (YYYY-MM-DD) required' });
const tasks = db.prepare(`
SELECT sc.id, sc.date, sc.title AS offlineTitle, sc.done,
i.id AS itemId, i.type, i.title AS itemTitle, i.body, c.name AS courseName
i.id AS itemId, i.type, i.title AS itemTitle, i.body, c.name AS courseName, c.color AS courseColor
FROM schedule sc
LEFT JOIN items i ON i.id = sc.item_id
LEFT JOIN units u ON u.id = i.unit_id