869 lines
No EOL
25 KiB
JavaScript
869 lines
No EOL
25 KiB
JavaScript
const express = require('express');
|
|
const { body, validationResult } = require('express-validator');
|
|
const { Enrollment, Course, User } = require('../models');
|
|
const { auth, requireTrainer } = require('../middleware/auth');
|
|
|
|
const router = express.Router();
|
|
|
|
// @route GET /api/enrollments
|
|
// @desc Get all enrollments (filtered by user role)
|
|
// @access Private
|
|
router.get('/', auth, async (req, res) => {
|
|
try {
|
|
const {
|
|
courseId,
|
|
userId,
|
|
status,
|
|
paymentStatus,
|
|
page = 1,
|
|
limit = 20,
|
|
sortBy = 'enrolledAt',
|
|
sortOrder = 'DESC'
|
|
} = req.query;
|
|
|
|
const offset = (page - 1) * limit;
|
|
const whereClause = {};
|
|
|
|
// Filter by course if provided
|
|
if (courseId) whereClause.courseId = courseId;
|
|
|
|
// Filter by user if provided or if user is not admin/trainer
|
|
if (userId) {
|
|
whereClause.userId = userId;
|
|
} else if (req.user.role === 'trainee') {
|
|
whereClause.userId = req.user.id;
|
|
}
|
|
|
|
if (status) whereClause.status = status;
|
|
if (paymentStatus) whereClause.paymentStatus = paymentStatus;
|
|
|
|
const { count, rows: enrollments } = await Enrollment.findAndCountAll({
|
|
where: whereClause,
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
attributes: ['id', 'title', 'thumbnail', 'price', 'trainerId'],
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName', 'email', 'avatar']
|
|
}
|
|
],
|
|
order: [[sortBy, sortOrder]],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({
|
|
enrollments,
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages: Math.ceil(count / limit),
|
|
totalItems: count,
|
|
itemsPerPage: parseInt(limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get enrollments error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/enrollments/my
|
|
// @desc Get current user's enrollments
|
|
// @access Private
|
|
router.get('/my', auth, async (req, res) => {
|
|
try {
|
|
const {
|
|
status,
|
|
paymentStatus,
|
|
page = 1,
|
|
limit = 20
|
|
} = req.query;
|
|
|
|
const offset = (page - 1) * limit;
|
|
const whereClause = { userId: req.user.id };
|
|
|
|
if (status) whereClause.status = status;
|
|
if (paymentStatus) whereClause.paymentStatus = paymentStatus;
|
|
|
|
const { count, rows: enrollments } = await Enrollment.findAndCountAll({
|
|
where: whereClause,
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
attributes: ['id', 'title', 'thumbnail', 'price', 'description', 'level', 'category'],
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar']
|
|
}
|
|
]
|
|
}
|
|
],
|
|
order: [['enrolledAt', 'DESC']],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({
|
|
enrollments,
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages: Math.ceil(count / limit),
|
|
totalItems: count,
|
|
itemsPerPage: parseInt(limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get my enrollments error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/enrollments/course/:courseId/trainees
|
|
// @desc Get all trainees enrolled in a specific course
|
|
// @access Private (Course trainer or Super Admin)
|
|
router.get('/course/:courseId/trainees', auth, async (req, res) => {
|
|
try {
|
|
const { courseId } = req.params;
|
|
const { status, page = 1, limit = 20 } = req.query;
|
|
|
|
const offset = (page - 1) * limit;
|
|
const whereClause = { courseId };
|
|
|
|
if (status) whereClause.status = status;
|
|
|
|
// Check if course exists and user has permission
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
// Check if user is trainer of this course or super admin
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to view trainees for this course.' });
|
|
}
|
|
|
|
const { count, rows: enrollments } = await Enrollment.findAndCountAll({
|
|
where: whereClause,
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName', 'email', 'phone', 'avatar'],
|
|
where: { role: 'trainee' }
|
|
}
|
|
],
|
|
order: [['enrolledAt', 'DESC']],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({
|
|
trainees: enrollments.map(e => ({
|
|
...e.user.toJSON(),
|
|
enrollmentId: e.id,
|
|
status: e.status,
|
|
progress: e.progress,
|
|
enrolledAt: e.enrolledAt,
|
|
completedAt: e.completedAt
|
|
})),
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages: Math.ceil(count / limit),
|
|
totalItems: count,
|
|
itemsPerPage: parseInt(limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get course trainees error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/enrollments/available-trainees
|
|
// @desc Get available trainees for course enrollment
|
|
// @access Private (Trainer or Super Admin)
|
|
router.get('/available-trainees', auth, async (req, res) => {
|
|
try {
|
|
const { courseId, search, page = 1, limit = 20 } = req.query;
|
|
|
|
const offset = (page - 1) * limit;
|
|
const whereClause = { role: 'trainee', isActive: true };
|
|
|
|
if (search) {
|
|
whereClause[require('sequelize').Op.or] = [
|
|
{ firstName: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
|
{ lastName: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
|
{ email: { [require('sequelize').Op.iLike]: `%${search}%` } },
|
|
{ phone: { [require('sequelize').Op.iLike]: `%${search}%` } }
|
|
];
|
|
}
|
|
|
|
// Get all trainees
|
|
const { count, rows: trainees } = await User.findAndCountAll({
|
|
where: whereClause,
|
|
attributes: ['id', 'firstName', 'lastName', 'email', 'phone', 'avatar'],
|
|
order: [['firstName', 'ASC']],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
// If courseId is provided, filter out already enrolled trainees
|
|
let availableTrainees = trainees;
|
|
if (courseId) {
|
|
const enrolledTraineeIds = await Enrollment.findAll({
|
|
where: { courseId },
|
|
attributes: ['userId']
|
|
});
|
|
|
|
const enrolledIds = enrolledTraineeIds.map(e => e.userId);
|
|
availableTrainees = trainees.filter(t => !enrolledIds.includes(t.id));
|
|
}
|
|
|
|
res.json({
|
|
trainees: availableTrainees,
|
|
pagination: {
|
|
currentPage: parseInt(page),
|
|
totalPages: Math.ceil(count / limit),
|
|
totalItems: count,
|
|
itemsPerPage: parseInt(limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get available trainees error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/enrollments/:id
|
|
// @desc Get enrollment by ID
|
|
// @access Private
|
|
router.get('/:id', auth, async (req, res) => {
|
|
try {
|
|
const enrollment = await Enrollment.findByPk(req.params.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName', 'avatar', 'email']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName', 'email', 'avatar']
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!enrollment) {
|
|
return res.status(404).json({ error: 'Enrollment not found.' });
|
|
}
|
|
|
|
// Check if user has access to this enrollment
|
|
if (req.user.role === 'trainee' && enrollment.userId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to view this enrollment.' });
|
|
}
|
|
|
|
res.json({ enrollment });
|
|
} catch (error) {
|
|
console.error('Get enrollment error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/enrollments
|
|
// @desc Create new enrollment (subscribe to course)
|
|
// @access Private
|
|
router.post('/', [
|
|
auth,
|
|
body('courseId').isUUID(),
|
|
body('paymentAmount').optional().isFloat({ min: 0 }),
|
|
body('notes').optional().isLength({ max: 1000 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { courseId, paymentAmount, notes } = req.body;
|
|
|
|
// Check if course exists and is published
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
if (!course.isPublished) {
|
|
return res.status(400).json({ error: 'Course is not published.' });
|
|
}
|
|
|
|
// Check if user is already enrolled
|
|
const existingEnrollment = await Enrollment.findOne({
|
|
where: { userId: req.user.id, courseId }
|
|
});
|
|
|
|
if (existingEnrollment) {
|
|
return res.status(400).json({ error: 'Already enrolled in this course.' });
|
|
}
|
|
|
|
// Check course capacity
|
|
if (course.maxStudents) {
|
|
const enrolledCount = await Enrollment.count({
|
|
where: { courseId, status: ['active', 'pending'] }
|
|
});
|
|
if (enrolledCount >= course.maxStudents) {
|
|
return res.status(400).json({ error: 'Course is at maximum capacity.' });
|
|
}
|
|
}
|
|
|
|
const enrollment = await Enrollment.create({
|
|
userId: req.user.id,
|
|
courseId,
|
|
status: 'pending',
|
|
paymentStatus: course.price > 0 ? 'pending' : 'paid',
|
|
paymentAmount: paymentAmount || course.price,
|
|
notes
|
|
});
|
|
|
|
const enrollmentWithDetails = await Enrollment.findByPk(enrollment.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName', 'email']
|
|
}
|
|
]
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: 'Enrollment created successfully.',
|
|
enrollment: enrollmentWithDetails
|
|
});
|
|
} catch (error) {
|
|
console.error('Create enrollment error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/enrollments/:id/status
|
|
// @desc Update enrollment status
|
|
// @access Private (Course owner or Super Admin)
|
|
router.put('/:id/status', [
|
|
auth,
|
|
body('status').isIn(['pending', 'active', 'completed', 'cancelled']),
|
|
body('notes').optional().isLength({ max: 1000 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const enrollment = await Enrollment.findByPk(req.params.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course'
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!enrollment) {
|
|
return res.status(404).json({ error: 'Enrollment not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
const canUpdate = req.user.role === 'super_admin' ||
|
|
enrollment.course.trainerId === req.user.id ||
|
|
enrollment.userId === req.user.id;
|
|
|
|
if (!canUpdate) {
|
|
return res.status(403).json({ error: 'Not authorized to update this enrollment.' });
|
|
}
|
|
|
|
const { status, notes } = req.body;
|
|
const updateData = { status };
|
|
|
|
if (status === 'completed' && enrollment.status !== 'completed') {
|
|
updateData.completedAt = new Date();
|
|
}
|
|
|
|
if (notes) {
|
|
updateData.notes = notes;
|
|
}
|
|
|
|
await enrollment.update(updateData);
|
|
|
|
res.json({
|
|
message: 'Enrollment status updated successfully.',
|
|
enrollment
|
|
});
|
|
} catch (error) {
|
|
console.error('Update enrollment status error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/enrollments/:id/payment
|
|
// @desc Update enrollment payment status
|
|
// @access Private (Course owner or Super Admin)
|
|
router.put('/:id/payment', [
|
|
auth,
|
|
body('paymentStatus').isIn(['pending', 'paid', 'failed', 'refunded']),
|
|
body('paymentAmount').optional().isFloat({ min: 0 }),
|
|
body('notes').optional().isLength({ max: 1000 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const enrollment = await Enrollment.findByPk(req.params.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course'
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!enrollment) {
|
|
return res.status(404).json({ error: 'Enrollment not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
const canUpdate = req.user.role === 'super_admin' ||
|
|
enrollment.course.trainerId === req.user.id;
|
|
|
|
if (!canUpdate) {
|
|
return res.status(403).json({ error: 'Not authorized to update payment status.' });
|
|
}
|
|
|
|
const { paymentStatus, paymentAmount, notes } = req.body;
|
|
const updateData = { paymentStatus };
|
|
|
|
if (paymentStatus === 'paid' && enrollment.paymentStatus !== 'paid') {
|
|
updateData.paymentDate = new Date();
|
|
}
|
|
|
|
if (paymentAmount) {
|
|
updateData.paymentAmount = paymentAmount;
|
|
}
|
|
|
|
if (notes) {
|
|
updateData.notes = notes;
|
|
}
|
|
|
|
await enrollment.update(updateData);
|
|
|
|
res.json({
|
|
message: 'Payment status updated successfully.',
|
|
enrollment
|
|
});
|
|
} catch (error) {
|
|
console.error('Update payment status error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/enrollments/:id/progress
|
|
// @desc Update enrollment progress
|
|
// @access Private (Enrolled user or Course owner)
|
|
router.put('/:id/progress', [
|
|
auth,
|
|
body('progress').isInt({ min: 0, max: 100 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const enrollment = await Enrollment.findByPk(req.params.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course'
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!enrollment) {
|
|
return res.status(404).json({ error: 'Enrollment not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
const canUpdate = req.user.role === 'super_admin' ||
|
|
enrollment.course.trainerId === req.user.id ||
|
|
enrollment.userId === req.user.id;
|
|
|
|
if (!canUpdate) {
|
|
return res.status(403).json({ error: 'Not authorized to update progress.' });
|
|
}
|
|
|
|
const { progress } = req.body;
|
|
await enrollment.update({ progress });
|
|
|
|
res.json({
|
|
message: 'Progress updated successfully.',
|
|
enrollment
|
|
});
|
|
} catch (error) {
|
|
console.error('Update progress error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route DELETE /api/enrollments/:id
|
|
// @desc Cancel enrollment
|
|
// @access Private (Enrolled user or Course owner)
|
|
router.delete('/:id', auth, async (req, res) => {
|
|
try {
|
|
const enrollment = await Enrollment.findByPk(req.params.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course'
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!enrollment) {
|
|
return res.status(404).json({ error: 'Enrollment not found.' });
|
|
}
|
|
|
|
// Check permissions
|
|
const canCancel = req.user.role === 'super_admin' ||
|
|
enrollment.course.trainerId === req.user.id ||
|
|
enrollment.userId === req.user.id;
|
|
|
|
if (!canCancel) {
|
|
return res.status(403).json({ error: 'Not authorized to cancel this enrollment.' });
|
|
}
|
|
|
|
await enrollment.update({ status: 'cancelled' });
|
|
|
|
res.json({ message: 'Enrollment cancelled successfully.' });
|
|
} catch (error) {
|
|
console.error('Cancel enrollment error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route GET /api/enrollments/stats/overview
|
|
// @desc Get enrollment statistics
|
|
// @access Private (Super Admin or Trainer)
|
|
router.get('/stats/overview', auth, async (req, res) => {
|
|
try {
|
|
const whereClause = {};
|
|
|
|
// If trainer, only show their course enrollments
|
|
if (req.user.role === 'trainer') {
|
|
const trainerCourses = await Course.findAll({
|
|
where: { trainerId: req.user.id },
|
|
attributes: ['id']
|
|
});
|
|
whereClause.courseId = trainerCourses.map(c => c.id);
|
|
}
|
|
|
|
// If trainee, only show their enrollments
|
|
if (req.user.role === 'trainee') {
|
|
whereClause.userId = req.user.id;
|
|
}
|
|
|
|
const totalEnrollments = await Enrollment.count({ where: whereClause });
|
|
const activeEnrollments = await Enrollment.count({
|
|
where: { ...whereClause, status: 'active' }
|
|
});
|
|
const completedEnrollments = await Enrollment.count({
|
|
where: { ...whereClause, status: 'completed' }
|
|
});
|
|
const pendingEnrollments = await Enrollment.count({
|
|
where: { ...whereClause, status: 'pending' }
|
|
});
|
|
|
|
// Get unique students count for trainers
|
|
let myStudents = 0;
|
|
if (req.user.role === 'trainer') {
|
|
const trainerCourses = await Course.findAll({
|
|
where: { trainerId: req.user.id },
|
|
attributes: ['id']
|
|
});
|
|
|
|
if (trainerCourses.length > 0) {
|
|
const uniqueStudents = await Enrollment.findAll({
|
|
where: {
|
|
courseId: trainerCourses.map(c => c.id),
|
|
status: { [require('sequelize').Op.in]: ['active', 'completed'] }
|
|
},
|
|
attributes: ['userId'],
|
|
group: ['userId']
|
|
});
|
|
myStudents = uniqueStudents.length;
|
|
}
|
|
}
|
|
|
|
// Get my enrollments count for trainees
|
|
let myEnrollments = 0;
|
|
let completedCourses = 0;
|
|
if (req.user.role === 'trainee') {
|
|
myEnrollments = await Enrollment.count({
|
|
where: { userId: req.user.id }
|
|
});
|
|
completedCourses = await Enrollment.count({
|
|
where: { userId: req.user.id, status: 'completed' }
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
stats: {
|
|
totalEnrollments,
|
|
activeEnrollments,
|
|
completedEnrollments,
|
|
pendingEnrollments,
|
|
cancelledEnrollments: totalEnrollments - activeEnrollments - completedEnrollments - pendingEnrollments,
|
|
myStudents,
|
|
myEnrollments,
|
|
completedCourses
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get enrollment stats error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/enrollments/bulk
|
|
// @desc Bulk enroll trainees to a course (Trainer only)
|
|
// @access Private (Trainer or Super Admin)
|
|
router.post('/bulk', [
|
|
auth,
|
|
body('courseId').isUUID(),
|
|
body('traineeIds').isArray({ min: 1 }),
|
|
body('traineeIds.*').isUUID(),
|
|
body('status').optional().isIn(['pending', 'active']),
|
|
body('notes').optional().isLength({ max: 1000 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { courseId, traineeIds, status = 'active', notes } = req.body;
|
|
|
|
// Check if course exists and user has permission
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
// Check if user is trainer of this course or super admin
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to enroll trainees to this course.' });
|
|
}
|
|
|
|
// Check course capacity
|
|
if (course.maxStudents) {
|
|
const enrolledCount = await Enrollment.count({
|
|
where: { courseId, status: ['active', 'pending'] }
|
|
});
|
|
const remainingCapacity = course.maxStudents - enrolledCount;
|
|
if (traineeIds.length > remainingCapacity) {
|
|
return res.status(400).json({
|
|
error: `Course capacity exceeded. Only ${remainingCapacity} more trainees can be enrolled.`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Get existing trainees to avoid duplicates
|
|
const existingEnrollments = await Enrollment.findAll({
|
|
where: {
|
|
courseId,
|
|
userId: traineeIds
|
|
},
|
|
attributes: ['userId']
|
|
});
|
|
const existingTraineeIds = existingEnrollments.map(e => e.userId);
|
|
|
|
// Filter out already enrolled trainees
|
|
const newTraineeIds = traineeIds.filter(id => !existingTraineeIds.includes(id));
|
|
|
|
if (newTraineeIds.length === 0) {
|
|
return res.status(400).json({ error: 'All selected trainees are already enrolled in this course.' });
|
|
}
|
|
|
|
// Create enrollments
|
|
const enrollments = await Promise.all(
|
|
newTraineeIds.map(traineeId =>
|
|
Enrollment.create({
|
|
userId: traineeId,
|
|
courseId,
|
|
status,
|
|
paymentStatus: course.price > 0 ? 'pending' : 'paid',
|
|
paymentAmount: course.price,
|
|
notes
|
|
})
|
|
)
|
|
);
|
|
|
|
// Get enrollment details with user and course info
|
|
const enrollmentsWithDetails = await Enrollment.findAll({
|
|
where: { id: enrollments.map(e => e.id) },
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName', 'email', 'phone']
|
|
}
|
|
]
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: `Successfully enrolled ${enrollments.length} trainees to the course.`,
|
|
enrollments: enrollmentsWithDetails,
|
|
skipped: existingTraineeIds.length
|
|
});
|
|
} catch (error) {
|
|
console.error('Bulk enrollment error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
// @route POST /api/enrollments/assign
|
|
// @desc Assign a single trainee to a course (Trainer only)
|
|
// @access Private (Trainer or Super Admin)
|
|
router.post('/assign', [
|
|
auth,
|
|
body('courseId').isUUID(),
|
|
body('traineeId').isUUID(),
|
|
body('status').optional().isIn(['pending', 'active']),
|
|
body('notes').optional().isLength({ max: 1000 })
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { courseId, traineeId, status = 'active', notes } = req.body;
|
|
|
|
// Check if course exists and user has permission
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found.' });
|
|
}
|
|
|
|
// Check if user is trainer of this course or super admin
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Not authorized to assign trainees to this course.' });
|
|
}
|
|
|
|
// Check if trainee exists
|
|
const trainee = await User.findByPk(traineeId);
|
|
if (!trainee || trainee.role !== 'trainee') {
|
|
return res.status(404).json({ error: 'Trainee not found.' });
|
|
}
|
|
|
|
// Check if already enrolled
|
|
const existingEnrollment = await Enrollment.findOne({
|
|
where: { userId: traineeId, courseId }
|
|
});
|
|
|
|
if (existingEnrollment) {
|
|
return res.status(400).json({ error: 'Trainee is already enrolled in this course.' });
|
|
}
|
|
|
|
// Check course capacity
|
|
if (course.maxStudents) {
|
|
const enrolledCount = await Enrollment.count({
|
|
where: { courseId, status: ['active', 'pending'] }
|
|
});
|
|
if (enrolledCount >= course.maxStudents) {
|
|
return res.status(400).json({ error: 'Course is at maximum capacity.' });
|
|
}
|
|
}
|
|
|
|
const enrollment = await Enrollment.create({
|
|
userId: traineeId,
|
|
courseId,
|
|
status,
|
|
paymentStatus: course.price > 0 ? 'pending' : 'paid',
|
|
paymentAmount: course.price,
|
|
notes
|
|
});
|
|
|
|
const enrollmentWithDetails = await Enrollment.findByPk(enrollment.id, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'course',
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'trainer',
|
|
attributes: ['id', 'firstName', 'lastName']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'firstName', 'lastName', 'email', 'phone']
|
|
}
|
|
]
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: 'Trainee successfully assigned to course.',
|
|
enrollment: enrollmentWithDetails
|
|
});
|
|
} catch (error) {
|
|
console.error('Assign trainee error:', error);
|
|
res.status(500).json({ error: 'Server error.' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|