courseworx/backend/routes/attendance.js
mmabdalla 5477297914 v2.0.2 - Complete Plugin Architecture System and Multi-Currency Implementation
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.
2025-09-14 04:20:37 +03:00

397 lines
No EOL
11 KiB
JavaScript

const express = require('express');
const { body, param, validationResult } = require('express-validator');
const { auth, requireTrainer, requireSuperAdmin } = require('../middleware/auth');
const AttendanceRecord = require('../models/AttendanceRecord');
const ClassroomSession = require('../models/ClassroomSession');
const Course = require('../models/Course');
const User = require('../models/User');
const router = express.Router();
// Check in using QR code
router.post('/checkin', [
auth,
body('qrCodeData').isString().withMessage('QR code data is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { qrCodeData } = req.body;
const traineeId = req.user.id;
// Parse QR code data
let qrData;
try {
qrData = JSON.parse(qrCodeData);
} catch (error) {
return res.status(400).json({ error: 'Invalid QR code format' });
}
// Find session by QR code
const session = await ClassroomSession.findOne({
where: { qrCode: qrCodeData },
include: [
{
model: Course,
as: 'Course',
attributes: ['id', 'title', 'courseType', 'trainerId']
}
]
});
if (!session) {
return res.status(404).json({ error: 'Session not found or QR code expired' });
}
// Check if QR code is still valid
if (new Date() > session.qrCodeExpiry) {
return res.status(400).json({ error: 'QR code has expired' });
}
// Check if trainee is enrolled in the course
const enrollment = await Course.findOne({
where: {
id: session.courseId,
'$Enrollments.traineeId$': traineeId
},
include: [
{
model: require('../models/Enrollment'),
where: { traineeId },
required: true
}
]
});
if (!enrollment) {
return res.status(403).json({ error: 'You are not enrolled in this course' });
}
// Check if already checked in
const existingRecord = await AttendanceRecord.findOne({
where: {
sessionId: session.id,
traineeId
}
});
if (existingRecord && existingRecord.checkInTime) {
return res.status(400).json({ error: 'You have already checked in for this session' });
}
// Determine if late
const sessionStartTime = new Date(`${session.sessionDate}T${session.startTime}`);
const isLate = new Date() > sessionStartTime;
// Create or update attendance record
const attendanceData = {
sessionId: session.id,
traineeId,
checkInTime: new Date(),
checkInMethod: 'qr_code',
status: isLate ? 'late' : 'present',
isPresent: true
};
if (existingRecord) {
await existingRecord.update(attendanceData);
} else {
await AttendanceRecord.create(attendanceData);
}
res.json({
message: isLate ? 'Checked in successfully (marked as late)' : 'Checked in successfully',
attendance: {
sessionId: session.id,
courseTitle: session.Course.title,
sessionDate: session.sessionDate,
startTime: session.startTime,
endTime: session.endTime,
location: session.location,
checkInTime: attendanceData.checkInTime,
status: attendanceData.status
}
});
} catch (error) {
console.error('Error during check-in:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Check out using QR code
router.post('/checkout', [
auth,
body('qrCodeData').isString().withMessage('QR code data is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { qrCodeData } = req.body;
const traineeId = req.user.id;
// Parse QR code data
let qrData;
try {
qrData = JSON.parse(qrCodeData);
} catch (error) {
return res.status(400).json({ error: 'Invalid QR code format' });
}
// Find session by QR code
const session = await ClassroomSession.findOne({
where: { qrCode: qrCodeData },
include: [
{
model: Course,
as: 'Course',
attributes: ['id', 'title', 'courseType', 'trainerId']
}
]
});
if (!session) {
return res.status(404).json({ error: 'Session not found or QR code expired' });
}
// Check if QR code is still valid
if (new Date() > session.qrCodeExpiry) {
return res.status(400).json({ error: 'QR code has expired' });
}
// Find attendance record
const attendanceRecord = await AttendanceRecord.findOne({
where: {
sessionId: session.id,
traineeId
}
});
if (!attendanceRecord) {
return res.status(404).json({ error: 'No check-in record found for this session' });
}
if (attendanceRecord.checkOutTime) {
return res.status(400).json({ error: 'You have already checked out for this session' });
}
// Calculate duration
const checkOutTime = new Date();
const duration = Math.round((checkOutTime - attendanceRecord.checkInTime) / (1000 * 60)); // in minutes
// Determine if left early
const sessionEndTime = new Date(`${session.sessionDate}T${session.endTime}`);
const leftEarly = checkOutTime < sessionEndTime;
// Update attendance record
await attendanceRecord.update({
checkOutTime,
checkOutMethod: 'qr_code',
status: leftEarly ? 'left_early' : attendanceRecord.status,
duration
});
res.json({
message: leftEarly ? 'Checked out successfully (marked as left early)' : 'Checked out successfully',
attendance: {
sessionId: session.id,
courseTitle: session.Course.title,
sessionDate: session.sessionDate,
startTime: session.startTime,
endTime: session.endTime,
checkInTime: attendanceRecord.checkInTime,
checkOutTime,
duration,
status: leftEarly ? 'left_early' : attendanceRecord.status
}
});
} catch (error) {
console.error('Error during check-out:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Manual attendance management (for trainers)
router.post('/manual', [
auth,
requireTrainer,
body('sessionId').isUUID().withMessage('Invalid session ID'),
body('traineeId').isUUID().withMessage('Invalid trainee ID'),
body('action').isIn(['checkin', 'checkout']).withMessage('Action must be checkin or checkout'),
body('notes').optional().isString()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { sessionId, traineeId, action, notes } = req.body;
// Check if session exists and user has access
const session = await ClassroomSession.findByPk(sessionId, {
include: [
{
model: Course,
attributes: ['id', 'title', 'trainerId']
}
]
});
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
if (req.user.role !== 'super_admin' && session.Course.trainerId !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
// Check if trainee is enrolled
const enrollment = await Course.findOne({
where: {
id: session.courseId,
'$Enrollments.traineeId$': traineeId
},
include: [
{
model: require('../models/Enrollment'),
where: { traineeId },
required: true
}
]
});
if (!enrollment) {
return res.status(400).json({ error: 'Trainee is not enrolled in this course' });
}
// Find or create attendance record
let attendanceRecord = await AttendanceRecord.findOne({
where: { sessionId, traineeId }
});
if (!attendanceRecord) {
attendanceRecord = await AttendanceRecord.create({
sessionId,
traineeId,
checkInMethod: 'manual',
status: 'present',
isPresent: true
});
}
if (action === 'checkin') {
if (attendanceRecord.checkInTime) {
return res.status(400).json({ error: 'Trainee has already checked in' });
}
const sessionStartTime = new Date(`${session.sessionDate}T${session.startTime}`);
const isLate = new Date() > sessionStartTime;
await attendanceRecord.update({
checkInTime: new Date(),
checkInMethod: 'manual',
status: isLate ? 'late' : 'present',
isPresent: true,
notes: notes || attendanceRecord.notes
});
res.json({
message: 'Manual check-in recorded successfully',
attendance: attendanceRecord
});
} else if (action === 'checkout') {
if (!attendanceRecord.checkInTime) {
return res.status(400).json({ error: 'Trainee must check in before checking out' });
}
if (attendanceRecord.checkOutTime) {
return res.status(400).json({ error: 'Trainee has already checked out' });
}
const checkOutTime = new Date();
const duration = Math.round((checkOutTime - attendanceRecord.checkInTime) / (1000 * 60));
const sessionEndTime = new Date(`${session.sessionDate}T${session.endTime}`);
const leftEarly = checkOutTime < sessionEndTime;
await attendanceRecord.update({
checkOutTime,
checkOutMethod: 'manual',
status: leftEarly ? 'left_early' : attendanceRecord.status,
duration,
notes: notes || attendanceRecord.notes
});
res.json({
message: 'Manual check-out recorded successfully',
attendance: attendanceRecord
});
}
} catch (error) {
console.error('Error in manual attendance:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get trainee's attendance history
router.get('/trainee/:traineeId', [
auth,
requireTrainer,
param('traineeId').isUUID().withMessage('Invalid trainee ID')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { traineeId } = req.params;
const { courseId, page = 1, limit = 10 } = req.query;
const whereClause = { traineeId };
if (courseId) {
whereClause['$ClassroomSession.courseId$'] = courseId;
}
const attendance = await AttendanceRecord.findAndCountAll({
where: whereClause,
include: [
{
model: ClassroomSession,
as: 'ClassroomSession',
include: [
{
model: Course,
as: 'Course',
attributes: ['id', 'title', 'courseType']
}
]
},
{
model: User,
as: 'User',
attributes: ['id', 'firstName', 'lastName', 'email']
}
],
order: [['checkInTime', 'DESC']],
limit: parseInt(limit),
offset: (parseInt(page) - 1) * parseInt(limit)
});
res.json({
attendance: attendance.rows,
totalCount: attendance.count,
totalPages: Math.ceil(attendance.count / parseInt(limit)),
currentPage: parseInt(page)
});
} catch (error) {
console.error('Error fetching trainee attendance:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;