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.
353 lines
10 KiB
JavaScript
353 lines
10 KiB
JavaScript
const express = require('express');
|
|
const { body, param, query, validationResult } = require('express-validator');
|
|
const { auth, requireTrainer, requireSuperAdmin } = require('../middleware/auth');
|
|
const ClassroomSession = require('../models/ClassroomSession');
|
|
const Course = require('../models/Course');
|
|
const User = require('../models/User');
|
|
const AttendanceRecord = require('../models/AttendanceRecord');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const QRCode = require('qrcode');
|
|
const { getFrontendURL } = require('../utils/getServerIP');
|
|
const router = express.Router();
|
|
|
|
// Get all classroom sessions for a course
|
|
router.get('/course/:courseId', [
|
|
auth,
|
|
requireTrainer,
|
|
param('courseId').isUUID().withMessage('Invalid course ID')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { courseId } = req.params;
|
|
const { page = 1, limit = 10, status } = req.query;
|
|
|
|
// Check if user has access to this course
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found' });
|
|
}
|
|
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Access denied' });
|
|
}
|
|
|
|
const whereClause = { courseId };
|
|
if (status) {
|
|
whereClause.status = status;
|
|
}
|
|
|
|
const sessions = await ClassroomSession.findAndCountAll({
|
|
where: whereClause,
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'Course',
|
|
attributes: ['id', 'title', 'courseType']
|
|
}
|
|
],
|
|
order: [['sessionDate', 'DESC'], ['startTime', 'DESC']],
|
|
limit: parseInt(limit),
|
|
offset: (parseInt(page) - 1) * parseInt(limit)
|
|
});
|
|
|
|
res.json({
|
|
sessions: sessions.rows,
|
|
totalCount: sessions.count,
|
|
totalPages: Math.ceil(sessions.count / parseInt(limit)),
|
|
currentPage: parseInt(page)
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching classroom sessions:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Create a new classroom session
|
|
router.post('/', [
|
|
auth,
|
|
requireTrainer,
|
|
body('courseId').isUUID().withMessage('Invalid course ID'),
|
|
body('sessionDate').isISO8601().withMessage('Invalid session date'),
|
|
body('startTime').matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/).withMessage('Invalid start time format'),
|
|
body('endTime').matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/).withMessage('Invalid end time format'),
|
|
body('location').optional().isString().isLength({ max: 500 }),
|
|
body('roomNumber').optional().isString().isLength({ max: 50 }),
|
|
body('maxCapacity').optional().isInt({ min: 1 }),
|
|
body('notes').optional().isString()
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { courseId, sessionDate, startTime, endTime, location, roomNumber, maxCapacity, notes } = req.body;
|
|
|
|
// Check if course exists and user has access
|
|
const course = await Course.findByPk(courseId);
|
|
if (!course) {
|
|
return res.status(404).json({ error: 'Course not found' });
|
|
}
|
|
|
|
if (req.user.role !== 'super_admin' && course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Access denied' });
|
|
}
|
|
|
|
// Check if course is classroom type
|
|
if (course.courseType !== 'classroom' && course.courseType !== 'hybrid') {
|
|
return res.status(400).json({ error: 'Course must be classroom or hybrid type' });
|
|
}
|
|
|
|
// Generate unique QR code
|
|
const sessionId = uuidv4();
|
|
|
|
// Create QR code URL using server IP for network access
|
|
const baseUrl = process.env.FRONTEND_URL || getFrontendURL(3000);
|
|
const qrCodeUrl = `${baseUrl}/attendance/join/${sessionId}`;
|
|
|
|
const qrCode = await QRCode.toDataURL(qrCodeUrl);
|
|
|
|
// Set QR code expiry to end of session day + 1 day
|
|
const qrCodeExpiry = new Date(sessionDate);
|
|
qrCodeExpiry.setDate(qrCodeExpiry.getDate() + 1);
|
|
qrCodeExpiry.setHours(23, 59, 59, 999);
|
|
|
|
const session = await ClassroomSession.create({
|
|
id: sessionId,
|
|
courseId,
|
|
sessionDate,
|
|
startTime,
|
|
endTime,
|
|
location: location || course.location,
|
|
roomNumber,
|
|
qrCode: qrCodeUrl,
|
|
qrCodeExpiry,
|
|
maxCapacity,
|
|
notes
|
|
});
|
|
|
|
res.status(201).json({
|
|
session,
|
|
qrCodeImage: qrCode
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating classroom session:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Get QR code for a session
|
|
router.get('/:sessionId/qr-code', [
|
|
auth,
|
|
requireTrainer,
|
|
param('sessionId').isUUID().withMessage('Invalid session ID')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { sessionId } = req.params;
|
|
|
|
const session = await ClassroomSession.findByPk(sessionId, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'Course',
|
|
attributes: ['id', 'title', 'trainerId']
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: 'Session not found' });
|
|
}
|
|
|
|
// Check access
|
|
if (req.user.role !== 'super_admin' && session.Course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Access denied' });
|
|
}
|
|
|
|
// Generate QR code image from URL
|
|
const qrCodeImage = await QRCode.toDataURL(session.qrCode);
|
|
|
|
res.json({
|
|
session: {
|
|
id: session.id,
|
|
courseId: session.courseId,
|
|
sessionDate: session.sessionDate,
|
|
startTime: session.startTime,
|
|
endTime: session.endTime,
|
|
location: session.location,
|
|
roomNumber: session.roomNumber,
|
|
status: session.status,
|
|
qrCodeExpiry: session.qrCodeExpiry
|
|
},
|
|
qrCodeImage
|
|
});
|
|
} catch (error) {
|
|
console.error('Error generating QR code:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Update session status
|
|
router.patch('/:sessionId/status', [
|
|
auth,
|
|
requireTrainer,
|
|
param('sessionId').isUUID().withMessage('Invalid session ID'),
|
|
body('status').isIn(['scheduled', 'in_progress', 'completed', 'cancelled']).withMessage('Invalid status')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { sessionId } = req.params;
|
|
const { status } = req.body;
|
|
|
|
const session = await ClassroomSession.findByPk(sessionId, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'Course',
|
|
attributes: ['id', 'trainerId']
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: 'Session not found' });
|
|
}
|
|
|
|
// Check access
|
|
if (req.user.role !== 'super_admin' && session.Course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Access denied' });
|
|
}
|
|
|
|
await session.update({ status });
|
|
|
|
res.json({ message: 'Session status updated successfully', session });
|
|
} catch (error) {
|
|
console.error('Error updating session status:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Get session attendance
|
|
router.get('/:sessionId/attendance', [
|
|
auth,
|
|
requireTrainer,
|
|
param('sessionId').isUUID().withMessage('Invalid session ID')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { sessionId } = req.params;
|
|
|
|
const session = await ClassroomSession.findByPk(sessionId, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'Course',
|
|
attributes: ['id', 'title', 'trainerId']
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: 'Session not found' });
|
|
}
|
|
|
|
// Check access
|
|
if (req.user.role !== 'super_admin' && session.Course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Access denied' });
|
|
}
|
|
|
|
const attendance = await AttendanceRecord.findAll({
|
|
where: { sessionId },
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'User',
|
|
attributes: ['id', 'firstName', 'lastName', 'email', 'phone']
|
|
}
|
|
],
|
|
order: [['checkInTime', 'ASC']]
|
|
});
|
|
|
|
res.json({
|
|
session: {
|
|
id: session.id,
|
|
courseId: session.courseId,
|
|
sessionDate: session.sessionDate,
|
|
startTime: session.startTime,
|
|
endTime: session.endTime,
|
|
location: session.location,
|
|
roomNumber: session.roomNumber,
|
|
status: session.status
|
|
},
|
|
attendance
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching attendance:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// Delete a session
|
|
router.delete('/:sessionId', [
|
|
auth,
|
|
requireTrainer,
|
|
param('sessionId').isUUID().withMessage('Invalid session ID')
|
|
], async (req, res) => {
|
|
try {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { sessionId } = req.params;
|
|
|
|
const session = await ClassroomSession.findByPk(sessionId, {
|
|
include: [
|
|
{
|
|
model: Course,
|
|
as: 'Course',
|
|
attributes: ['id', 'trainerId']
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!session) {
|
|
return res.status(404).json({ error: 'Session not found' });
|
|
}
|
|
|
|
// Check access
|
|
if (req.user.role !== 'super_admin' && session.Course.trainerId !== req.user.id) {
|
|
return res.status(403).json({ error: 'Access denied' });
|
|
}
|
|
|
|
// Delete attendance records first
|
|
await AttendanceRecord.destroy({ where: { sessionId } });
|
|
|
|
// Delete session
|
|
await session.destroy();
|
|
|
|
res.json({ message: 'Session deleted successfully' });
|
|
} catch (error) {
|
|
console.error('Error deleting session:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|