- Add is_recommended field to Course model - Add allow_multiple_attempts field to Quiz model - Create RecommendedCoursesController for admin management - List all approved courses - Get course by ID - Toggle recommendation status - Add is_recommended filter to CoursesService.ListCourses - Add allow_multiple_attempts to quiz update and response types - Update ChaptersLessonService.updateQuiz to support allow_multiple_attempts
128 lines
No EOL
4.5 KiB
TypeScript
128 lines
No EOL
4.5 KiB
TypeScript
import { prisma } from '../config/database';
|
|
import { Prisma } from '@prisma/client';
|
|
import { config } from '../config';
|
|
import { logger } from '../config/logger';
|
|
import { listCourseResponse, getCourseResponse, ListCoursesInput } from '../types/courses.types';
|
|
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
|
import { getPresignedUrl } from '../config/minio';
|
|
|
|
export class CoursesService {
|
|
async ListCourses(input: ListCoursesInput): Promise<listCourseResponse> {
|
|
try {
|
|
const { category_id, is_recommended, page = 1, limit = 10, random = false } = input;
|
|
|
|
const where: Prisma.CourseWhereInput = {
|
|
status: 'APPROVED',
|
|
};
|
|
|
|
if (category_id) {
|
|
where.category_id = category_id;
|
|
}
|
|
|
|
if (is_recommended !== undefined) {
|
|
where.is_recommended = is_recommended;
|
|
}
|
|
|
|
// Get total count for pagination
|
|
const total = await prisma.course.count({ where });
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
let courses;
|
|
|
|
if (random) {
|
|
// Random mode: ดึงทั้งหมดแล้วสุ่ม
|
|
const allCourses = await prisma.course.findMany({ where });
|
|
|
|
// Fisher-Yates shuffle
|
|
for (let i = allCourses.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[allCourses[i], allCourses[j]] = [allCourses[j], allCourses[i]];
|
|
}
|
|
|
|
// Apply pagination after shuffle
|
|
const skip = (page - 1) * limit;
|
|
courses = allCourses.slice(skip, skip + limit);
|
|
} else {
|
|
// Normal mode: pagination with skip/take
|
|
const skip = (page - 1) * limit;
|
|
courses = await prisma.course.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { created_at: 'desc' }
|
|
});
|
|
}
|
|
|
|
// Generate presigned URLs for thumbnails
|
|
const coursesWithUrls = await Promise.all(
|
|
courses.map(async (course) => {
|
|
let thumbnail_presigned_url: string | null = null;
|
|
if (course.thumbnail_url) {
|
|
try {
|
|
thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600);
|
|
} catch (err) {
|
|
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
|
|
}
|
|
}
|
|
return {
|
|
...course,
|
|
thumbnail_url: thumbnail_presigned_url,
|
|
};
|
|
})
|
|
);
|
|
|
|
return {
|
|
code: 200,
|
|
message: 'Courses fetched successfully',
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages,
|
|
data: coursesWithUrls,
|
|
};
|
|
} catch (error) {
|
|
logger.error('Failed to fetch courses', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async GetCourseById(id: number): Promise<getCourseResponse> {
|
|
try {
|
|
const course = await prisma.course.findFirst({
|
|
where: {
|
|
id,
|
|
status: 'APPROVED' // Only show approved courses to students
|
|
}
|
|
});
|
|
|
|
if (!course) {
|
|
return {
|
|
code: 200,
|
|
message: 'no Course fetched successfully',
|
|
data: null,
|
|
};
|
|
}
|
|
|
|
// Generate presigned URL for thumbnail
|
|
let thumbnail_presigned_url: string | null = null;
|
|
if (course.thumbnail_url) {
|
|
try {
|
|
thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600);
|
|
} catch (err) {
|
|
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
|
|
}
|
|
}
|
|
return {
|
|
code: 200,
|
|
message: 'Course fetched successfully',
|
|
data: {
|
|
...course,
|
|
thumbnail_url: thumbnail_presigned_url,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
logger.error('Failed to fetch course', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
} |