update api chapterlesson

This commit is contained in:
JakkrapartXD 2026-01-22 15:56:56 +07:00
parent 2fc0fb7a76
commit 5c2b5d55aa
11 changed files with 855 additions and 85 deletions

View file

@ -165,7 +165,7 @@ export class ChaptersLessonService {
async reorderChapter(request: ReorderChapterRequest): Promise<ReorderChapterResponse> {
try {
const { token, course_id, chapter_id, sort_order } = request;
const { token, course_id, chapter_id, sort_order: newSortOrder } = 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 } });
@ -176,8 +176,54 @@ export class ChaptersLessonService {
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] };
// Get current chapter to find its current sort_order
const currentChapter = await prisma.chapter.findUnique({ where: { id: chapter_id } });
if (!currentChapter) {
throw new NotFoundError('Chapter not found');
}
const oldSortOrder = currentChapter.sort_order;
// If same position, no need to reorder
if (oldSortOrder === newSortOrder) {
const chapters = await prisma.chapter.findMany({
where: { course_id },
orderBy: { sort_order: 'asc' }
});
return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] };
}
// Shift other chapters to make room for the insert
if (oldSortOrder > newSortOrder) {
// Moving up: shift chapters between newSortOrder and oldSortOrder-1 down by 1
await prisma.chapter.updateMany({
where: {
course_id,
sort_order: { gte: newSortOrder, lt: oldSortOrder }
},
data: { sort_order: { increment: 1 } }
});
} else {
// Moving down: shift chapters between oldSortOrder+1 and newSortOrder up by 1
await prisma.chapter.updateMany({
where: {
course_id,
sort_order: { gt: oldSortOrder, lte: newSortOrder }
},
data: { sort_order: { decrement: 1 } }
});
}
// Update the target chapter to the new position
await prisma.chapter.update({ where: { id: chapter_id }, data: { sort_order: newSortOrder } });
// Fetch all chapters with updated order
const chapters = await prisma.chapter.findMany({
where: { course_id },
orderBy: { sort_order: 'asc' }
});
return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] };
} catch (error) {
logger.error(`Error reordering chapter: ${error}`);
throw error;
@ -315,7 +361,7 @@ export class ChaptersLessonService {
*/
async reorderLessons(request: ReorderLessonsRequest): Promise<ReorderLessonsResponse> {
try {
const { token, course_id, chapter_id, lesson_ids } = request;
const { token, course_id, chapter_id, lesson_id, sort_order: newSortOrder } = request;
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
await CoursesInstructorService.validateCourseStatus(course_id);
@ -331,17 +377,52 @@ export class ChaptersLessonService {
throw new NotFoundError('Chapter not found');
}
// Update sort_order for each lesson
for (let i = 0; i < lesson_ids.length; i++) {
await prisma.lesson.update({
where: { id: lesson_ids[i] },
data: { sort_order: i }
// Get current lesson to find its current sort_order
const currentLesson = await prisma.lesson.findUnique({ where: { id: lesson_id } });
if (!currentLesson) {
throw new NotFoundError('Lesson not found');
}
if (currentLesson.chapter_id !== chapter_id) {
throw new NotFoundError('Lesson not found in this chapter');
}
const oldSortOrder = currentLesson.sort_order;
// If same position, no need to reorder
if (oldSortOrder === newSortOrder) {
const lessons = await prisma.lesson.findMany({
where: { chapter_id },
orderBy: { sort_order: 'asc' }
});
return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] };
}
// Shift other lessons to make room for the insert
if (oldSortOrder > newSortOrder) {
// Moving up: shift lessons between newSortOrder and oldSortOrder-1 down by 1
await prisma.lesson.updateMany({
where: {
chapter_id,
sort_order: { gte: newSortOrder, lt: oldSortOrder }
},
data: { sort_order: { increment: 1 } }
});
} else {
// Moving down: shift lessons between oldSortOrder+1 and newSortOrder up by 1
await prisma.lesson.updateMany({
where: {
chapter_id,
sort_order: { gt: oldSortOrder, lte: newSortOrder }
},
data: { sort_order: { decrement: 1 } }
});
}
// Fetch reordered lessons
// Update the target lesson to the new position
await prisma.lesson.update({ where: { id: lesson_id }, data: { sort_order: newSortOrder } });
// Fetch all lessons with updated order
const lessons = await prisma.lesson.findMany({
where: { chapter_id: chapter_id },
where: { chapter_id },
orderBy: { sort_order: 'asc' }
});
@ -498,6 +579,7 @@ export class ChaptersLessonService {
await CoursesInstructorService.validateCourseStatus(course_id);
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
logger.info(`User: ${user}`);
if (!user) throw new UnauthorizedError('Invalid token');
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);

View file

@ -22,6 +22,8 @@ import {
CompleteLessonInput,
CompleteLessonResponse,
} from "../types/CoursesStudent.types";
import { getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } from '../config/minio';
export class CoursesStudentService {
async enrollCourse(input: EnrollCourseInput): Promise<EnrollCourseResponse> {
@ -265,6 +267,8 @@ export class CoursesStudentService {
const { token, course_id, lesson_id } = input;
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
// Import MinIO functions
// Check enrollment
const enrollment = await prisma.enrollment.findUnique({
where: {
@ -319,6 +323,80 @@ export class CoursesStudentService {
const prevLessonId = currentIndex > 0 ? allLessons[currentIndex - 1].id : null;
const nextLessonId = currentIndex < allLessons.length - 1 ? allLessons[currentIndex + 1].id : null;
// Get course_id from chapter
const chapter_course_id = lesson.chapter.course_id;
// Import additional MinIO functions
// Using MinIO functions imported above
// Get video URL from video folder (first file)
let video_url: string | null = null;
try {
const videoPrefix = getVideoFolder(chapter_course_id, lesson_id);
const videoFiles = await listObjects(videoPrefix);
if (videoFiles.length > 0) {
// Get presigned URL for the first video file
video_url = await getPresignedUrl(videoFiles[0].name, 3600);
}
} catch (err) {
logger.error(`Failed to get video from MinIO: ${err}`);
}
// Get attachments from MinIO folder
const attachmentsWithUrls: {
file_name: string;
file_path: string;
file_size: number;
mime_type: string;
presigned_url: string | null;
}[] = [];
try {
const attachmentsPrefix = getAttachmentsFolder(chapter_course_id, lesson_id);
const attachmentFiles = await listObjects(attachmentsPrefix);
for (const file of attachmentFiles) {
let presigned_url: string | null = null;
try {
presigned_url = await getPresignedUrl(file.name, 3600);
} catch (err) {
logger.error(`Failed to generate presigned URL for ${file.name}: ${err}`);
}
// Extract filename from path
const fileName = file.name.split('/').pop() || file.name;
// Guess mime type from extension
const ext = fileName.split('.').pop()?.toLowerCase() || '';
const mimeTypes: { [key: string]: string } = {
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'ppt': 'application/vnd.ms-powerpoint',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'mp4': 'video/mp4',
'zip': 'application/zip',
};
const mime_type = mimeTypes[ext] || 'application/octet-stream';
attachmentsWithUrls.push({
file_name: fileName,
file_path: file.name,
file_size: file.size,
mime_type,
presigned_url,
});
}
} catch (err) {
logger.error(`Failed to list attachments from MinIO: ${err}`);
}
return {
code: 200,
message: 'Lesson retrieved successfully',
@ -332,14 +410,8 @@ export class CoursesStudentService {
is_sequential: lesson.is_sequential,
prerequisite_lesson_ids: lesson.prerequisite_lesson_ids as number[] | null,
require_pass_quiz: lesson.require_pass_quiz,
attachments: lesson.attachments.map(att => ({
id: att.id,
file_name: att.file_name,
file_path: att.file_path,
file_size: att.file_size,
mime_type: att.mime_type,
description: att.description as { th: string; en: string } | null,
})),
video_url, // Presigned URL for video
attachments: attachmentsWithUrls,
quiz: lesson.quiz ? {
id: lesson.quiz.id,
title: lesson.quiz.title as { th: string; en: string },