const db = require('../db/db'); /* ══════════════════════════════════════════════════════════════════════ COURSE TEMPLATES ══════════════════════════════════════════════════════════════════════ */ /* ── GET /api/templates/courses ──────────────────────────────────────── */ function listCourseTemplates(req, res) { const uid = req.user.id; const { my, subject } = req.query; let where; const args = []; if (my) { where = 'WHERE ct.created_by = ?'; args.push(uid); } else if (subject) { where = 'WHERE (ct.is_public = 1 OR ct.created_by = ?) AND ct.subject_slug = ?'; args.push(uid, subject); } else { where = 'WHERE ct.is_public = 1 OR ct.created_by = ?'; args.push(uid); } const rows = db.prepare(` SELECT ct.*, u.name AS creator_name FROM course_templates ct LEFT JOIN users u ON ct.created_by = u.id ${where} ORDER BY ct.created_at DESC `).all(...args); res.json(rows.map(r => ({ id: r.id, title: r.title, description: r.description || '', category: r.category, subjectSlug: r.subject_slug, structure: safeJSON(r.structure, {}), isPublic: r.is_public === 1, createdBy: r.created_by, creatorName: r.creator_name || '', createdAt: r.created_at, }))); } /* ── POST /api/templates/courses ─────────────────────────────────────── */ function saveCourseTemplate(req, res) { const uid = req.user.id; const { title, description, category, subject_slug, courseId } = req.body; if (!title) return res.status(400).json({ error: 'title required' }); let structure = {}; if (courseId) { // Snapshot course structure const course = db.prepare('SELECT * FROM courses WHERE id = ?').get(courseId); if (!course) return res.status(404).json({ error: 'Course not found' }); const sections = db.prepare( 'SELECT * FROM course_sections WHERE course_id = ? ORDER BY order_index, id' ).all(courseId); const lessons = db.prepare( 'SELECT * FROM lessons WHERE course_id = ? ORDER BY order_index, id' ).all(courseId); const sectionArr = sections.map(s => { const sectionLessons = lessons.filter(l => l.section_id === s.id); return { title: s.title, lessons: sectionLessons.map(l => ({ title: l.title, blocks: db.prepare( 'SELECT type, data FROM lesson_blocks WHERE lesson_id = ? ORDER BY order_index, id' ).all(l.id).map(b => ({ type: b.type, data: safeJSON(b.data, {}) })), })), }; }); // Lessons without a section const unsectioned = lessons.filter(l => !l.section_id); if (unsectioned.length) { sectionArr.unshift({ title: null, lessons: unsectioned.map(l => ({ title: l.title, blocks: db.prepare( 'SELECT type, data FROM lesson_blocks WHERE lesson_id = ? ORDER BY order_index, id' ).all(l.id).map(b => ({ type: b.type, data: safeJSON(b.data, {}) })), })), }); } structure = { sections: sectionArr }; } const r = db.prepare(` INSERT INTO course_templates (title, description, category, subject_slug, structure, is_public, created_by) VALUES (?, ?, ?, ?, ?, 1, ?) `).run( title.trim(), description || null, category || 'general', subject_slug || null, JSON.stringify(structure), uid ); res.status(201).json({ id: r.lastInsertRowid }); } /* ── POST /api/templates/courses/:id/create ──────────────────────────── */ function createFromCourseTemplate(req, res) { const tpl = db.prepare('SELECT * FROM course_templates WHERE id = ?').get(req.params.id); if (!tpl) return res.status(404).json({ error: 'Template not found' }); const { title, subjectSlug } = req.body; const structure = safeJSON(tpl.structure, {}); const sections = structure.sections || []; let newCourseId; db.transaction(() => { const cr = db.prepare(` INSERT INTO courses (subject_slug, title, description, cover_emoji, order_index, is_published, created_by) VALUES (?, ?, ?, '', 0, 0, ?) `).run( subjectSlug || tpl.subject_slug || 'other', title || tpl.title, tpl.description || null, req.user.id ); newCourseId = cr.lastInsertRowid; let lessonOrder = 0; for (const sec of sections) { let sectionId = null; if (sec.title) { const sr = db.prepare( 'INSERT INTO course_sections (course_id, title, order_index) VALUES (?, ?, ?)' ).run(newCourseId, sec.title, lessonOrder); sectionId = sr.lastInsertRowid; } for (const lesson of (sec.lessons || [])) { const lr = db.prepare( 'INSERT INTO lessons (course_id, title, order_index, section_id) VALUES (?, ?, ?, ?)' ).run(newCourseId, lesson.title, lessonOrder++, sectionId); const newLid = lr.lastInsertRowid; (lesson.blocks || []).forEach((b, i) => { db.prepare( 'INSERT INTO lesson_blocks (lesson_id, type, order_index, data) VALUES (?, ?, ?, ?)' ).run(newLid, b.type, i, JSON.stringify(b.data || {})); }); } } })(); res.status(201).json({ id: newCourseId }); } /* ── DELETE /api/templates/courses/:id ────────────────────────────────── */ function deleteCourseTemplate(req, res) { const tpl = db.prepare('SELECT * FROM course_templates WHERE id = ?').get(req.params.id); if (!tpl) return res.status(404).json({ error: 'Template not found' }); if (tpl.created_by !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' }); db.prepare('DELETE FROM course_templates WHERE id = ?').run(tpl.id); res.json({ ok: true }); } /* ══════════════════════════════════════════════════════════════════════ LESSON TEMPLATES ══════════════════════════════════════════════════════════════════════ */ /* ── GET /api/templates/lessons ──────────────────────────────────────── */ function listLessonTemplates(req, res) { const uid = req.user.id; const { my, category } = req.query; let where; const args = []; if (my) { where = 'WHERE lt.created_by = ?'; args.push(uid); } else if (category) { where = 'WHERE (lt.is_public = 1 OR lt.created_by = ?) AND lt.category = ?'; args.push(uid, category); } else { where = 'WHERE lt.is_public = 1 OR lt.created_by = ?'; args.push(uid); } const rows = db.prepare(` SELECT lt.*, u.name AS creator_name FROM lesson_templates lt LEFT JOIN users u ON lt.created_by = u.id ${where} ORDER BY lt.created_at DESC `).all(...args); res.json(rows.map(r => ({ id: r.id, title: r.title, category: r.category, subjectSlug: r.subject_slug, blocks: safeJSON(r.blocks, []), isPublic: r.is_public === 1, createdBy: r.created_by, creatorName: r.creator_name || '', createdAt: r.created_at, }))); } /* ── POST /api/templates/lessons ─────────────────────────────────────── */ function saveLessonTemplate(req, res) { const uid = req.user.id; const { title, category, subject_slug, lessonId } = req.body; if (!title) return res.status(400).json({ error: 'title required' }); let blocksJSON = '[]'; if (lessonId) { const lesson = db.prepare('SELECT id FROM lessons WHERE id = ?').get(lessonId); if (!lesson) return res.status(404).json({ error: 'Lesson not found' }); const rawBlocks = db.prepare( 'SELECT type, data FROM lesson_blocks WHERE lesson_id = ? ORDER BY order_index, id' ).all(lesson.id); blocksJSON = JSON.stringify(rawBlocks.map(b => ({ type: b.type, data: safeJSON(b.data, {}), }))); } const r = db.prepare(` INSERT INTO lesson_templates (title, category, subject_slug, blocks, is_public, created_by) VALUES (?, ?, ?, ?, 1, ?) `).run( title.trim(), category || 'general', subject_slug || null, blocksJSON, uid ); res.status(201).json({ id: r.lastInsertRowid }); } /* ── POST /api/templates/lessons/:id/create ──────────────────────────── */ function createFromLessonTemplate(req, res) { const tpl = db.prepare('SELECT * FROM lesson_templates WHERE id = ?').get(req.params.id); if (!tpl) return res.status(404).json({ error: 'Template not found' }); const { courseId, sectionId, title } = req.body; if (!courseId) return res.status(400).json({ error: 'courseId required' }); const course = db.prepare('SELECT id FROM courses WHERE id = ?').get(courseId); if (!course) return res.status(404).json({ error: 'Course not found' }); const tplBlocks = safeJSON(tpl.blocks, []); let newLessonId; db.transaction(() => { // Get max order_index const maxOrd = db.prepare( 'SELECT MAX(order_index) AS mx FROM lessons WHERE course_id = ?' ).get(courseId); const lr = db.prepare( 'INSERT INTO lessons (course_id, title, order_index, section_id) VALUES (?, ?, ?, ?)' ).run(courseId, title || tpl.title, (maxOrd?.mx ?? -1) + 1, sectionId || null); newLessonId = lr.lastInsertRowid; tplBlocks.forEach((b, i) => { db.prepare( 'INSERT INTO lesson_blocks (lesson_id, type, order_index, data) VALUES (?, ?, ?, ?)' ).run(newLessonId, b.type, i, JSON.stringify(b.data || {})); }); })(); res.status(201).json({ id: newLessonId }); } /* ── DELETE /api/templates/lessons/:id ────────────────────────────────── */ function deleteLessonTemplate(req, res) { const tpl = db.prepare('SELECT * FROM lesson_templates WHERE id = ?').get(req.params.id); if (!tpl) return res.status(404).json({ error: 'Template not found' }); if (tpl.created_by !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' }); db.prepare('DELETE FROM lesson_templates WHERE id = ?').run(tpl.id); res.json({ ok: true }); } /* ── helpers ─────────────────────────────────────────────────────────── */ function safeJSON(str, fallback) { try { return JSON.parse(str); } catch { return fallback; } } module.exports = { listCourseTemplates, saveCourseTemplate, createFromCourseTemplate, deleteCourseTemplate, listLessonTemplates, saveLessonTemplate, createFromLessonTemplate, deleteLessonTemplate, };