2026-01-20 13:39:42 +07:00
|
|
|
import { prisma } from '../config/database';
|
|
|
|
|
import { Prisma } from '@prisma/client';
|
|
|
|
|
import { config } from '../config';
|
|
|
|
|
import { logger } from '../config/logger';
|
2026-01-20 16:51:42 +07:00
|
|
|
import { generateFilePath, uploadFile } from '../config/minio';
|
2026-01-20 13:39:42 +07:00
|
|
|
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
|
|
|
|
|
import jwt from 'jsonwebtoken';
|
|
|
|
|
import {
|
|
|
|
|
LessonAttachmentData,
|
|
|
|
|
LessonData,
|
|
|
|
|
ChapterData,
|
|
|
|
|
CreateLessonInput,
|
|
|
|
|
UpdateLessonInput,
|
|
|
|
|
CreateChapterInput,
|
|
|
|
|
UpdateChapterInput,
|
|
|
|
|
ChaptersRequest,
|
|
|
|
|
DeleteChapterRequest,
|
|
|
|
|
ReorderChapterRequest,
|
|
|
|
|
ListChaptersResponse,
|
|
|
|
|
GetChapterResponse,
|
|
|
|
|
CreateChapterResponse,
|
|
|
|
|
UpdateChapterResponse,
|
|
|
|
|
DeleteChapterResponse,
|
|
|
|
|
ReorderChapterResponse,
|
|
|
|
|
ChapterWithLessonsResponse,
|
|
|
|
|
ListLessonsRequest,
|
|
|
|
|
GetLessonRequest,
|
|
|
|
|
CreateLessonRequest,
|
|
|
|
|
UpdateLessonRequest,
|
|
|
|
|
DeleteLessonRequest,
|
|
|
|
|
ReorderLessonsRequest,
|
|
|
|
|
ListLessonsResponse,
|
|
|
|
|
GetLessonResponse,
|
|
|
|
|
CreateLessonResponse,
|
|
|
|
|
UpdateLessonResponse,
|
|
|
|
|
DeleteLessonResponse,
|
|
|
|
|
ReorderLessonsResponse,
|
|
|
|
|
} from "../types/ChaptersLesson.typs";
|
2026-01-20 16:51:42 +07:00
|
|
|
import { CoursesInstructorService } from './CoursesInstructor.service';
|
2026-01-20 13:39:42 +07:00
|
|
|
|
|
|
|
|
export class ChaptersLessonService {
|
|
|
|
|
async listChapters(request: ChaptersRequest): Promise<ListChaptersResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const { token, course_id } = request;
|
|
|
|
|
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
|
|
|
|
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new UnauthorizedError('Invalid token');
|
|
|
|
|
}
|
2026-01-20 16:51:42 +07:00
|
|
|
const chapters = await prisma.chapter.findMany({
|
|
|
|
|
where: { course_id }, orderBy: { sort_order: 'asc' },
|
|
|
|
|
include: { lessons: { orderBy: { sort_order: 'asc' } } }
|
|
|
|
|
});
|
2026-01-20 13:39:42 +07:00
|
|
|
return { code: 200, message: 'Chapters fetched successfully', data: chapters as ChapterData[], total: chapters.length };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Error fetching chapters: ${error}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 16:51:42 +07:00
|
|
|
async createChapter(request: CreateChapterInput): Promise<CreateChapterResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const { token, course_id, title, description, sort_order } = request;
|
|
|
|
|
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
|
|
|
|
await CoursesInstructorService.validateCourseStatus(course_id);
|
|
|
|
|
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new UnauthorizedError('Invalid token');
|
|
|
|
|
}
|
|
|
|
|
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
|
|
|
|
if (!courseInstructor) {
|
|
|
|
|
throw new ForbiddenError('You are not permitted to create chapter');
|
|
|
|
|
}
|
|
|
|
|
const chapter = await prisma.chapter.create({ data: { course_id, title, description, sort_order } });
|
|
|
|
|
return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Error creating chapter: ${error}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateChapter(request: UpdateChapterInput): Promise<UpdateChapterResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const { token, course_id, chapter_id, title, description, sort_order } = request;
|
|
|
|
|
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
|
|
|
|
await CoursesInstructorService.validateCourseStatus(course_id);
|
|
|
|
|
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new UnauthorizedError('Invalid token');
|
|
|
|
|
}
|
|
|
|
|
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
|
|
|
|
if (!courseInstructor) {
|
|
|
|
|
throw new ForbiddenError('You are not permitted to update chapter');
|
|
|
|
|
}
|
|
|
|
|
const chapter = await prisma.chapter.update({ where: { id: chapter_id }, data: { title, description, sort_order } });
|
|
|
|
|
return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Error updating chapter: ${error}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteChapter(request: DeleteChapterRequest): Promise<DeleteChapterResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const { token, course_id, chapter_id } = request;
|
|
|
|
|
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
|
|
|
|
await CoursesInstructorService.validateCourseStatus(course_id);
|
|
|
|
|
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new UnauthorizedError('Invalid token');
|
|
|
|
|
}
|
|
|
|
|
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
|
|
|
|
if (!courseInstructor) {
|
|
|
|
|
throw new ForbiddenError('You are not permitted to delete chapter');
|
|
|
|
|
}
|
|
|
|
|
await prisma.chapter.delete({ where: { id: chapter_id } });
|
|
|
|
|
return { code: 200, message: 'Chapter deleted successfully' };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Error deleting chapter: ${error}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async reorderChapter(request: ReorderChapterRequest): Promise<ReorderChapterResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const { token, course_id, chapter_id, sort_order } = request;
|
|
|
|
|
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
|
|
|
|
await CoursesInstructorService.validateCourseStatus(course_id);
|
|
|
|
|
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new UnauthorizedError('Invalid token');
|
|
|
|
|
}
|
|
|
|
|
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
|
|
|
|
if (!courseInstructor) {
|
|
|
|
|
throw new ForbiddenError('You are not permitted to reorder chapter');
|
|
|
|
|
}
|
|
|
|
|
const chapter = await prisma.chapter.update({ where: { id: chapter_id }, data: { sort_order } });
|
|
|
|
|
return { code: 200, message: 'Chapter reordered successfully', data: [chapter as ChapterData] };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Error reordering chapter: ${error}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createLesson(request: CreateLessonInput): Promise<CreateLessonResponse> {
|
|
|
|
|
try {
|
|
|
|
|
const { token, course_id, chapter_id, title, content, type, sort_order, video, attachments } = request;
|
|
|
|
|
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
|
|
|
|
await CoursesInstructorService.validateCourseStatus(course_id);
|
|
|
|
|
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new UnauthorizedError('Invalid token');
|
|
|
|
|
}
|
|
|
|
|
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
|
|
|
|
if (!courseInstructor) {
|
|
|
|
|
throw new ForbiddenError('You are not permitted to create lesson');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the lesson first
|
|
|
|
|
const lesson = await prisma.lesson.create({
|
|
|
|
|
data: { chapter_id, title, content, type, sort_order }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const uploadedAttachments: { file_name: string; file_path: string; file_size: number; mime_type: string }[] = [];
|
|
|
|
|
|
|
|
|
|
// Handle video upload for VIDEO type lessons
|
|
|
|
|
if (type === 'VIDEO' && video) {
|
|
|
|
|
const videoPath = generateFilePath(course_id, lesson.id, 'video', video.originalname);
|
|
|
|
|
await uploadFile(videoPath, video.buffer, video.mimetype);
|
|
|
|
|
|
|
|
|
|
// Save video as attachment
|
|
|
|
|
await prisma.lessonAttachment.create({
|
|
|
|
|
data: {
|
|
|
|
|
lesson_id: lesson.id,
|
|
|
|
|
file_name: video.originalname,
|
|
|
|
|
file_size: video.size,
|
|
|
|
|
mime_type: video.mimetype,
|
|
|
|
|
file_path: videoPath,
|
|
|
|
|
sort_order: 0,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
uploadedAttachments.push({
|
|
|
|
|
file_name: video.originalname,
|
|
|
|
|
file_path: videoPath,
|
|
|
|
|
file_size: video.size,
|
|
|
|
|
mime_type: video.mimetype,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle additional attachments (PDFs, documents, etc.)
|
|
|
|
|
if (attachments && attachments.length > 0) {
|
|
|
|
|
for (let i = 0; i < attachments.length; i++) {
|
|
|
|
|
const attachment = attachments[i];
|
|
|
|
|
const attachmentPath = generateFilePath(course_id, lesson.id, 'attachment', attachment.originalname);
|
|
|
|
|
await uploadFile(attachmentPath, attachment.buffer, attachment.mimetype);
|
|
|
|
|
|
|
|
|
|
await prisma.lessonAttachment.create({
|
|
|
|
|
data: {
|
|
|
|
|
lesson_id: lesson.id,
|
|
|
|
|
file_name: attachment.originalname,
|
|
|
|
|
file_size: attachment.size,
|
|
|
|
|
mime_type: attachment.mimetype,
|
|
|
|
|
file_path: attachmentPath,
|
|
|
|
|
sort_order: i + 1, // Start from 1 since video is 0
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
uploadedAttachments.push({
|
|
|
|
|
file_name: attachment.originalname,
|
|
|
|
|
file_path: attachmentPath,
|
|
|
|
|
file_size: attachment.size,
|
|
|
|
|
mime_type: attachment.mimetype,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (type === 'QUIZ' && request.quiz_data) {
|
|
|
|
|
// Create Quiz with Questions and Choices
|
|
|
|
|
const { quiz_data } = request;
|
|
|
|
|
const userId = decodedToken.id;
|
|
|
|
|
|
|
|
|
|
// Create the quiz
|
|
|
|
|
const quiz = await prisma.quiz.create({
|
|
|
|
|
data: {
|
|
|
|
|
lesson_id: lesson.id,
|
|
|
|
|
title: quiz_data.title,
|
|
|
|
|
description: quiz_data.description,
|
|
|
|
|
passing_score: quiz_data.passing_score ?? 60,
|
|
|
|
|
time_limit: quiz_data.time_limit,
|
|
|
|
|
shuffle_questions: quiz_data.shuffle_questions ?? false,
|
|
|
|
|
shuffle_choices: quiz_data.shuffle_choices ?? false,
|
|
|
|
|
show_answers_after_completion: quiz_data.show_answers_after_completion ?? true,
|
|
|
|
|
created_by: userId,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create questions with choices
|
|
|
|
|
if (quiz_data.questions && quiz_data.questions.length > 0) {
|
|
|
|
|
for (let i = 0; i < quiz_data.questions.length; i++) {
|
|
|
|
|
const questionInput = quiz_data.questions[i];
|
|
|
|
|
|
|
|
|
|
// Create the question
|
|
|
|
|
const question = await prisma.question.create({
|
|
|
|
|
data: {
|
|
|
|
|
quiz_id: quiz.id,
|
|
|
|
|
question: questionInput.question,
|
|
|
|
|
explanation: questionInput.explanation,
|
|
|
|
|
question_type: questionInput.question_type,
|
|
|
|
|
score: questionInput.score ?? 1,
|
|
|
|
|
sort_order: questionInput.sort_order ?? i,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create choices for this question
|
|
|
|
|
if (questionInput.choices && questionInput.choices.length > 0) {
|
|
|
|
|
for (let j = 0; j < questionInput.choices.length; j++) {
|
|
|
|
|
const choiceInput = questionInput.choices[j];
|
|
|
|
|
await prisma.choice.create({
|
|
|
|
|
data: {
|
|
|
|
|
question_id: question.id,
|
|
|
|
|
text: choiceInput.text,
|
|
|
|
|
is_correct: choiceInput.is_correct,
|
|
|
|
|
sort_order: choiceInput.sort_order ?? j,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch the complete lesson with attachments and quiz
|
|
|
|
|
const completeLesson = await prisma.lesson.findUnique({
|
|
|
|
|
where: { id: lesson.id },
|
|
|
|
|
include: {
|
|
|
|
|
attachments: { orderBy: { sort_order: 'asc' } },
|
|
|
|
|
quiz: {
|
|
|
|
|
include: {
|
|
|
|
|
questions: {
|
|
|
|
|
orderBy: { sort_order: 'asc' },
|
|
|
|
|
include: {
|
|
|
|
|
choices: { orderBy: { sort_order: 'asc' } }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { code: 200, message: 'Lesson created successfully', data: completeLesson as LessonData };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Error creating lesson: ${error}`);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-20 13:39:42 +07:00
|
|
|
}
|