const { Chapter, Subject, Lesson } = require('../models'); const { cacheUtils } = require('../config/redis'); /** * Chapter Controller - Quản lý chương học */ class ChapterController { /** * Get all chapters with pagination */ async getAllChapters(req, res, next) { try { const { page = 1, limit = 20, subject_id, is_published } = req.query; const offset = (page - 1) * limit; const cacheKey = `chapters:list:${page}:${limit}:${subject_id || 'all'}:${is_published || 'all'}`; const cached = await cacheUtils.get(cacheKey); if (cached) { return res.json({ success: true, data: cached, cached: true, }); } const where = {}; if (subject_id) where.subject_id = subject_id; if (is_published !== undefined) where.is_published = is_published === 'true'; const { count, rows } = await Chapter.findAndCountAll({ where, include: [ { model: Subject, as: 'subject', attributes: ['id', 'subject_name', 'subject_code'], }, ], limit: parseInt(limit), offset: parseInt(offset), order: [['display_order', 'ASC'], ['chapter_number', 'ASC']], }); const result = { chapters: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit), }, }; await cacheUtils.set(cacheKey, result, 1800); res.json({ success: true, data: result, cached: false, }); } catch (error) { next(error); } } /** * Get chapter by ID */ async getChapterById(req, res, next) { try { const { id } = req.params; const { include_lessons } = req.query; const cacheKey = `chapter:${id}:${include_lessons || 'no'}`; const cached = await cacheUtils.get(cacheKey); if (cached) { return res.json({ success: true, data: cached, cached: true, }); } const includeOptions = [ { model: Subject, as: 'subject', attributes: ['id', 'subject_name', 'subject_code', 'description'], }, ]; if (include_lessons === 'true') { includeOptions.push({ model: Lesson, as: 'lessons', where: { is_published: true }, required: false, order: [['display_order', 'ASC']], }); } const chapter = await Chapter.findByPk(id, { include: includeOptions, }); if (!chapter) { return res.status(404).json({ success: false, message: 'Chapter not found', }); } await cacheUtils.set(cacheKey, chapter, 3600); res.json({ success: true, data: chapter, cached: false, }); } catch (error) { next(error); } } /** * Create new chapter */ async createChapter(req, res, next) { try { const { subject_id, chapter_number, chapter_title, chapter_description, duration_minutes, is_published, display_order, } = req.body; // Validate required fields if (!subject_id || !chapter_number || !chapter_title) { return res.status(400).json({ success: false, message: 'subject_id, chapter_number, and chapter_title are required', }); } // Check if subject exists const subject = await Subject.findByPk(subject_id); if (!subject) { return res.status(404).json({ success: false, message: 'Subject not found', }); } const chapter = await Chapter.create({ subject_id, chapter_number, chapter_title, chapter_description, duration_minutes, is_published: is_published !== undefined ? is_published : false, display_order: display_order || 0, }); // Clear cache await cacheUtils.deletePattern('chapters:*'); res.status(201).json({ success: true, data: chapter, message: 'Chapter created successfully', }); } catch (error) { next(error); } } /** * Update chapter */ async updateChapter(req, res, next) { try { const { id } = req.params; const { chapter_number, chapter_title, chapter_description, duration_minutes, is_published, display_order, } = req.body; const chapter = await Chapter.findByPk(id); if (!chapter) { return res.status(404).json({ success: false, message: 'Chapter not found', }); } await chapter.update({ ...(chapter_number !== undefined && { chapter_number }), ...(chapter_title !== undefined && { chapter_title }), ...(chapter_description !== undefined && { chapter_description }), ...(duration_minutes !== undefined && { duration_minutes }), ...(is_published !== undefined && { is_published }), ...(display_order !== undefined && { display_order }), }); // Clear cache await cacheUtils.deletePattern('chapters:*'); await cacheUtils.deletePattern(`chapter:${id}*`); res.json({ success: true, data: chapter, message: 'Chapter updated successfully', }); } catch (error) { next(error); } } /** * Delete chapter */ async deleteChapter(req, res, next) { try { const { id } = req.params; const chapter = await Chapter.findByPk(id); if (!chapter) { return res.status(404).json({ success: false, message: 'Chapter not found', }); } // Check if chapter has lessons const lessonsCount = await Lesson.count({ where: { chapter_id: id } }); if (lessonsCount > 0) { return res.status(400).json({ success: false, message: `Cannot delete chapter. It has ${lessonsCount} lesson(s). Delete lessons first.`, }); } await chapter.destroy(); // Clear cache await cacheUtils.deletePattern('chapters:*'); await cacheUtils.deletePattern(`chapter:${id}*`); res.json({ success: true, message: 'Chapter deleted successfully', }); } catch (error) { next(error); } } /** * Get lessons in a chapter */ async getLessonsByChapter(req, res, next) { try { const { id } = req.params; const { page = 1, limit = 20, is_published } = req.query; const offset = (page - 1) * limit; const cacheKey = `chapter:${id}:lessons:${page}:${limit}:${is_published || 'all'}`; const cached = await cacheUtils.get(cacheKey); if (cached) { return res.json({ success: true, data: cached, cached: true, }); } const chapter = await Chapter.findByPk(id); if (!chapter) { return res.status(404).json({ success: false, message: 'Chapter not found', }); } const where = { chapter_id: id }; if (is_published !== undefined) where.is_published = is_published === 'true'; const { count, rows } = await Lesson.findAndCountAll({ where, limit: parseInt(limit), offset: parseInt(offset), order: [['display_order', 'ASC'], ['lesson_number', 'ASC']], }); const result = { chapter: { id: chapter.id, chapter_title: chapter.chapter_title, chapter_number: chapter.chapter_number, }, lessons: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit), }, }; await cacheUtils.set(cacheKey, result, 1800); res.json({ success: true, data: result, cached: false, }); } catch (error) { next(error); } } /** * Add lesson to chapter (Create lesson within chapter context) */ async addLessonToChapter(req, res, next) { try { const { chapterId } = req.params; const lessonData = req.body; // Check if chapter exists const chapter = await Chapter.findByPk(chapterId); if (!chapter) { return res.status(404).json({ success: false, message: 'Chapter not found', }); } // Validate required fields if (!lessonData.lesson_number || !lessonData.lesson_title) { return res.status(400).json({ success: false, message: 'lesson_number and lesson_title are required', }); } // Create lesson with chapter_id const lesson = await Lesson.create({ ...lessonData, chapter_id: chapterId, }); // Clear cache await cacheUtils.deletePattern('lessons:*'); await cacheUtils.deletePattern(`chapter:${chapterId}:lessons:*`); res.status(201).json({ success: true, message: 'Lesson added to chapter successfully', data: lesson, }); } catch (error) { next(error); } } /** * Remove lesson from chapter (Delete lesson within chapter context) */ async removeLessonFromChapter(req, res, next) { try { const { chapterId, lessonId } = req.params; // Check if chapter exists const chapter = await Chapter.findByPk(chapterId); if (!chapter) { return res.status(404).json({ success: false, message: 'Chapter not found', }); } // Find lesson const lesson = await Lesson.findOne({ where: { id: lessonId, chapter_id: chapterId, }, }); if (!lesson) { return res.status(404).json({ success: false, message: 'Lesson not found in this chapter', }); } await lesson.destroy(); // Clear cache await cacheUtils.deletePattern('lessons:*'); await cacheUtils.deletePattern(`lesson:${lessonId}*`); await cacheUtils.deletePattern(`chapter:${chapterId}:lessons:*`); res.json({ success: true, message: 'Lesson removed from chapter successfully', }); } catch (error) { next(error); } } } module.exports = new ChapterController();