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:
1 parent
7580257ba0
commit
60a2a597c5
4 files changed
+112
-21
No files matched your search
@@ -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
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user