Add auto-schedule: fill planner from a course automatically
New "Auto-schedule a course" section in the Weekly Planner lets you pick a course, start date, and items-per-day, then fills the planner in order skipping weekends. POST /api/schedule/auto handles the logic server-side. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0cf2428d5
commit
544469d6c9
3 files changed
+75
-16
No files matched your search
@@ -197,6 +197,18 @@
|
|||||||
<button id="planner-copy" class="secondary">Copy last week here</button>
|
<button id="planner-copy" class="secondary">Copy last week here</button>
|
||||||
<button id="planner-print">🖨 Print week report</button>
|
<button id="planner-print">🖨 Print week report</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details id="auto-schedule-details" class="admin-card" style="margin-bottom:1rem">
|
||||||
|
<summary style="cursor:pointer;font-weight:600">⚡ Auto-schedule a course</summary>
|
||||||
|
<div class="inline-form" style="margin-top:.75rem">
|
||||||
|
<label>Course <select id="auto-course"></select></label>
|
||||||
|
<label>Start date <input type="date" id="auto-start-date"></label>
|
||||||
|
<label>Items/day <input type="number" id="auto-items-per-day" value="1" min="1" max="10" style="width:5rem"></label>
|
||||||
|
<button id="auto-schedule-btn">Schedule</button>
|
||||||
|
</div>
|
||||||
|
<p id="auto-schedule-msg" class="hint" style="margin-top:.5rem"></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div id="planner-grid" class="planner-grid"></div>
|
<div id="planner-grid" class="planner-grid"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -539,7 +539,7 @@ let plannerStudentId = null;
|
|||||||
let plannerCoursesCache = [];
|
let plannerCoursesCache = [];
|
||||||
|
|
||||||
async function loadPlannerPanel() {
|
async function loadPlannerPanel() {
|
||||||
const students = await api('/api/students');
|
const [students, courses] = await Promise.all([api('/api/students'), api('/api/admin/courses')]);
|
||||||
if (students.length === 0) {
|
if (students.length === 0) {
|
||||||
$('#planner-grid').innerHTML = `<p class="hint">Add a kid first.</p>`;
|
$('#planner-grid').innerHTML = `<p class="hint">Add a kid first.</p>`;
|
||||||
return;
|
return;
|
||||||
@@ -547,9 +547,25 @@ async function loadPlannerPanel() {
|
|||||||
$('#planner-student').innerHTML = students.map((s) => `<option value="${s.id}">${esc(s.emoji)} ${esc(s.name)}</option>`).join('');
|
$('#planner-student').innerHTML = students.map((s) => `<option value="${s.id}">${esc(s.emoji)} ${esc(s.name)}</option>`).join('');
|
||||||
if (!plannerStudentId || !students.some((s) => s.id === plannerStudentId)) plannerStudentId = students[0].id;
|
if (!plannerStudentId || !students.some((s) => s.id === plannerStudentId)) plannerStudentId = students[0].id;
|
||||||
$('#planner-student').value = plannerStudentId;
|
$('#planner-student').value = plannerStudentId;
|
||||||
|
$('#auto-course').innerHTML = courses.map((c) => `<option value="${c.id}">${esc(c.name)}</option>`).join('');
|
||||||
|
if (!$('#auto-start-date').value) $('#auto-start-date').value = plannerWeekStart;
|
||||||
await renderPlanner();
|
await renderPlanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('#auto-schedule-btn').addEventListener('click', async () => {
|
||||||
|
const courseId = Number($('#auto-course').value);
|
||||||
|
const startDate = $('#auto-start-date').value;
|
||||||
|
const itemsPerDay = Number($('#auto-items-per-day').value) || 1;
|
||||||
|
if (!courseId || !startDate) { $('#auto-schedule-msg').textContent = 'Pick a course and start date.'; return; }
|
||||||
|
const { scheduled } = await api('/api/schedule/auto', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { studentId: plannerStudentId, courseId, startDate, itemsPerDay },
|
||||||
|
});
|
||||||
|
$('#auto-schedule-msg').textContent = `Scheduled ${scheduled} item${scheduled === 1 ? '' : 's'} starting ${startDate}.`;
|
||||||
|
plannerWeekStart = mondayOf(startDate + 'T12:00:00');
|
||||||
|
renderPlanner();
|
||||||
|
});
|
||||||
|
|
||||||
$('#planner-student').addEventListener('change', () => {
|
$('#planner-student').addEventListener('change', () => {
|
||||||
plannerStudentId = Number($('#planner-student').value);
|
plannerStudentId = Number($('#planner-student').value);
|
||||||
renderPlanner();
|
renderPlanner();
|
||||||
|
|||||||
@@ -951,6 +951,52 @@ app.get('/api/schedule/:studentId', (req, res) => {
|
|||||||
res.json({ tasks });
|
res.json({ tasks });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/schedule/auto', requirePin, (req, res) => {
|
||||||
|
const { studentId, courseId, startDate, itemsPerDay = 1 } = req.body;
|
||||||
|
if (!isDateStr(startDate)) return res.status(400).json({ error: 'startDate=YYYY-MM-DD required' });
|
||||||
|
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 = ?
|
||||||
|
ORDER BY u.sort, i.sort
|
||||||
|
`).all(courseId);
|
||||||
|
if (!items.length) return res.json({ scheduled: 0 });
|
||||||
|
|
||||||
|
const perDay = Math.max(1, Math.min(20, Number(itemsPerDay)));
|
||||||
|
const ins = db.prepare(`INSERT INTO schedule (student_id, date, item_id, title, sort) VALUES (?, ?, ?, '', ?)`);
|
||||||
|
const sortQ = db.prepare(`SELECT COALESCE(MAX(sort), -1) + 1 AS n FROM schedule WHERE student_id = ? AND date = ?`);
|
||||||
|
|
||||||
|
let d = new Date(startDate + 'T12:00:00Z');
|
||||||
|
let scheduled = 0;
|
||||||
|
for (let i = 0; i < items.length; ) {
|
||||||
|
const dow = d.getUTCDay();
|
||||||
|
if (dow !== 0 && dow !== 6) {
|
||||||
|
const dateStr = d.toISOString().slice(0, 10);
|
||||||
|
for (let j = 0; j < perDay && i < items.length; j++, i++) {
|
||||||
|
const sort = sortQ.get(studentId, dateStr).n + j;
|
||||||
|
ins.run(studentId, dateStr, items[i].id, sort);
|
||||||
|
scheduled++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d = new Date(d.getTime() + 86400000);
|
||||||
|
}
|
||||||
|
res.json({ scheduled });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/schedule/copy', requirePin, (req, res) => {
|
||||||
|
const { studentId, from, to } = req.body;
|
||||||
|
if (!isDateStr(from) || !isDateStr(to)) return res.status(400).json({ error: 'from/to dates required' });
|
||||||
|
const offsetDays = Math.round((new Date(to) - new Date(from)) / 86400000);
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT date, item_id, title, sort FROM schedule WHERE student_id = ? AND date BETWEEN ? AND date(?, '+4 days')
|
||||||
|
`).all(studentId, from, from);
|
||||||
|
const ins = db.prepare(`
|
||||||
|
INSERT INTO schedule (student_id, date, item_id, title, sort) VALUES (?, date(?, ?), ?, ?, ?)
|
||||||
|
`);
|
||||||
|
for (const r of rows) ins.run(studentId, r.date, `+${offsetDays} days`, r.item_id, r.title, r.sort);
|
||||||
|
res.json({ copied: rows.length });
|
||||||
|
});
|
||||||
|
|
||||||
// Kid updates a standalone offline task: status (not_started/in_progress/done) + optional evidence
|
// Kid updates a standalone offline task: status (not_started/in_progress/done) + optional evidence
|
||||||
app.post('/api/schedule/:id/done', (req, res) => {
|
app.post('/api/schedule/:id/done', (req, res) => {
|
||||||
const row = db.prepare(`SELECT item_id FROM schedule WHERE id = ?`).get(req.params.id);
|
const row = db.prepare(`SELECT item_id FROM schedule WHERE id = ?`).get(req.params.id);
|
||||||
@@ -1005,21 +1051,6 @@ app.get('/api/schedule-week/:studentId', requirePin, (req, res) => {
|
|||||||
res.json({ tasks });
|
res.json({ tasks });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy one week's plan to another week
|
|
||||||
app.post('/api/schedule/copy', requirePin, (req, res) => {
|
|
||||||
const { studentId, from, to } = req.body;
|
|
||||||
if (!isDateStr(from) || !isDateStr(to)) return res.status(400).json({ error: 'from/to dates required' });
|
|
||||||
const offsetDays = Math.round((new Date(to) - new Date(from)) / 86400000);
|
|
||||||
const rows = db.prepare(`
|
|
||||||
SELECT date, item_id, title, sort FROM schedule WHERE student_id = ? AND date BETWEEN ? AND date(?, '+4 days')
|
|
||||||
`).all(studentId, from, from);
|
|
||||||
const ins = db.prepare(`
|
|
||||||
INSERT INTO schedule (student_id, date, item_id, title, sort) VALUES (?, date(?, ?), ?, ?, ?)
|
|
||||||
`);
|
|
||||||
for (const r of rows) ins.run(studentId, r.date, `+${offsetDays} days`, r.item_id, r.title, r.sort);
|
|
||||||
res.json({ copied: rows.length });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Printable weekly report: schedule completion + any graded work that week
|
// Printable weekly report: schedule completion + any graded work that week
|
||||||
app.get('/api/week-report/:studentId', requirePin, (req, res) => {
|
app.get('/api/week-report/:studentId', requirePin, (req, res) => {
|
||||||
const start = isDateStr(req.query.start) ? req.query.start : null;
|
const start = isDateStr(req.query.start) ? req.query.start : null;
|
||||||
|
|||||||
Reference in new issue
Block a user