428 lines
11 KiB
JavaScript
428 lines
11 KiB
JavaScript
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();
|