Major Features Added: - Complete Plugin Architecture System with financial plugin - Multi-currency support with exchange rates - Course type system (online, classroom, hybrid) - Attendance tracking and QR code scanning - Classroom sessions management - Course sections and content management - Professional video player with authentication - Secure media serving system - Shopping cart and checkout system - Financial dashboard and earnings tracking - Trainee progress tracking - User notes and assignments system Backend Infrastructure: - Plugin loader and registry system - Multi-currency database models - Secure media middleware - Course access middleware - Financial plugin with payment processing - Database migrations for new features - API endpoints for all new functionality Frontend Components: - Course management interface - Content creation and editing - Section management with drag-and-drop - Professional video player - QR scanner for attendance - Shopping cart and checkout flow - Financial dashboard - Plugin management interface - Trainee details and progress views This represents a major evolution of CourseWorx from a basic LMS to a comprehensive educational platform with plugin architecture.
734 lines
No EOL
23 KiB
JavaScript
734 lines
No EOL
23 KiB
JavaScript
const express = require('express');
|
|
const { body, validationResult } = require('express-validator');
|
|
const { Course, User, Enrollment } = require('../models');
|
|
const { auth, requireSuperAdmin, requireTrainer } = require('../middleware/auth');
|
|
const { requireEnrollment } = require('../middleware/courseAccess');
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const router = express.Router();
|
|
|
|
// Helper function to create a safe directory name from a course title
|
|
const createSafeDirectoryName = (title, language = 'english') => {
|
|
if (!title || typeof title !== 'string') {
|
|
return 'course-' + Date.now();
|
|
}
|
|
|
|
let safeName;
|
|
|
|
if (language === 'arabic') {
|
|
// For Arabic courses, use first three words separated by hyphens
|
|
const words = title.trim().split(/\s+/).filter(word => word.length > 0);
|
|
safeName = words.slice(0, 3).join('-');
|
|
|
|
// Clean up any remaining special characters
|
|
safeName = safeName.replace(/[^\p{L}\p{N}\s-]/gu, '');
|
|
|
|
// If still empty, use fallback
|
|
if (!safeName || safeName.trim() === '') {
|
|
safeName = 'arabic-course-' + Date.now();
|
|
}
|
|
} else {
|
|
// For all other languages, use the first 3 words approach consistently
|
|
const words = title.trim().split(/\s+/).filter(word => word.length > 0);
|
|
safeName = words.slice(0, 3).join('-');
|
|
|
|
// Clean up special characters
|
|
safeName = safeName
|
|
.replace(/[^\p{L}\p{N}\s-]/gu, '') // Keep letters (including non-Latin), numbers, spaces, and hyphens
|
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
.replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
|
|
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
.substring(0, 100); // Limit length to prevent path issues
|
|
|
|
// If the result is empty, use a fallback
|
|
if (!safeName || safeName.trim() === '') {
|
|
safeName = 'course-' + Date.now();
|
|
}
|
|
}
|
|
|
|
return safeName;
|
|
};
|
|
|
|
// Multer storage for course images
|
|
const courseImageStorage = multer.diskStorage({
|
|
destination: async function (req, file, cb) {
|
|
try {
|
|
let courseName;
|
|
|
|
// Handle both course name and course ID endpoints
|
|
if (req.params.courseName) {
|
|
// For course image endpoint, use the courseName parameter
|
|
courseName = req.params.courseName;
|
|
} else if (req.params.id) {
|
|
// For thumbnail endpoint, we need to get the course title
|
|
// We'll use a temporary name and rename the directory after upload
|
|
courseName = 'temp-' + req.params.id;
|
|
} else {
|
|
courseName = 'temp';
|
|
}
|
|
|
|
const dir = path.join(__dirname, '../uploads/courses', courseName);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
cb(null, dir);
|
|
} catch (error) {
|
|
cb(error, null);
|
|
}
|
|
},
|
|
filename: function (req, file, cb) {
|
|
cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname));
|
|
}
|
|
});
|
|
|
|
const uploadCourseImage = multer({
|
|
storage: courseImageStorage,
|
|
fileFilter: (req, file, cb) => {
|
|
if (file.mimetype.startsWith('image/')) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Only image files are allowed'), false);
|
|
}
|
|
},
|
|
limits: { fileSize: 5 * 1024 * 1024 }
|
|
});
|
|
|
|
// @route GET /api/courses
|
|
// @desc Get all courses (with filters)
|
|
// @access Public
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const {
|
|
category,
|
|
level,
|
|
courseType,
|
|
trainerId,
|
|
isPublished,
|
|
page = 1,
|
|
limit = 12,
|
|
search,
|
|
sortBy = 'createdAt',
|
|
sortOrder = 'DESC'
|
|
} = req.query;
|
|
|
|
const offset = (page - 1) * limit;
|
|
const whereClause = {};
|
|
|
|
if (category) whereClause.category = category;
|
|
if (level) whereClause.level = level;
|
|
if (courseType) whereClause.courseType = courseType;
|
|
if (trainerId) whereClause.trainerId = trainerId;
|
|
// Only apply isPublished filter if it's provided in the query
|
|
if (isPublished !== undefined) {
|
|
whereClause.isPublished = isPublished === 'true';
|
|
} else {
|
|
// For non-authenticated users, only show published courses
|
|
if (!req.user || req.user.role === 'trainee') {
|
|
whereClause.isPublished = true;
|
|
}
|
|
}
|
|
|
|
if (search) {
|
|
whereClause[require('sequelize').Op.or] = [
|
|
{ title: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
|
{ description: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
|
{ shortDescription: { [require('sequelize').Op.iLike]: `%${search}%` } }
|
|
];
|
|
}
|
|
|
|
const { count, rows: courses } = await Course.findAndCountAll({
|
|
where: whereClause,
|
|
attributes: {
|
|
include: ['id', 'title', 'description', 'shortDescription', 'courseType', 'language', 'thumbnail', 'price', 'duration', 'level', 'category', 'tags', 'isPublished', 'isFeatured', 'maxStudents', 'startDate', 'endDate', 'requirements', 'learningOutcomes', 'curriculum', 'rating', 'totalRatings', 'enrolledStudents', 'trainerId', 'location', 'allowRecording', 'recordForReplay', 'recordForFutureStudents', 'createdAt', 'updatedAt']
|
|
},
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar']
|
|
}
|
|
],
|
|
order: [[sortBy, sortOrder]],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({
|
|
courses,
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages: Math.ceil(count / limit),
|
|
totalItems: count,
|
|
itemsPerPage: parseInt(limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get courses error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/courses/:id
|
|
// @desc Get course by ID
|
|
// @access Private (Enrolled students or course owner)
|
|
router.get('/:id', auth, requireEnrollment, async (req, res) => {
|
|
try {
|
|
const course = await Course.findByPk(req.params.id, {
|
|
attributes: {
|
|
include: ['id', 'title', 'description', 'shortDescription', 'courseType', 'language', 'thumbnail', 'price', 'duration', 'level', 'category', 'tags', 'isPublished', 'isFeatured', 'maxStudents', 'startDate', 'endDate', 'requirements', 'learningOutcomes', 'curriculum', 'rating', 'totalRatings', 'enrolledStudents', 'trainerId', 'location', 'allowRecording', 'recordForReplay', 'recordForFutureStudents', 'createdAt', 'updatedAt']
|
|
},
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar', 'email']
|
|
},
|
|
{
|
|
model: Enrollment,
|
|
as: 'enrollments',
|
|
attributes: ['id', 'status', 'enrolledAt', 'progress'],
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar']
|
|
}
|
|
]
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
res.json({ course });
|
|
} catch (error) {
|
|
console.error('Get course error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/courses
|
|
// @desc Create new course (Super Admin or Trainer)
|
|
// @access Private
|
|
router.post('/', [
|
|
auth,
|
|
requireTrainer,
|
|
body('title').isLength({ min: 3, max: 200 }),
|
|
body('description').notEmpty(),
|
|
body('price').isFloat({ min: 0 }),
|
|
body('level').isIn(['beginner', 'intermediate', 'advanced']),
|
|
body('category').optional().isLength({ min: 2, max: 100 }),
|
|
body('courseType').isIn(['online', 'classroom', 'hybrid']),
|
|
body('language').isIn(['english', 'arabic', 'french', 'spanish', 'german', 'chinese', 'japanese', 'korean', 'hindi', 'other'])
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const {
|
|
title,
|
|
description,
|
|
shortDescription,
|
|
courseType,
|
|
language,
|
|
price,
|
|
duration,
|
|
level,
|
|
category,
|
|
tags,
|
|
requirements,
|
|
learningOutcomes,
|
|
curriculum,
|
|
maxStudents,
|
|
startDate,
|
|
endDate,
|
|
location,
|
|
allowRecording,
|
|
recordForReplay,
|
|
recordForFutureStudents
|
|
} = req.body;
|
|
|
|
const course = await Course.create({
|
|
title,
|
|
description,
|
|
shortDescription,
|
|
courseType,
|
|
language,
|
|
price,
|
|
duration,
|
|
level,
|
|
category,
|
|
tags: tags || [],
|
|
requirements,
|
|
learningOutcomes,
|
|
curriculum: curriculum || [],
|
|
maxStudents,
|
|
startDate,
|
|
endDate,
|
|
location,
|
|
allowRecording,
|
|
recordForReplay,
|
|
recordForFutureStudents,
|
|
trainerId: req.user.id,
|
|
isPublished: true // Auto-publish for all users
|
|
});
|
|
|
|
const courseWithTrainer = await Course.findByPk(course.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar']
|
|
}
|
|
]
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: 'Course created successfully.',
|
|
course: courseWithTrainer
|
|
});
|
|
} catch (error) {
|
|
console.error('Create course error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/courses/:id
|
|
// @desc Update course (Owner or Super Admin)
|
|
// @access Private
|
|
router.put('/:id', [
|
|
auth,
|
|
body('title').optional().isLength({ min: 3, max: 200 }),
|
|
body('description').optional().notEmpty(),
|
|
body('price').optional().isFloat({ min: 0 }),
|
|
body('level').optional().isIn(['beginner', 'intermediate', 'advanced']),
|
|
body('category').optional().isLength({ min: 2, max: 100 }),
|
|
body('courseType').optional().isIn(['online', 'classroom', 'hybrid']),
|
|
body('language').optional().isIn(['english', 'arabic', 'french', 'spanish', 'german', 'chinese', 'japanese', 'korean', 'hindi', 'other'])
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const course = await Course.findByPk(req.params.id);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to update this course.' });
|
|
}
|
|
|
|
const updateData = req.body;
|
|
await course.update(updateData);
|
|
|
|
const updatedCourse = await Course.findByPk(course.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar']
|
|
}
|
|
]
|
|
});
|
|
|
|
res.json({
|
|
message: 'Course updated successfully.',
|
|
course: updatedCourse
|
|
});
|
|
} catch (error) {
|
|
console.error('Update course error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route DELETE /api/courses/:id
|
|
// @desc Delete course (Owner or Super Admin)
|
|
// @access Private
|
|
router.delete('/:id', auth, async (req, res) => {
|
|
try {
|
|
const course = await Course.findByPk(req.params.id);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to delete this course.' });
|
|
}
|
|
|
|
await course.destroy();
|
|
|
|
res.json({ message: 'Course deleted successfully.' });
|
|
} catch (error) {
|
|
console.error('Delete course error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/courses/:id/publish
|
|
// @desc Publish/unpublish course (Owner or Super Admin)
|
|
// @access Private
|
|
router.put('/:id/publish', [
|
|
auth,
|
|
body('isPublished').isBoolean()
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const course = await Course.findByPk(req.params.id);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to update this course.' });
|
|
}
|
|
|
|
await course.update({ isPublished: req.body.isPublished });
|
|
|
|
res.json({
|
|
message: `Course ${req.body.isPublished ? 'published' : 'unpublished'} successfully.`,
|
|
course: {
|
|
id: course.id,
|
|
title: course.title,
|
|
isPublished: course.isPublished
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Publish course error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/courses/:courseName/image
|
|
// @desc Upload course image (Super Admin or Trainer)
|
|
// @access Private
|
|
router.post('/:courseName/image', [auth, requireTrainer, uploadCourseImage.single('image')], async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No image file uploaded.' });
|
|
}
|
|
|
|
// For consistency, we should use the course title instead of courseName parameter
|
|
// But since this route might be used for existing courses, we'll keep the current behavior
|
|
// and update the frontend to use the thumbnail route instead for new courses
|
|
|
|
res.json({
|
|
message: 'Image uploaded successfully.',
|
|
imageUrl: `/uploads/courses/${req.params.courseName}/${req.file.filename}`
|
|
});
|
|
} catch (error) {
|
|
console.error('Upload course image error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/courses/:id/thumbnail
|
|
// @desc Upload and set course thumbnail (Super Admin or Trainer)
|
|
// @access Private
|
|
router.post('/:id/thumbnail', [auth, requireTrainer, uploadCourseImage.single('image')], async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No image file uploaded.' });
|
|
}
|
|
|
|
const course = await Course.findByPk(req.params.id);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to update this course.' });
|
|
}
|
|
|
|
// Generate proper course directory name
|
|
const courseDirName = createSafeDirectoryName(course.title, course.language);
|
|
const tempDir = path.join(__dirname, '../uploads/courses', `temp-${req.params.id}`);
|
|
const finalDir = path.join(__dirname, '../uploads/courses', courseDirName);
|
|
|
|
console.log('📁 Creating directory for course:', {
|
|
originalTitle: course.title,
|
|
safeDirName: courseDirName,
|
|
tempDir: tempDir,
|
|
finalDir: finalDir
|
|
});
|
|
|
|
// Validate the safe directory name
|
|
if (!courseDirName || courseDirName.trim() === '') {
|
|
throw new Error('Failed to generate safe directory name from course title');
|
|
}
|
|
|
|
// Create final directory if it doesn't exist
|
|
try {
|
|
if (!fs.existsSync(finalDir)) {
|
|
fs.mkdirSync(finalDir, { recursive: true });
|
|
console.log('✅ Directory created successfully:', finalDir);
|
|
} else {
|
|
console.log('✅ Directory already exists:', finalDir);
|
|
}
|
|
} catch (dirError) {
|
|
console.error('❌ Error creating directory:', dirError);
|
|
throw new Error(`Failed to create directory: ${dirError.message}`);
|
|
}
|
|
|
|
// Move file from temp directory to final directory
|
|
const tempFilePath = req.file.path;
|
|
const finalFilePath = path.join(finalDir, path.basename(req.file.filename));
|
|
|
|
// Copy file to final location
|
|
fs.copyFileSync(tempFilePath, finalFilePath);
|
|
|
|
// Remove temp file and directory
|
|
fs.unlinkSync(tempFilePath);
|
|
if (fs.existsSync(tempDir)) {
|
|
fs.rmdirSync(tempDir, { recursive: true });
|
|
}
|
|
|
|
// Generate image URL using proxy route to avoid CORS issues
|
|
const imageUrl = `/uploads/courses/${courseDirName}/${path.basename(req.file.filename)}`;
|
|
|
|
// Update course thumbnail
|
|
await course.update({ thumbnail: imageUrl });
|
|
|
|
res.json({
|
|
message: 'Course thumbnail updated successfully.',
|
|
imageUrl: imageUrl
|
|
});
|
|
} catch (error) {
|
|
console.error('Upload course thumbnail error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/courses/categories/all
|
|
// @desc Get all course categories
|
|
// @access Public
|
|
router.get('/categories/all', async (req, res) => {
|
|
try {
|
|
const categories = await Course.findAll({
|
|
attributes: [
|
|
[require('sequelize').fn('DISTINCT', require('sequelize').col('category')), 'category']
|
|
],
|
|
where: {
|
|
category: { [require('sequelize').Op.not]: null },
|
|
isPublished: true
|
|
},
|
|
raw: true
|
|
});
|
|
|
|
const categoryList = categories
|
|
.map(cat => cat.category)
|
|
.filter(cat => cat)
|
|
.sort();
|
|
|
|
res.json({ categories: categoryList });
|
|
} catch (error) {
|
|
console.error('Get categories error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/courses/:id/assign-trainer
|
|
// @desc Assign trainer to course (Super Admin only)
|
|
// @access Private (Super Admin)
|
|
router.put('/:id/assign-trainer', [
|
|
auth,
|
|
requireSuperAdmin,
|
|
body('trainerId').isUUID().withMessage('Valid trainer ID is required')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const course = await Course.findByPk(req.params.id);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
const { trainerId } = req.body;
|
|
|
|
// Verify the trainer exists and is actually a trainer
|
|
const trainer = await User.findByPk(trainerId);
|
|
if (!trainer) {
|
|
return res.status(404).json({ error: 'Trainer not found.' });
|
|
}
|
|
|
|
if (trainer.role !== 'trainer') {
|
|
return res.status(400).json({ error: 'Selected user is not a trainer.' });
|
|
}
|
|
|
|
if (!trainer.isActive) {
|
|
return res.status(400).json({ error: 'Selected trainer account is inactive.' });
|
|
}
|
|
|
|
// Update the course with the new trainer
|
|
await course.update({ trainerId });
|
|
|
|
const updatedCourse = await Course.findByPk(course.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar', 'email']
|
|
}
|
|
]
|
|
});
|
|
|
|
res.json({
|
|
message: 'Trainer assigned successfully.',
|
|
course: updatedCourse
|
|
});
|
|
} catch (error) {
|
|
console.error('Assign trainer error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/courses/trainers/available
|
|
// @desc Get available trainers for assignment (Super Admin only)
|
|
// @access Private (Super Admin)
|
|
router.get('/trainers/available', auth, requireSuperAdmin, async (req, res) => {
|
|
try {
|
|
console.log('Available trainers endpoint called by user:', req.user.id, req.user.role);
|
|
|
|
const trainers = await User.findAll({
|
|
where: {
|
|
role: 'trainer',
|
|
isActive: true
|
|
},
|
|
attributes: ['id', 'firstName', 'lastName', 'email', 'avatar'],
|
|
order: [['firstName', 'ASC'], ['lastName', 'ASC']]
|
|
});
|
|
|
|
console.log('Available trainers found:', trainers.length, trainers.map(t => ({ id: t.id, name: `${t.firstName} ${t.lastName}`, email: t.email })));
|
|
|
|
res.json({ trainers });
|
|
} catch (error) {
|
|
console.error('Get available trainers error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/courses/trainer/:trainerId
|
|
// @desc Get courses for a specific trainer
|
|
// @access Private (Trainer can only see their own courses, Super Admin can see any trainer's courses)
|
|
router.get('/trainer/:trainerId', auth, async (req, res) => {
|
|
try {
|
|
const { trainerId } = req.params;
|
|
const { isPublished, page = 1, limit = 12, search, sortBy = 'createdAt', sortOrder = 'DESC' } = req.query;
|
|
|
|
// Check if user can access this trainer's courses
|
|
if (req.user.role === 'trainer' && req.user.id !== trainerId) {
|
|
return res.status(403).json({ error: 'Access denied. You can only view your own courses.' });
|
|
}
|
|
|
|
const offset = (page - 1) * limit;
|
|
const whereClause = { trainerId };
|
|
|
|
// Apply filters
|
|
if (isPublished !== undefined) {
|
|
whereClause.isPublished = isPublished === 'true';
|
|
}
|
|
|
|
if (search) {
|
|
whereClause[require('sequelize').Op.or] = [
|
|
{ title: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
|
{ description: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
|
{ shortDescription: { [require('sequelize').Op.iLike]: `%${search}%` } }
|
|
];
|
|
}
|
|
|
|
const { count, rows: courses } = await Course.findAndCountAll({
|
|
where: whereClause,
|
|
attributes: {
|
|
include: ['id', 'title', 'description', 'shortDescription', 'courseType', 'language', 'thumbnail', 'price', 'duration', 'level', 'category', 'tags', 'isPublished', 'isFeatured', 'maxStudents', 'startDate', 'endDate', 'requirements', 'learningOutcomes', 'curriculum', 'rating', 'totalRatings', 'enrolledStudents', 'trainerId', 'location', 'allowRecording', 'recordForReplay', 'recordForFutureStudents', 'createdAt', 'updatedAt']
|
|
},
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar']
|
|
}
|
|
],
|
|
order: [[sortBy, sortOrder]],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({
|
|
courses,
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages: Math.ceil(count / limit),
|
|
totalItems: count,
|
|
itemsPerPage: parseInt(limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get trainer courses error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/courses/stats/overview
|
|
// @desc Get course statistics (Super Admin or Trainer)
|
|
// @access Private
|
|
router.get('/stats/overview', auth, async (req, res) => {
|
|
try {
|
|
console.log('Course stats endpoint called by user:', req.user.id, req.user.role);
|
|
|
|
const whereClause = {};
|
|
if (req.user.role === 'trainer') {
|
|
whereClause.trainerId = req.user.id;
|
|
}
|
|
|
|
const totalCourses = await Course.count({ where: whereClause });
|
|
const publishedCourses = await Course.count({
|
|
where: { ...whereClause, isPublished: true }
|
|
});
|
|
const featuredCourses = await Course.count({
|
|
where: { ...whereClause, isFeatured: true }
|
|
});
|
|
|
|
console.log('Course stats calculated:', { totalCourses, publishedCourses, featuredCourses, whereClause });
|
|
|
|
// For trainers, provide specific stats
|
|
let myCourses = 0;
|
|
let myPublishedCourses = 0;
|
|
if (req.user.role === 'trainer') {
|
|
myCourses = totalCourses;
|
|
myPublishedCourses = publishedCourses;
|
|
}
|
|
|
|
res.json({
|
|
stats: {
|
|
totalCourses,
|
|
publishedCourses,
|
|
unpublishedCourses: totalCourses - publishedCourses,
|
|
featuredCourses,
|
|
myCourses,
|
|
myPublishedCourses
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get course stats error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|