diff --git a/public/admin.html b/public/admin.html
index f0ecf29..e9d04ab 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -334,6 +334,7 @@
+
diff --git a/public/admin.js b/public/admin.js
index 272103f..28e56de 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -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 `
⭐ Spelling ${t.score}/${t.total} (${pct}%)
`;
}).join('');
- return `
+ const taskHtml = dayTasks.map((t) => {
+ const colorStyle = t.courseColor ? ` style="border-left:3px solid ${esc(t.courseColor)}"` : '';
+ return `
+
+ ${t.itemId ? TYPE_ICON[t.type] : '📌'} ${esc(t.itemTitle || t.offlineTitle)}
+
+
`;
+ }).join('');
+ return `
${compact ? day.name.slice(0,3) : day.name}${compact ? day.date.slice(5) : day.date}
${testBadges}
- ${dayTasks.map((t) => `
- ${t.itemId ? TYPE_ICON[t.type] : '📌'} ${esc(t.itemTitle || t.offlineTitle)}
-
-
`).join('') || (!testBadges ? `
—
` : '')}
+ ${taskHtml || (!testBadges ? `
—
` : '')}
`;
@@ -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 =
`` +
weeks.map((week) =>
- `
${week.map((day) => renderDayCell(day, tasks, true, spellingTests)).join('')}
`
+ `
${week.map((day) => renderDayCell(day, tasks, true, spellingTests, today)).join('')}
`
).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() {
${evidenceBtn}
diff --git a/public/styles.css b/public/styles.css
index 8aa8a72..2f2424f 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -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; }
diff --git a/server.js b/server.js
index c3c6798..5c8bedb 100644
--- a/server.js
+++ b/server.js
@@ -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