2026-01-20 16:51:42 +07:00
|
|
|
import { Request, Response, NextFunction } from 'express';
|
|
|
|
|
import { ChaptersLessonService } from '../services/ChaptersLesson.service';
|
|
|
|
|
import { ValidationError } from '../middleware/errorHandler';
|
|
|
|
|
import {
|
|
|
|
|
lessonUpload,
|
|
|
|
|
LessonUploadRequest,
|
|
|
|
|
validateTotalAttachmentSize,
|
|
|
|
|
validateVideoSize
|
|
|
|
|
} from '../middleware/upload';
|
|
|
|
|
import { UploadedFileInfo, CreateLessonInput } from '../types/ChaptersLesson.typs';
|
|
|
|
|
|
|
|
|
|
const chaptersLessonService = new ChaptersLessonService();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Controller for handling lesson CRUD operations with file uploads
|
|
|
|
|
*/
|
|
|
|
|
export class LessonsController {
|
|
|
|
|
/**
|
|
|
|
|
* สร้างบทเรียนใหม่พร้อมไฟล์แนบ
|
|
|
|
|
* Create a new lesson with optional video and attachments
|
|
|
|
|
*
|
|
|
|
|
* @route POST /api/instructors/courses/:courseId/chapters/:chapterId/lessons
|
|
|
|
|
* @contentType multipart/form-data
|
|
|
|
|
*
|
|
|
|
|
* @param {number} courseId - รหัสคอร์ส / Course ID
|
|
|
|
|
* @param {number} chapterId - รหัสบท / Chapter ID
|
|
|
|
|
* @param {string} title - ชื่อบทเรียน (JSON: { th: "", en: "" })
|
|
|
|
|
* @param {string} [content] - เนื้อหาบทเรียน (JSON: { th: "", en: "" })
|
|
|
|
|
* @param {string} type - ประเภทบทเรียน (VIDEO | QUIZ)
|
|
|
|
|
* @param {number} [sort_order] - ลำดับการแสดงผล
|
|
|
|
|
* @param {File} [video] - ไฟล์วิดีโอ (สำหรับ type=VIDEO เท่านั้น)
|
|
|
|
|
* @param {File[]} [attachments] - ไฟล์แนบ (PDFs, เอกสาร, รูปภาพ)
|
|
|
|
|
*/
|
|
|
|
|
async createLesson(req: LessonUploadRequest, res: Response, next: NextFunction): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
|
|
|
if (!token) {
|
|
|
|
|
throw new ValidationError('No token provided');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 10:03:06 +00:00
|
|
|
const courseId = parseInt(req.params.courseId as string, 10);
|
|
|
|
|
const chapterId = parseInt(req.params.chapterId as string, 10);
|
2026-01-20 16:51:42 +07:00
|
|
|
|
|
|
|
|
if (isNaN(courseId) || isNaN(chapterId)) {
|
|
|
|
|
throw new ValidationError('Invalid course ID or chapter ID');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse JSON fields from multipart form
|
|
|
|
|
const title = JSON.parse(req.body.title || '{}');
|
|
|
|
|
const content = req.body.content ? JSON.parse(req.body.content) : undefined;
|
|
|
|
|
const type = req.body.type as 'VIDEO' | 'QUIZ';
|
|
|
|
|
const sortOrder = req.body.sort_order ? parseInt(req.body.sort_order, 10) : undefined;
|
|
|
|
|
|
|
|
|
|
if (!type || !['VIDEO', 'QUIZ'].includes(type)) {
|
|
|
|
|
throw new ValidationError('Invalid lesson type. Must be VIDEO or QUIZ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!title.th || !title.en) {
|
|
|
|
|
throw new ValidationError('Title must have both Thai (th) and English (en) values');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process uploaded files
|
|
|
|
|
const files = req.files as { video?: Express.Multer.File[]; attachments?: Express.Multer.File[] } | undefined;
|
|
|
|
|
|
|
|
|
|
let video: UploadedFileInfo | undefined;
|
|
|
|
|
let attachments: UploadedFileInfo[] | undefined;
|
|
|
|
|
|
|
|
|
|
if (files?.video && files.video.length > 0) {
|
|
|
|
|
const videoFile = files.video[0];
|
|
|
|
|
validateVideoSize(videoFile);
|
|
|
|
|
video = {
|
|
|
|
|
originalname: videoFile.originalname,
|
|
|
|
|
mimetype: videoFile.mimetype,
|
|
|
|
|
size: videoFile.size,
|
|
|
|
|
buffer: videoFile.buffer,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (files?.attachments && files.attachments.length > 0) {
|
|
|
|
|
validateTotalAttachmentSize(files.attachments);
|
|
|
|
|
attachments = files.attachments.map(file => ({
|
|
|
|
|
originalname: file.originalname,
|
|
|
|
|
mimetype: file.mimetype,
|
|
|
|
|
size: file.size,
|
|
|
|
|
buffer: file.buffer,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate VIDEO type must have video file (optional - can be uploaded later)
|
|
|
|
|
// if (type === 'VIDEO' && !video) {
|
|
|
|
|
// throw new ValidationError('Video file is required for VIDEO type lessons');
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
const input: CreateLessonInput = {
|
|
|
|
|
token,
|
|
|
|
|
course_id: courseId,
|
|
|
|
|
chapter_id: chapterId,
|
|
|
|
|
title,
|
|
|
|
|
content,
|
|
|
|
|
type,
|
|
|
|
|
sort_order: sortOrder,
|
|
|
|
|
video,
|
|
|
|
|
attachments,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const result = await chaptersLessonService.createLesson(input);
|
|
|
|
|
res.status(201).json(result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
next(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Express router middleware wrapper for file upload
|
|
|
|
|
* Use this in routes like:
|
|
|
|
|
*
|
|
|
|
|
* router.post(
|
|
|
|
|
* '/api/instructors/courses/:courseId/chapters/:chapterId/lessons',
|
|
|
|
|
* authenticateMiddleware,
|
|
|
|
|
* handleLessonUpload,
|
|
|
|
|
* lessonsController.createLesson.bind(lessonsController)
|
|
|
|
|
* );
|
|
|
|
|
*/
|
|
|
|
|
export const handleLessonUpload = (req: Request, res: Response, next: NextFunction) => {
|
|
|
|
|
lessonUpload(req, res, (err) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
error: {
|
|
|
|
|
code: 'FILE_UPLOAD_ERROR',
|
|
|
|
|
message: err.message,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const lessonsController = new LessonsController();
|