Add course duplicate and compact list/roster layout

- POST /api/courses/:id/duplicate deep-copies a course with all units,
  items, and quiz questions; opens the copy immediately for renaming
- Roster changed from broken vertical stack to horizontal chip-pills
  (fixes admin-card label override that was collapsing inline-flex)
- item-row padding reduced for tighter lists throughout admin
- Courses and Kids admin panels now use a 2-column auto-fill grid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-27 10:48:38 -04:00
1 parent ca68e9df52
commit 0ae0db76bc
4 files changed
+68 -3

No files matched your search

+37
View File
@@ -479,6 +479,43 @@ app.delete('/api/courses/:id', requirePin, (req, res) => {
res.json({ ok: true });
});
app.post('/api/courses/:id/duplicate', requirePin, (req, res) => {
const src = db.prepare(`SELECT name, subject, color FROM courses WHERE id = ?`).get(req.params.id);
if (!src) return res.status(404).json({ error: 'No such course' });
const newCourseId = db.prepare(`INSERT INTO courses (name, subject, color) VALUES (?, ?, ?)`)
.run(`Copy of ${src.name}`, src.subject || '', src.color || '#4f86f7').lastInsertRowid;
const insUnit = db.prepare(`INSERT INTO units (course_id, name, sort) VALUES (?, ?, ?)`);
const insItem = db.prepare(`
INSERT INTO items (unit_id, type, title, body, points, ref_id, sort, due_date, allow_retakes, prereq_item_id, evidence_mode, retake_policy)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insQ = db.prepare(`INSERT INTO quiz_questions (item_id, type, prompt, choices, correct_answer, points, sort) VALUES (?, ?, ?, ?, ?, ?, ?)`);
const getItems = db.prepare(`SELECT * FROM items WHERE unit_id = ? ORDER BY sort, id`);
const getQs = db.prepare(`SELECT * FROM quiz_questions WHERE item_id = ? ORDER BY sort, id`);
const units = db.prepare(`SELECT id, name, sort FROM units WHERE course_id = ? ORDER BY sort, id`).all(req.params.id);
for (const u of units) {
const newUnitId = insUnit.run(newCourseId, u.name, u.sort).lastInsertRowid;
for (const it of getItems.all(u.id)) {
const newItemId = insItem.run(
newUnitId, it.type, it.title, it.body, it.points, it.ref_id,
it.sort, it.due_date, it.allow_retakes,
null, // prereq_item_id: don't copy — old IDs won't match new items
it.evidence_mode || 'none', it.retake_policy || 'latest'
).lastInsertRowid;
if (it.type === 'quiz') {
for (const q of getQs.all(it.id)) {
insQ.run(newItemId, q.type, q.prompt, q.choices, q.correct_answer, q.points, q.sort);
}
}
}
}
res.json({ id: newCourseId });
});
app.post('/api/courses/:id/enroll', requirePin, (req, res) => {
db.prepare(`INSERT OR IGNORE INTO enrollments (student_id, course_id) VALUES (?, ?)`)
.run(req.body.studentId, req.params.id);