const { Lesson, Chapter, Subject, Game } = require('../models'); const { Op } = require('sequelize'); /** * Lesson Controller * Quản lý các bài học trong chương */ class LessonController { /** * Lấy danh sách tất cả bài học */ async getAllLessons(req, res, next) { try { const { page = 1, limit = 20, is_published, is_free, lesson_type, search } = req.query; const offset = (page - 1) * limit; const where = {}; // Filters if (is_published !== undefined) { where.is_published = is_published === 'true'; } if (is_free !== undefined) { where.is_free = is_free === 'true'; } if (lesson_type) { where.lesson_type = lesson_type; } if (search) { where.lesson_title = { [Op.like]: `%${search}%` }; } const { rows: lessons, count } = await Lesson.findAndCountAll({ where, include: [ { model: Chapter, as: 'chapter', attributes: ['id', 'chapter_title', 'chapter_number'], include: [ { model: Subject, as: 'subject', attributes: ['id', 'subject_name', 'subject_code'] } ] } ], order: [['display_order', 'ASC'], ['lesson_number', 'ASC']], limit: parseInt(limit), offset: parseInt(offset) }); res.json({ success: true, data: { lessons, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), total_pages: Math.ceil(count / limit) } } }); } catch (error) { next(error); } } /** * Lấy chi tiết bài học */ async getLessonById(req, res, next) { try { const { id } = req.params; const lesson = await Lesson.findByPk(id, { include: [ { model: Chapter, as: 'chapter', include: [ { model: Subject, as: 'subject' } ] } ] }); if (!lesson) { return res.status(404).json({ success: false, message: 'Không tìm thấy bài học' }); } // Nếu là composite lesson, lấy thông tin games cho từng component if (lesson.lesson_type === 'json_content' && lesson.content_json?.type === 'composite_lesson') { const components = lesson.content_json.components || []; // Tìm tất cả game_type trong components const gameTypes = components .filter(c => c.type === 'game' && c.config?.game_type) .map(c => c.config.game_type); if (gameTypes.length > 0) { const games = await Game.findAll({ where: { type: { [Op.in]: gameTypes }, is_active: true }, attributes: ['id', 'title', 'type', 'url', 'thumbnail', 'config'] }); // Gắn game info vào lesson lesson.dataValues.available_games = games; } } res.json({ success: true, data: lesson }); } catch (error) { next(error); } } /** * Lấy danh sách bài học của một chương */ async getLessonsByChapter(req, res, next) { try { const { chapter_id } = req.params; const { include_unpublished = false } = req.query; const where = { chapter_id }; if (!include_unpublished || include_unpublished === 'false') { where.is_published = true; } const lessons = await Lesson.findAll({ where, order: [['display_order', 'ASC'], ['lesson_number', 'ASC']], attributes: { exclude: ['content_json', 'content_url'] // Không trả full content trong list } }); res.json({ success: true, data: lessons }); } catch (error) { next(error); } } /** * Tạo bài học mới */ async createLesson(req, res, next) { try { const lessonData = req.body; // Validate chapter exists const chapter = await Chapter.findByPk(lessonData.chapter_id); if (!chapter) { return res.status(404).json({ success: false, message: 'Không tìm thấy chương học' }); } // Auto increment lesson_number if not provided if (!lessonData.lesson_number) { const maxLesson = await Lesson.findOne({ where: { chapter_id: lessonData.chapter_id }, order: [['lesson_number', 'DESC']] }); lessonData.lesson_number = maxLesson ? maxLesson.lesson_number + 1 : 1; } // Auto increment display_order if not provided if (!lessonData.display_order) { const maxOrder = await Lesson.findOne({ where: { chapter_id: lessonData.chapter_id }, order: [['display_order', 'DESC']] }); lessonData.display_order = maxOrder ? maxOrder.display_order + 1 : 1; } const lesson = await Lesson.create(lessonData); res.status(201).json({ success: true, message: 'Tạo bài học thành công', data: lesson }); } catch (error) { next(error); } } /** * Cập nhật bài học */ async updateLesson(req, res, next) { try { const { id } = req.params; const updateData = req.body; const lesson = await Lesson.findByPk(id); if (!lesson) { return res.status(404).json({ success: false, message: 'Không tìm thấy bài học' }); } // Validate chapter if changing if (updateData.chapter_id && updateData.chapter_id !== lesson.chapter_id) { const chapter = await Chapter.findByPk(updateData.chapter_id); if (!chapter) { return res.status(404).json({ success: false, message: 'Không tìm thấy chương học' }); } } await lesson.update(updateData); res.json({ success: true, message: 'Cập nhật bài học thành công', data: lesson }); } catch (error) { next(error); } } /** * Xóa bài học */ async deleteLesson(req, res, next) { try { const { id } = req.params; const lesson = await Lesson.findByPk(id); if (!lesson) { return res.status(404).json({ success: false, message: 'Không tìm thấy bài học' }); } await lesson.destroy(); res.json({ success: true, message: 'Xóa bài học thành công' }); } catch (error) { next(error); } } /** * Đánh dấu hoàn thành bài học */ async completeLesson(req, res, next) { try { const { id } = req.params; const userId = req.user.id; // From auth middleware const { score, time_spent, results_data } = req.body; const lesson = await Lesson.findByPk(id); if (!lesson) { return res.status(404).json({ success: false, message: 'Không tìm thấy bài học' }); } // TODO: Save to UserLessonProgress table // This would track completion, score, etc. res.json({ success: true, message: 'Đã hoàn thành bài học', data: { lesson_id: id, user_id: userId, score, time_spent, completed_at: new Date() } }); } catch (error) { next(error); } } /** * Lấy game phù hợp cho lesson */ async getMatchingGames(req, res, next) { try { const { id } = req.params; const lesson = await Lesson.findByPk(id); if (!lesson) { return res.status(404).json({ success: false, message: 'Không tìm thấy bài học' }); } if (lesson.lesson_type !== 'json_content' || !lesson.content_json?.type) { return res.json({ success: true, data: [], message: 'Bài học này không sử dụng game engine' }); } const lessonType = lesson.content_json.type; // Handle composite lesson if (lessonType === 'composite_lesson') { const components = lesson.content_json.components || []; const gameTypes = components .filter(c => c.type === 'game' && c.config?.game_type) .map(c => c.config.game_type); const games = await Game.findAll({ where: { type: { [Op.in]: gameTypes }, is_active: true } }); return res.json({ success: true, data: games }); } // Handle single component lesson const games = await Game.findAll({ where: { type: lessonType, is_active: true } }); res.json({ success: true, data: games }); } catch (error) { next(error); } } } module.exports = new LessonController();