From 95f437717067bde38f9183933c4e6f2ed64dfd23 Mon Sep 17 00:00:00 2001 From: "Mahmoud M. Abdalla" Date: Wed, 20 Aug 2025 20:57:41 +0300 Subject: [PATCH] v1.2.0: Responsive Design & Bug Fixes - Implemented responsive dropdown menu for course actions - Fixed trainer assignment dropdown population - Resolved available trainees API routing conflict - Enhanced mobile responsiveness and accessibility - Fixed setup page redirect issues - Improved code quality and ESLint compliance - Added comprehensive logging and debugging - Updated version.txt with detailed changelog --- README.md | 4 +- backend/routes/auth.js | 249 ++++++- backend/routes/courses.js | 4 + backend/routes/enrollments.js | 318 ++++++++ backend/scripts/setup-database.js | 62 +- backend/server.js | 2 + frontend/craco.config.js | 11 + frontend/package.json | 3 +- frontend/src/App.js | 44 +- frontend/src/components/Layout.js | 4 +- .../src/components/TrainerAssignmentModal.js | 6 + frontend/src/contexts/AuthContext.js | 51 +- frontend/src/pages/CourseDetail.js | 117 ++- frontend/src/pages/CourseEnrollment.js | 357 +++++++++ frontend/src/pages/Dashboard.js | 258 +++---- frontend/src/pages/Login.js | 33 +- frontend/src/pages/Setup.js | 283 ++++++++ frontend/src/pages/TraineeLogin.js | 164 +++++ frontend/src/pages/Users.js | 4 +- frontend/src/services/api.js | 38 +- start-courseworx.bat | 37 + start-courseworx.ps1 | 46 +- tat -an ๏ผ findstr ๏€บ5000 | 10 + version.txt | 680 ++---------------- 24 files changed, 1807 insertions(+), 978 deletions(-) create mode 100644 frontend/src/pages/CourseEnrollment.js create mode 100644 frontend/src/pages/Setup.js create mode 100644 frontend/src/pages/TraineeLogin.js create mode 100644 tat -an ๏ผ findstr ๏€บ5000 diff --git a/README.md b/README.md index 0ea66b8..96b9fba 100644 --- a/README.md +++ b/README.md @@ -171,8 +171,8 @@ cd .. DB_HOST=localhost DB_PORT=5432 DB_NAME=courseworx - DB_USER=your_postgres_username - DB_PASSWORD=your_postgres_password + DB_USER=mabdalla + DB_PASSWORD=7ouDa-123q # JWT Configuration JWT_SECRET=your_super_secret_jwt_key_here_make_it_long_and_random diff --git a/backend/routes/auth.js b/backend/routes/auth.js index a09914f..d755285 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -14,11 +14,109 @@ const generateToken = (userId) => { }); }; +// @route GET /api/auth/setup-status +// @desc Check if system setup is required +// @access Public +router.get('/setup-status', async (req, res) => { + try { + const superAdminCount = await User.count({ where: { role: 'super_admin' } }); + res.json({ + setupRequired: superAdminCount === 0, + superAdminCount + }); + } catch (error) { + console.error('Setup status check error:', error); + res.status(500).json({ error: 'Server error.' }); + } +}); + +// @route POST /api/auth/setup +// @desc First-time setup - Create super admin (Public) +// @access Public (only when no SA exists) +router.post('/setup', [ + body('firstName').isLength({ min: 2, max: 50 }), + body('lastName').isLength({ min: 2, max: 50 }), + body('email').isEmail().normalizeEmail(), + body('password').isLength({ min: 6 }), + body('phone').notEmpty().withMessage('Phone number is required'), + body('isActive').optional().isBoolean() +], async (req, res) => { + try { + // Check if setup is already completed + const superAdminCount = await User.count({ where: { role: 'super_admin' } }); + if (superAdminCount > 0) { + return res.status(400).json({ error: 'System setup already completed.' }); + } + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { firstName, lastName, email, password, phone } = req.body; + + // Check if user already exists (by email or phone) + const existingUser = await User.findOne({ + where: { + [require('sequelize').Op.or]: [ + { email }, + { phone } + ] + } + }); + + if (existingUser) { + if (existingUser.email === email) { + return res.status(400).json({ error: 'User with this email already exists.' }); + } else { + return res.status(400).json({ error: 'User with this phone number already exists.' }); + } + } + + // Create the first super admin + const superAdmin = await User.create({ + firstName, + lastName, + email, + password, + role: 'super_admin', + phone, + isActive: true + }); + + console.log('Super Admin created during setup:', { + id: superAdmin.id, + email: superAdmin.email, + phone: superAdmin.phone, + role: superAdmin.role + }); + + // Generate token for immediate login + const token = generateToken(superAdmin.id); + + res.status(201).json({ + message: 'System setup completed successfully. Super Admin account created.', + token, + user: { + id: superAdmin.id, + firstName: superAdmin.firstName, + lastName: superAdmin.lastName, + email: superAdmin.email, + phone: superAdmin.phone, + role: superAdmin.role + } + }); + } catch (error) { + console.error('Setup error:', error); + res.status(500).json({ error: 'Server error.' }); + } +}); + // @route POST /api/auth/login // @desc Login user // @access Public router.post('/login', [ - body('email').isEmail().normalizeEmail(), + body('identifier').notEmpty().withMessage('Email or phone number is required'), body('password').isLength({ min: 6 }) ], async (req, res) => { try { @@ -27,10 +125,20 @@ router.post('/login', [ return res.status(400).json({ errors: errors.array() }); } - const { email, password } = req.body; + const { identifier, password } = req.body; - const user = await User.findOne({ where: { email } }); - console.log('Login attempt for email:', email, 'User found:', !!user, 'User active:', user?.isActive); + // Find user by email or phone + const user = await User.findOne({ + where: { + [require('sequelize').Op.or]: [ + { email: identifier }, + { phone: identifier } + ], + isActive: true + } + }); + + console.log('Login attempt for identifier:', identifier, 'User found:', !!user, 'User active:', user?.isActive); if (!user || !user.isActive) { return res.status(401).json({ error: 'Invalid credentials or account inactive.' }); @@ -55,9 +163,9 @@ router.post('/login', [ firstName: user.firstName, lastName: user.lastName, email: user.email, + phone: user.phone, role: user.role, avatar: user.avatar, - phone: user.phone, requiresPasswordChange: user.requiresPasswordChange } }); @@ -77,7 +185,8 @@ router.post('/register', [ body('lastName').isLength({ min: 2, max: 50 }), body('email').isEmail().normalizeEmail(), body('password').isLength({ min: 6 }), - body('role').isIn(['super_admin', 'trainer', 'trainee']) + body('role').isIn(['super_admin', 'trainer', 'trainee']), + body('phone').notEmpty().withMessage('Phone number is required') ], async (req, res) => { try { const errors = validationResult(req); @@ -85,7 +194,7 @@ router.post('/register', [ return res.status(400).json({ errors: errors.array() }); } - const { firstName, lastName, email, password, role, phone } = req.body; + const { firstName, lastName, email, password, role, phone, isActive = true } = req.body; // Check if user already exists const existingUser = await User.findOne({ where: { email } }); @@ -93,13 +202,22 @@ router.post('/register', [ return res.status(400).json({ error: 'User with this email already exists.' }); } + // Prevent creating another super admin if not during setup + if (role === 'super_admin') { + const superAdminCount = await User.count({ where: { role: 'super_admin' } }); + if (superAdminCount > 0) { + return res.status(400).json({ error: 'Super Admin account already exists. Cannot create another one.' }); + } + } + const user = await User.create({ firstName, lastName, email, password, role, - phone + phone, + isActive }); console.log('User created successfully:', { @@ -117,7 +235,8 @@ router.post('/register', [ lastName: user.lastName, email: user.email, role: user.role, - phone: user.phone + phone: user.phone, + isActive: user.isActive } }); } catch (error) { @@ -156,7 +275,7 @@ router.put('/profile', [ auth, body('firstName').optional().isLength({ min: 2, max: 50 }), body('lastName').optional().isLength({ min: 2, max: 50 }), - body('phone').optional().isMobilePhone() + body('phone').optional() ], async (req, res) => { try { const errors = validationResult(req); @@ -265,4 +384,114 @@ router.put('/first-password-change', [ } }); +// @route POST /api/auth/trainee-login +// @desc Login trainee with phone or email +// @access Public +router.post('/trainee-login', [ + body('identifier').notEmpty().withMessage('Phone or email is required'), + body('password').isLength({ min: 6 }) +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { identifier, password } = req.body; + + // Find user by email or phone + const user = await User.findOne({ + where: { + [require('sequelize').Op.or]: [ + { email: identifier }, + { phone: identifier } + ], + role: 'trainee', + isActive: true + } + }); + + if (!user) { + return res.status(401).json({ error: 'Invalid credentials or account inactive.' }); + } + + const isPasswordValid = await user.comparePassword(password); + + if (!isPasswordValid) { + return res.status(401).json({ error: 'Invalid credentials.' }); + } + + // Update last login + await user.update({ lastLogin: new Date() }); + + const token = generateToken(user.id); + + res.json({ + token, + user: { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + phone: user.phone, + role: user.role, + avatar: user.avatar, + requiresPasswordChange: user.requiresPasswordChange + } + }); + } catch (error) { + console.error('Trainee login error:', error); + res.status(500).json({ error: 'Server error.' }); + } +}); + +// @route POST /api/auth/check-enrollment +// @desc Check if trainee is enrolled in any courses +// @access Private (Trainee) +router.post('/check-enrollment', auth, async (req, res) => { + try { + if (req.user.role !== 'trainee') { + return res.status(403).json({ error: 'Only trainees can check enrollment.' }); + } + + const { Enrollment, Course } = require('../models'); + + const enrollments = await Enrollment.findAll({ + where: { + userId: req.user.id, + status: ['active', 'pending'] + }, + include: [ + { + model: Course, + as: 'course', + attributes: ['id', 'title', 'thumbnail', 'description'], + include: [ + { + model: User, + as: 'trainer', + attributes: ['id', 'firstName', 'lastName'] + } + ] + } + ], + order: [['enrolledAt', 'DESC']] + }); + + res.json({ + hasEnrollments: enrollments.length > 0, + enrollments: enrollments.map(e => ({ + id: e.id, + status: e.status, + progress: e.progress, + enrolledAt: e.enrolledAt, + course: e.course + })) + }); + } catch (error) { + console.error('Check enrollment error:', error); + res.status(500).json({ error: 'Server error.' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/courses.js b/backend/routes/courses.js index ab19f29..fa5efcf 100644 --- a/backend/routes/courses.js +++ b/backend/routes/courses.js @@ -437,6 +437,8 @@ router.put('/:id/assign-trainer', [ // @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', @@ -446,6 +448,8 @@ router.get('/trainers/available', auth, requireSuperAdmin, async (req, res) => { 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); diff --git a/backend/routes/enrollments.js b/backend/routes/enrollments.js index c6fe735..45893f7 100644 --- a/backend/routes/enrollments.js +++ b/backend/routes/enrollments.js @@ -132,6 +132,122 @@ router.get('/my', auth, async (req, res) => { } }); +// @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 @@ -548,4 +664,206 @@ router.get('/stats/overview', auth, async (req, res) => { } }); +// @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; \ No newline at end of file diff --git a/backend/scripts/setup-database.js b/backend/scripts/setup-database.js index 8e836ed..7e34d3a 100644 --- a/backend/scripts/setup-database.js +++ b/backend/scripts/setup-database.js @@ -1,71 +1,25 @@ const { sequelize } = require('../config/database'); // Import all models to ensure they are registered with Sequelize require('../models'); -const { User } = require('../models'); require('dotenv').config(); const setupDatabase = async () => { try { - console.log('๐Ÿ”„ Setting up database...'); + console.log('๐Ÿ”„ Setting up clean database...'); // Test database connection await sequelize.authenticate(); console.log('โœ… Database connection established.'); - // Sync database with force to recreate all tables + // Sync database with force to recreate all tables (clean slate) await sequelize.sync({ force: true }); - console.log('โœ… Database synchronized successfully.'); + console.log('โœ… Database synchronized successfully - all tables recreated.'); - // Create super admin user - const superAdmin = await User.create({ - firstName: 'Super', - lastName: 'Admin', - email: 'admin@courseworx.com', - password: 'admin123', - role: 'super_admin', - phone: '+1234567890', - isActive: true - }); - - console.log('โœ… Super admin user created successfully.'); - console.log('๐Ÿ“ง Email: admin@courseworx.com'); - console.log('๐Ÿ”‘ Password: admin123'); - - // Create sample trainer - const trainer = await User.create({ - firstName: 'John', - lastName: 'Trainer', - email: 'trainer@courseworx.com', - password: 'trainer123', - role: 'trainer', - phone: '+1234567891', - isActive: true - }); - - console.log('โœ… Sample trainer created successfully.'); - console.log('๐Ÿ“ง Email: trainer@courseworx.com'); - console.log('๐Ÿ”‘ Password: trainer123'); - - // Create sample trainee - const trainee = await User.create({ - firstName: 'Jane', - lastName: 'Trainee', - email: 'trainee@courseworx.com', - password: 'trainee123', - role: 'trainee', - phone: '+1234567892', - isActive: true - }); - - console.log('โœ… Sample trainee created successfully.'); - console.log('๐Ÿ“ง Email: trainee@courseworx.com'); - console.log('๐Ÿ”‘ Password: trainee123'); - - console.log('\n๐ŸŽ‰ Database setup completed successfully!'); - console.log('\n๐Ÿ“‹ Default Users:'); - console.log('Super Admin: admin@courseworx.com / admin123'); - console.log('Trainer: trainer@courseworx.com / trainer123'); - console.log('Trainee: trainee@courseworx.com / trainee123'); + console.log('\n๐ŸŽ‰ Clean database setup completed successfully!'); + console.log('\n๐Ÿ“‹ Database is now ready for your data:'); + console.log('- All tables have been recreated'); + console.log('- No demo users exist'); + console.log('- Ready for fresh data input'); process.exit(0); } catch (error) { diff --git a/backend/server.js b/backend/server.js index c5a07d7..56609f0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -24,6 +24,8 @@ app.use(cors({ origin: process.env.CORS_ORIGIN || 'http://localhost:3000', credentials: true })); + +app.use(express.json({ limit: '10mb' })); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); diff --git a/frontend/craco.config.js b/frontend/craco.config.js index f18c4a3..b450495 100644 --- a/frontend/craco.config.js +++ b/frontend/craco.config.js @@ -16,6 +16,17 @@ module.exports = { '/api': { target: 'http://localhost:5000', changeOrigin: true, + secure: false, + logLevel: 'debug', + onProxyReq: (proxyReq, req, res) => { + // Ensure proper content-type headers + if (req.body) { + const bodyData = JSON.stringify(req.body); + proxyReq.setHeader('Content-Type', 'application/json'); + proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); + proxyReq.write(bodyData); + } + }, }, }, }, diff --git a/frontend/package.json b/frontend/package.json index eda5748..8411fa0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,6 +50,5 @@ "autoprefixer": "^10.4.16", "postcss": "^8.4.32", "tailwindcss": "^3.3.6" - }, - "proxy": "http://localhost:5000" + } } diff --git a/frontend/src/App.js b/frontend/src/App.js index 4575477..55be855 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -2,6 +2,8 @@ import React from 'react'; import { Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import Login from './pages/Login'; +import TraineeLogin from './pages/TraineeLogin'; +import Setup from './pages/Setup'; import Dashboard from './pages/Dashboard'; import Courses from './pages/Courses'; import CourseDetail from './pages/CourseDetail'; @@ -14,15 +16,21 @@ import LoadingSpinner from './components/LoadingSpinner'; import CourseEdit from './pages/CourseEdit'; import CourseContent from './pages/CourseContent'; import CourseContentViewer from './pages/CourseContentViewer'; +import CourseEnrollment from './pages/CourseEnrollment'; import Home from './pages/Home'; const PrivateRoute = ({ children, allowedRoles = [] }) => { - const { user, loading } = useAuth(); + const { user, loading, setupRequired } = useAuth(); if (loading) { return ; } + // If setup is required, redirect to setup + if (setupRequired) { + return ; + } + if (!user) { return ; } @@ -35,13 +43,35 @@ const PrivateRoute = ({ children, allowedRoles = [] }) => { }; const AppRoutes = () => { - const { user } = useAuth(); + const { user, loading, setupRequired } = useAuth(); + console.log('AppRoutes render - user:', user, 'loading:', loading, 'setupRequired:', setupRequired); + + // Show loading while checking setup and auth status + if (loading) { + return ; + } + + // If setup is required, show setup page + if (setupRequired) { + console.log('Setup required, showing setup page'); + return ( + + } /> + } /> + + ); + } + + console.log('Setup not required, showing normal routes'); return ( : } /> + : + } /> {/* Public homepage route */} } /> @@ -65,11 +95,21 @@ const AppRoutes = () => { } /> + + + + } /> } /> + + + + } /> diff --git a/frontend/src/components/Layout.js b/frontend/src/components/Layout.js index e786fd3..7d1aff1 100644 --- a/frontend/src/components/Layout.js +++ b/frontend/src/components/Layout.js @@ -58,7 +58,9 @@ const Layout = () => { {/* Logo and Navigation */}
- CourseWorx +
+ CX +
{/* Navigation Items */} diff --git a/frontend/src/components/TrainerAssignmentModal.js b/frontend/src/components/TrainerAssignmentModal.js index 66e80d3..6de31a1 100644 --- a/frontend/src/components/TrainerAssignmentModal.js +++ b/frontend/src/components/TrainerAssignmentModal.js @@ -20,6 +20,12 @@ const TrainerAssignmentModal = ({ isOpen, onClose, courseId, currentTrainer }) = () => coursesAPI.getAvailableTrainers(), { enabled: isOpen, + onSuccess: (data) => { + console.log('Available trainers loaded:', data); + console.log('Data structure:', JSON.stringify(data, null, 2)); + console.log('Data.trainers:', data?.trainers); + console.log('Data.trainers length:', data?.trainers?.length); + }, onError: (error) => { console.error('Trainer loading error:', error); toast.error('Failed to load trainers'); diff --git a/frontend/src/contexts/AuthContext.js b/frontend/src/contexts/AuthContext.js index ead263d..f7bbe9a 100644 --- a/frontend/src/contexts/AuthContext.js +++ b/frontend/src/contexts/AuthContext.js @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useState, useEffect } from 'react'; +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { authAPI } from '../services/api'; import toast from 'react-hot-toast'; @@ -15,17 +15,39 @@ export const useAuth = () => { export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); + const [setupRequired, setSetupRequired] = useState(false); - useEffect(() => { - checkAuth(); + const checkSetupAndAuth = useCallback(async () => { + try { + // First check if setup is required + const setupResponse = await authAPI.setupStatus(); + const { setupRequired: needsSetup } = setupResponse.data; + + if (needsSetup) { + setSetupRequired(true); + setLoading(false); + return; + } + + // If setup is not required, check authentication + await checkAuth(); + } catch (error) { + console.error('Setup and auth check failed:', error); + setLoading(false); + } }, []); - const checkAuth = async () => { + const checkAuth = useCallback(async () => { try { const token = localStorage.getItem('token'); if (token) { const response = await authAPI.getCurrentUser(); - setUser(response.data.user); + if (response.data && response.data.user) { + setUser(response.data.user); + } else { + // Token is invalid, remove it + localStorage.removeItem('token'); + } } } catch (error) { console.error('Auth check failed:', error); @@ -33,12 +55,16 @@ export const AuthProvider = ({ children }) => { } finally { setLoading(false); } - }; + }, []); - const login = async (email, password) => { + useEffect(() => { + checkSetupAndAuth(); + }, [checkSetupAndAuth]); + + const login = async (identifier, password) => { try { - console.log('Attempting login with email:', email); - const response = await authAPI.login(email, password); + console.log('Attempting login with identifier:', identifier); + const response = await authAPI.login(identifier, password); const { token, user } = response.data; console.log('Login successful, user:', user); @@ -91,12 +117,19 @@ export const AuthProvider = ({ children }) => { }; const updateUser = (userData) => { + console.log('updateUser called with:', userData); setUser(userData); + // If we have a user, setup is no longer required + if (userData) { + console.log('Setting setupRequired to false'); + setSetupRequired(false); + } }; const value = { user, loading, + setupRequired, login, logout, updateProfile, diff --git a/frontend/src/pages/CourseDetail.js b/frontend/src/pages/CourseDetail.js index d2fe748..a7f5e90 100644 --- a/frontend/src/pages/CourseDetail.js +++ b/frontend/src/pages/CourseDetail.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from 'react-query'; import { useAuth } from '../contexts/AuthContext'; @@ -11,6 +11,8 @@ import { BookOpenIcon, CogIcon, EyeIcon, + UserPlusIcon, + EllipsisVerticalIcon, } from '@heroicons/react/24/outline'; import LoadingSpinner from '../components/LoadingSpinner'; import TrainerAssignmentModal from '../components/TrainerAssignmentModal'; @@ -21,7 +23,23 @@ const CourseDetail = () => { const { user, isTrainee, isTrainer, isSuperAdmin } = useAuth(); const [enrolling, setEnrolling] = useState(false); const [showTrainerModal, setShowTrainerModal] = useState(false); + const [showActionsDropdown, setShowActionsDropdown] = useState(false); const queryClient = useQueryClient(); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setShowActionsDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); const { data: course, isLoading, error } = useQuery( ['course', id], @@ -117,32 +135,79 @@ const CourseDetail = () => {

{courseData.title}

{courseData.shortDescription}

-
- + + + + + {/* Dropdown Menu */} + {showActionsDropdown && ( +
+
+ {/* View Content - Always visible */} + setShowActionsDropdown(false)} + > + + View Content + + + {/* Manage Content - Trainer/Admin only */} + {(isTrainer || isSuperAdmin) && ( + setShowActionsDropdown(false)} + > + + Manage Content + + )} + + {/* Manage Enrollment - Trainer/Admin only */} + {(isTrainer || isSuperAdmin) && ( + setShowActionsDropdown(false)} + > + + Manage Enrollment + + )} + + {/* Assign Trainer - Super Admin only */} + {isSuperAdmin && ( + + )} +
+
)}
diff --git a/frontend/src/pages/CourseEnrollment.js b/frontend/src/pages/CourseEnrollment.js new file mode 100644 index 0000000..36841bc --- /dev/null +++ b/frontend/src/pages/CourseEnrollment.js @@ -0,0 +1,357 @@ +import React, { useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { useQuery, useMutation, useQueryClient } from 'react-query'; +import { useAuth } from '../contexts/AuthContext'; +import { coursesAPI, enrollmentsAPI } from '../services/api'; +import { + UserPlusIcon, + UsersIcon, + AcademicCapIcon, + MagnifyingGlassIcon, + CheckIcon, + XMarkIcon, + PlusIcon, + TrashIcon, + EyeIcon, + ClockIcon, + CheckCircleIcon, + ExclamationTriangleIcon +} from '@heroicons/react/24/outline'; +import LoadingSpinner from '../components/LoadingSpinner'; +import toast from 'react-hot-toast'; + +const CourseEnrollment = () => { + const { id } = useParams(); + const { user, isTrainer, isSuperAdmin } = useAuth(); + const queryClient = useQueryClient(); + + const [showAssignModal, setShowAssignModal] = useState(false); + const [selectedTrainees, setSelectedTrainees] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [assigning, setAssigning] = useState(false); + + // Get course details + const { data: course, isLoading: courseLoading } = useQuery( + ['course', id], + () => coursesAPI.getById(id), + { enabled: !!id } + ); + + // Get enrolled trainees + const { data: enrolledData, isLoading: enrolledLoading } = useQuery( + ['enrollments', 'course', id, 'trainees'], + () => enrollmentsAPI.getCourseTrainees(id), + { enabled: !!id } + ); + + // Get available trainees + const { data: availableData, isLoading: availableLoading } = useQuery( + ['enrollments', 'available-trainees', id], + () => enrollmentsAPI.getAvailableTrainees({ courseId: id }), + { enabled: !!id && showAssignModal } + ); + + // Bulk enrollment mutation + const bulkEnrollMutation = useMutation( + (data) => enrollmentsAPI.bulkEnroll(data), + { + onSuccess: (data) => { + queryClient.invalidateQueries(['enrollments', 'course', id, 'trainees']); + queryClient.invalidateQueries(['enrollments', 'available-trainees', id]); + toast.success(data.message); + setShowAssignModal(false); + setSelectedTrainees([]); + setAssigning(false); + }, + onError: (error) => { + toast.error(error.response?.data?.error || 'Failed to assign trainees'); + setAssigning(false); + } + } + ); + + // Single trainee assignment mutation + const assignTraineeMutation = useMutation( + (data) => enrollmentsAPI.assignTrainee(data), + { + onSuccess: (data) => { + queryClient.invalidateQueries(['enrollments', 'course', id, 'trainees']); + queryClient.invalidateQueries(['enrollments', 'available-trainees', id]); + toast.success(data.message); + setShowAssignModal(false); + setSelectedTrainees([]); + setAssigning(false); + }, + onError: (error) => { + toast.error(error.response?.data?.error || 'Failed to assign trainee'); + setAssigning(false); + } + } + ); + + const handleBulkAssign = async () => { + if (selectedTrainees.length === 0) { + toast.error('Please select at least one trainee'); + return; + } + + setAssigning(true); + bulkEnrollMutation.mutate({ + courseId: id, + traineeIds: selectedTrainees, + status: 'active', + notes: `Bulk assigned by ${user.firstName} ${user.lastName}` + }); + }; + + const handleSingleAssign = async (traineeId) => { + setAssigning(true); + assignTraineeMutation.mutate({ + courseId: id, + traineeId, + status: 'active', + notes: `Assigned by ${user.firstName} ${user.lastName}` + }); + }; + + const toggleTraineeSelection = (traineeId) => { + setSelectedTrainees(prev => + prev.includes(traineeId) + ? prev.filter(id => id !== traineeId) + : [...prev, traineeId] + ); + }; + + const filteredAvailableTrainees = availableData?.trainees?.filter(trainee => + trainee.firstName?.toLowerCase().includes(searchTerm.toLowerCase()) || + trainee.lastName?.toLowerCase().includes(searchTerm.toLowerCase()) || + trainee.email?.toLowerCase().includes(searchTerm.toLowerCase()) || + trainee.phone?.includes(searchTerm) + ) || []; + + const isLoading = courseLoading || enrolledLoading; + + if (isLoading) { + return ; + } + + if (!course) { + return ( +
+

Course not found.

+
+ ); + } + + // Check if user has permission to manage this course + const canManage = isSuperAdmin || (isTrainer && course.trainerId === user.id); + if (!canManage) { + return ( +
+

You don't have permission to manage this course.

+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Course Enrollment

+

Manage trainees for "{course.title}"

+
+
+ + + View Course + + +
+
+ + {/* Course Info */} +
+
+
+ +
+
+

{course.title}

+

+ Trainer: {course.trainer?.firstName} {course.trainer?.lastName} +

+
+
+

Enrolled Trainees

+

+ {enrolledData?.trainees?.length || 0} +

+
+
+
+ + {/* Enrolled Trainees */} +
+
+

Enrolled Trainees

+ + {enrolledData?.trainees?.length || 0} trainees + +
+ + {enrolledData?.trainees?.length > 0 ? ( +
+ {enrolledData.trainees.map((trainee) => ( +
+
+
+ + {trainee.firstName?.charAt(0)}{trainee.lastName?.charAt(0)} + +
+
+

+ {trainee.firstName} {trainee.lastName} +

+

{trainee.email}

+
+
+
+
+

Progress

+

{trainee.progress || 0}%

+
+
+

Status

+ + {trainee.status === 'active' && } + {trainee.status === 'pending' && } + {trainee.status === 'completed' && } + {trainee.status} + +
+
+
+ ))} +
+ ) : ( +
+ +

No trainees enrolled yet.

+

Assign trainees to get started.

+
+ )} +
+ + {/* Assign Trainees Modal */} + {showAssignModal && ( +
+
+
+

Assign Trainees

+ +
+ + {/* Search */} +
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500" + /> +
+
+ + {/* Available Trainees */} +
+ {availableLoading ? ( + + ) : filteredAvailableTrainees.length > 0 ? ( +
+ {filteredAvailableTrainees.map((trainee) => ( +
+
+ toggleTraineeSelection(trainee.id)} + className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" + /> +
+ + {trainee.firstName?.charAt(0)}{trainee.lastName?.charAt(0)} + +
+
+

+ {trainee.firstName} {trainee.lastName} +

+

{trainee.email}

+
+
+ +
+ ))} +
+ ) : ( +
+

No available trainees found.

+
+ )} +
+ + {/* Actions */} +
+ + +
+
+
+ )} +
+ ); +}; + +export default CourseEnrollment; \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.js b/frontend/src/pages/Dashboard.js index 3654506..2436c5c 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -84,46 +84,13 @@ const Dashboard = () => { const { data: userStats, isLoading: userStatsLoading } = useQuery( ['users', 'stats'], () => usersAPI.getStats(), - { - enabled: isSuperAdmin, - onSuccess: (data) => { - console.log('User stats response:', data); - console.log('User stats data structure:', { - totalUsers: data?.stats?.totalUsers, - trainers: data?.stats?.trainers, - trainees: data?.stats?.trainees - }); - console.log('Raw userStats object:', userStats); - }, - onError: (error) => { - console.error('User stats error:', error); - console.error('User stats error response:', error.response); - }, - refetchOnWindowFocus: false, - staleTime: 0 - } + { enabled: isSuperAdmin } ); const { data: courseStats, isLoading: courseStatsLoading } = useQuery( ['courses', 'stats'], () => coursesAPI.getStats(), - { - enabled: isSuperAdmin || isTrainer, - onSuccess: (data) => { - console.log('Course stats response:', data); - console.log('Course stats data structure:', { - totalCourses: data?.stats?.totalCourses, - publishedCourses: data?.stats?.publishedCourses - }); - console.log('Raw courseStats object:', courseStats); - }, - onError: (error) => { - console.error('Course stats error:', error); - console.error('Course stats error response:', error.response); - }, - refetchOnWindowFocus: false, - staleTime: 0 - } + { enabled: isSuperAdmin || isTrainer } ); // New queries for real counts @@ -153,154 +120,100 @@ const Dashboard = () => { return ; } - // Debug section to show raw API responses - const debugSection = ( -
-

Debug Information

-
-

User Stats: {JSON.stringify(userStats)}

-

Course Stats: {JSON.stringify(courseStats)}

-

User Role: {user?.role}

-

Is Super Admin: {isSuperAdmin ? 'Yes' : 'No'}

-

Card Values:

-
    -
  • Total Users: {userStats?.data?.stats?.totalUsers} (type: {typeof userStats?.data?.stats?.totalUsers})
  • -
  • Trainers: {userStats?.data?.stats?.trainers} (type: {typeof userStats?.data?.stats?.trainers})
  • -
  • Trainees: {userStats?.data?.stats?.trainees} (type: {typeof userStats?.data?.stats?.trainees})
  • -
  • Total Courses: {courseStats?.data?.stats?.totalCourses} (type: {typeof courseStats?.data?.stats?.totalCourses})
  • -
-

Direct Test Values:

-
    -
  • userStats?.data?.stats?.totalUsers: "{userStats?.data?.stats?.totalUsers}"
  • -
  • userStats?.data?.stats?.trainers: "{userStats?.data?.stats?.trainers}"
  • -
  • userStats?.data?.stats?.trainees: "{userStats?.data?.stats?.trainees}"
  • -
  • courseStats?.data?.stats?.totalCourses: "{courseStats?.data?.stats?.totalCourses}"
  • -
-

Loading States:

-
    -
  • userStatsLoading: {userStatsLoading ? 'true' : 'false'}
  • -
  • courseStatsLoading: {courseStatsLoading ? 'true' : 'false'}
  • -
+ const renderSuperAdminDashboard = () => ( +
+
+
+
+
+ +
+
+

Total Users

+

+ {userStats?.data?.stats?.totalUsers || 0} +

+
+
+
+ +
+
+
+ +
+
+

Trainers

+

+ {userStats?.data?.stats?.trainers || 0} +

+
+
+
+ +
+
+
+ +
+
+

Trainees

+

+ {userStats?.data?.stats?.trainees || 0} +

+
+
+
+ +
+
+
+ +
+
+

Total Courses

+

+ {courseStats?.data?.stats?.totalCourses || 0} +

+
+
+
-
- ); - const renderSuperAdminDashboard = () => { - console.log('Rendering SuperAdmin dashboard with data:', { - userStats, - courseStats, - totalUsers: userStats?.data?.stats?.totalUsers, - trainers: userStats?.data?.stats?.trainers, - trainees: userStats?.data?.stats?.trainees, - totalCourses: courseStats?.data?.stats?.totalCourses - }); - - return ( -
- {/* Test Card to verify data */} -
-

TEST CARD - Raw Data

-
-

userStats?.data?.stats?.totalUsers: {userStats?.data?.stats?.totalUsers}

-

userStats?.data?.stats?.trainers: {userStats?.data?.stats?.trainers}

-

userStats?.data?.stats?.trainees: {userStats?.data?.stats?.trainees}

-

courseStats?.data?.stats?.totalCourses: {courseStats?.data?.stats?.totalCourses}

+
+
+

Recent Users

+
+ {/* Add recent users list here */} +

No recent users to display

+ -
-
-
-
- -
-
-

Total Users

-

- {userStats?.data?.stats?.totalUsers || 0} -

-
+
+

System Overview

+
+
+ Active Users + {userStats?.data?.stats?.activeUsers || 0}
-
- -
-
-
- -
-
-

Trainers

-

- {userStats?.data?.stats?.trainers || 0} -

-
+
+ Published Courses + {courseStats?.data?.stats?.publishedCourses || 0}
-
- -
-
-
- -
-
-

Trainees

-

- {userStats?.data?.stats?.trainees || 0} -

-
+
+ Total Enrollments + {enrollmentStats?.data?.stats?.totalEnrollments || 0}
-
- -
-
-
- -
-
-

Total Courses

-

- {courseStats?.data?.stats?.totalCourses || 0} -

-
-
-
-
- -
-
-

Recent Users

-
- {/* Add recent users list here */} -

No recent users to display

-
-
- - -
-

System Overview

-
-
- Active Users - {userStats?.data?.stats?.activeUsers || 0} -
-
- Published Courses - {courseStats?.data?.stats?.publishedCourses || 0} -
-
- Total Enrollments - {enrollmentStats?.data?.stats?.totalEnrollments || 0} -
-
- Featured Courses - {courseStats?.data?.stats?.featuredCourses || 0} -
+
+ Featured Courses + {courseStats?.data?.stats?.featuredCourses || 0}
- ); - }; +
+ ); const renderTrainerDashboard = () => (
@@ -484,9 +397,6 @@ const Dashboard = () => {

Welcome back, {user?.firstName}! Here's what's happening.

- {/* Debug section */} - {debugSection} - {isSuperAdmin && renderSuperAdminDashboard()} {isTrainer && renderTrainerDashboard()} {isTrainee && renderTraineeDashboard()} diff --git a/frontend/src/pages/Login.js b/frontend/src/pages/Login.js index bdcda20..5e07c83 100644 --- a/frontend/src/pages/Login.js +++ b/frontend/src/pages/Login.js @@ -5,7 +5,7 @@ import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; import LoadingSpinner from '../components/LoadingSpinner'; const Login = () => { - const [email, setEmail] = useState(''); + const [identifier, setIdentifier] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(false); @@ -19,7 +19,7 @@ const Login = () => { setError(''); // Clear previous errors try { - const result = await login(email, password); + const result = await login(identifier, password); if (result.success) { // Add a small delay to ensure the user sees the success message setTimeout(() => { @@ -66,19 +66,19 @@ const Login = () => {
-
@@ -143,17 +143,6 @@ const Login = () => { )}
- -
-

- Demo Accounts: -

-
-

Super Admin: admin@courseworx.com / admin123

-

Trainer: trainer@courseworx.com / trainer123

-

Trainee: trainee@courseworx.com / trainee123

-
-
diff --git a/frontend/src/pages/Setup.js b/frontend/src/pages/Setup.js new file mode 100644 index 0000000..6387d70 --- /dev/null +++ b/frontend/src/pages/Setup.js @@ -0,0 +1,283 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; +import { authAPI } from '../services/api'; +import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; +import LoadingSpinner from '../components/LoadingSpinner'; +import toast from 'react-hot-toast'; + +const Setup = () => { + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', + phone: '' + }); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const { updateUser } = useAuth(); + const navigate = useNavigate(); + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + + // Validation + if (formData.password !== formData.confirmPassword) { + setError('Passwords do not match'); + setLoading(false); + return; + } + + if (formData.password.length < 6) { + setError('Password must be at least 6 characters long'); + setLoading(false); + return; + } + + if (!formData.phone || formData.phone.trim() === '') { + setError('Phone number is required'); + setLoading(false); + return; + } + + try { + const response = await authAPI.setup({ + firstName: formData.firstName, + lastName: formData.lastName, + email: formData.email, + password: formData.password, + phone: formData.phone || null + }); + + const { token, user } = response.data; + + console.log('Setup successful, user:', user); + console.log('About to update user in auth context...'); + + // Store token and set user + localStorage.setItem('token', token); + + // Update the auth context user state directly + updateUser(user); + + console.log('User updated in auth context, about to redirect...'); + + toast.success('System setup completed successfully!'); + + // Redirect to dashboard + setTimeout(() => { + console.log('Redirecting to dashboard...'); + navigate('/dashboard'); + }, 1000); + + } catch (error) { + console.error('Setup error:', error); + const message = error.response?.data?.error || 'Setup failed'; + setError(message); + toast.error(message); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ + + +
+

+ CourseWorx Setup +

+

+ Create your Super Admin account to get started +

+
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ + {error && ( +
+
+
+ + + +
+
+

+ Setup Error +

+
+ {error} +
+
+
+
+ )} + +
+ +
+
+
+
+ ); +}; + +export default Setup; diff --git a/frontend/src/pages/TraineeLogin.js b/frontend/src/pages/TraineeLogin.js new file mode 100644 index 0000000..44c2762 --- /dev/null +++ b/frontend/src/pages/TraineeLogin.js @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; +import { authAPI } from '../services/api'; +import { + AcademicCapIcon, + EyeIcon, + EyeSlashIcon, + UserIcon, + LockClosedIcon, +} from '@heroicons/react/24/outline'; +import toast from 'react-hot-toast'; + +const TraineeLogin = () => { + const [formData, setFormData] = useState({ + identifier: '', + password: '' + }); + const [showPassword, setShowPassword] = useState(false); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const { login } = useAuth(); + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!formData.identifier || !formData.password) { + toast.error('Please fill in all fields'); + return; + } + + setLoading(true); + try { + const response = await authAPI.traineeLogin(formData); + + if (response.token) { + await login(response.token, response.user); + toast.success('Welcome back!'); + navigate('/dashboard'); + } + } catch (error) { + toast.error(error.response?.data?.error || 'Login failed'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ +
+
+

+ Trainee Login +

+

+ Access your enrolled courses +

+
+ +
+
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+ + Need help? + +
+
+ +
+

+ Contact your trainer or administrator for access +

+
+
+
+
+
+ ); +}; + +export default TraineeLogin; \ No newline at end of file diff --git a/frontend/src/pages/Users.js b/frontend/src/pages/Users.js index d8cea94..dd58a62 100644 --- a/frontend/src/pages/Users.js +++ b/frontend/src/pages/Users.js @@ -436,8 +436,8 @@ const Users = () => {
- - + +
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 3e86aff..f893445 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -2,7 +2,7 @@ import axios from 'axios'; // Create axios instance const api = axios.create({ - baseURL: '/api', + baseURL: 'http://localhost:5000/api', headers: { 'Content-Type': 'application/json', }, @@ -36,13 +36,17 @@ api.interceptors.response.use( // Auth API export const authAPI = { - login: (email, password) => api.post('/auth/login', { email, password }), - getCurrentUser: () => api.get('/auth/me'), - updateProfile: (data) => api.put('/auth/profile', data), - changePassword: (currentPassword, newPassword) => - api.put('/auth/change-password', { currentPassword, newPassword }), - firstPasswordChange: (data) => api.put('/auth/first-password-change', data), + login: (identifier, password) => api.post('/auth/login', { identifier, password }), + traineeLogin: (credentials) => api.post('/auth/trainee-login', credentials), + checkEnrollment: () => api.post('/auth/check-enrollment'), register: (userData) => api.post('/auth/register', userData), + getCurrentUser: () => api.get('/auth/me'), + changePassword: (data) => api.put('/auth/change-password', data), + forgotPassword: (email) => api.post('/auth/forgot-password', { email }), + resetPassword: (token, password) => api.post('/auth/reset-password', { token, password }), + verifyToken: () => api.get('/auth/verify'), + setupStatus: () => api.get('/auth/setup-status'), + setup: (userData) => api.post('/auth/setup', userData) }; // Users API @@ -76,7 +80,7 @@ export const coursesAPI = { delete: (id) => api.delete(`/courses/${id}`), publish: (id, isPublished) => api.put(`/courses/${id}/publish`, { isPublished }), assignTrainer: (id, trainerId) => api.put(`/courses/${id}/assign-trainer`, { trainerId }), - getAvailableTrainers: () => api.get('/courses/trainers/available'), + getAvailableTrainers: () => api.get('/courses/trainers/available').then(res => res.data), getCategories: () => api.get('/courses/categories/all'), getStats: () => api.get('/courses/stats/overview'), uploadCourseImage: (courseName, file) => { @@ -111,15 +115,19 @@ export const courseContentAPI = { // Enrollments API export const enrollmentsAPI = { - getAll: (params) => api.get('/enrollments', { params }), + getAll: (params) => api.get('/enrollments', { params }).then(res => res.data), + getMy: (params) => api.get('/enrollments/my', { params }).then(res => res.data), + getById: (id) => api.get(`/enrollments/${id}`).then(res => res.data), create: (data) => api.post('/enrollments', data), - getMy: (params) => api.get('/enrollments/my', { params }), - getById: (id) => api.get(`/enrollments/${id}`), - updateStatus: (id, data) => api.put(`/enrollments/${id}/status`, data), - updatePayment: (id, data) => api.put(`/enrollments/${id}/payment`, data), - updateProgress: (id, progress) => api.put(`/enrollments/${id}/progress`, { progress }), - cancel: (id) => api.delete(`/enrollments/${id}`), + update: (id, data) => api.put(`/enrollments/${id}`, data), + delete: (id) => api.delete(`/enrollments/${id}`), + updateStatus: (id, status, notes) => api.put(`/enrollments/${id}/status`, { status, notes }), getStats: () => api.get('/enrollments/stats/overview'), + // New enrollment management endpoints + bulkEnroll: (data) => api.post('/enrollments/bulk', data), + assignTrainee: (data) => api.post('/enrollments/assign', data), + getCourseTrainees: (courseId, params) => api.get(`/enrollments/course/${courseId}/trainees`, { params }).then(res => res.data), + getAvailableTrainees: (params) => api.get('/enrollments/available-trainees', { params }).then(res => res.data) }; // Attendance API diff --git a/start-courseworx.bat b/start-courseworx.bat index 2d07ef4..dfab70f 100644 --- a/start-courseworx.bat +++ b/start-courseworx.bat @@ -46,6 +46,37 @@ if not exist "frontend\node_modules" ( ) echo. + +REM Check for port conflicts +echo ๐Ÿ” Checking for port conflicts... + +netstat -an | findstr ":5000" >nul 2>&1 +if %errorlevel% equ 0 ( + echo โš ๏ธ Port 5000 is already in use + echo This might be another CourseWorx instance or different application + set /p choice="Do you want to continue anyway? (y/N): " + if /i not "%choice%"=="y" ( + echo Stopping startup process... + pause + exit /b 1 + ) +) + +netstat -an | findstr ":3000" >nul 2>&1 +if %errorlevel% equ 0 ( + echo โš ๏ธ Port 3000 is already in use + echo This might be another CourseWorx instance or different application + set /p choice="Do you want to continue anyway? (y/N): " + if /i not "%choice%"=="y" ( + echo Stopping startup process... + pause + exit /b 1 + ) +) + +echo โœ… Port check completed +echo. + echo ๐Ÿš€ Starting CourseWorx... echo. echo ๐Ÿ“ฑ Frontend will be available at: http://localhost:3000 @@ -56,5 +87,11 @@ echo. REM Start both frontend and backend npm run start +if %errorlevel% neq 0 ( + echo โŒ Error starting CourseWorx + echo Please check the error messages above and try again + pause + exit /b 1 +) pause \ No newline at end of file diff --git a/start-courseworx.ps1 b/start-courseworx.ps1 index 0c4ba67..2d4f703 100644 --- a/start-courseworx.ps1 +++ b/start-courseworx.ps1 @@ -1,61 +1,47 @@ # CourseWorx Start Script -Write-Host "" -Write-Host "========================================" -ForegroundColor Cyan -Write-Host " CourseWorx - Starting Application" -ForegroundColor Cyan -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "" +Write-Host "Starting CourseWorx..." # Check if Node.js is installed -try { - $nodeVersion = node --version - Write-Host "โœ… Node.js version: $nodeVersion" -ForegroundColor Green -} catch { - Write-Host "โŒ ERROR: Node.js is not installed or not in PATH" -ForegroundColor Red - Write-Host "Please install Node.js from https://nodejs.org/" -ForegroundColor Yellow +$nodeVersion = node --version +if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Node.js is not installed or not in PATH" -ForegroundColor Red Read-Host "Press Enter to exit" exit 1 } +Write-Host "Node.js version: $nodeVersion" -ForegroundColor Green # Check if npm is installed -try { - $npmVersion = npm --version - Write-Host "โœ… npm version: $npmVersion" -ForegroundColor Green -} catch { - Write-Host "โŒ ERROR: npm is not installed or not in PATH" -ForegroundColor Red +$npmVersion = npm --version +if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: npm is not installed or not in PATH" -ForegroundColor Red Read-Host "Press Enter to exit" exit 1 } - -Write-Host "" +Write-Host "npm version: $npmVersion" -ForegroundColor Green # Check if dependencies are installed if (-not (Test-Path "node_modules")) { - Write-Host "๐Ÿ“ฆ Installing root dependencies..." -ForegroundColor Yellow + Write-Host "Installing root dependencies..." -ForegroundColor Yellow npm install } if (-not (Test-Path "backend\node_modules")) { - Write-Host "๐Ÿ“ฆ Installing backend dependencies..." -ForegroundColor Yellow + Write-Host "Installing backend dependencies..." -ForegroundColor Yellow Set-Location backend npm install Set-Location .. } if (-not (Test-Path "frontend\node_modules")) { - Write-Host "๐Ÿ“ฆ Installing frontend dependencies..." -ForegroundColor Yellow + Write-Host "Installing frontend dependencies..." -ForegroundColor Yellow Set-Location frontend npm install Set-Location .. } -Write-Host "" -Write-Host "๐Ÿš€ Starting CourseWorx..." -ForegroundColor Green -Write-Host "" -Write-Host "๐Ÿ“ฑ Frontend will be available at: http://localhost:3000" -ForegroundColor Cyan -Write-Host "๐Ÿ”ง Backend API will be available at: http://localhost:5000" -ForegroundColor Cyan -Write-Host "" -Write-Host "๐Ÿ’ก To stop the application, press Ctrl+C" -ForegroundColor Yellow -Write-Host "" +Write-Host "Starting CourseWorx..." -ForegroundColor Green +Write-Host "Frontend: http://localhost:3000" -ForegroundColor Cyan +Write-Host "Backend: http://localhost:5000" -ForegroundColor Cyan # Start both frontend and backend -npm run start \ No newline at end of file +npm run start diff --git a/tat -an ๏ผ findstr ๏€บ5000 b/tat -an ๏ผ findstr ๏€บ5000 new file mode 100644 index 0000000..6d05160 --- /dev/null +++ b/tat -an ๏ผ findstr ๏€บ5000 @@ -0,0 +1,10 @@ +d7075fd (HEAD -> main) Fix dashboard data access issue - Fixed data access path to use .data wrapper - Updated all dashboard cards to use correct data structure - Updated debug section to show correct data access - Fixed trainer and trainee dashboard data access - The issue was that API response has extra 'data' wrapper +a4aba8e Add comprehensive debugging for dashboard data display - Added detailed console logging for data access - Enhanced debug section with direct value testing - Added test card to verify raw data values - Added loading state debugging - Disabled React Query caching to force fresh data +d0ee0c1 Add debugging to dashboard cards and fix data display - Added console logging to see exact data being passed to dashboard cards - Enhanced debug section to show data types and values - Added specific debugging for SuperAdmin dashboard rendering +ceb1f56 Fix header layout and add debugging for dashboard statistics - Removed 'CourseWorx' text from header - Fixed logo path to use /images/cx-logo.png - Moved navigation items next to Home icon - Added debugging to API calls and dashboard - Added console logging to backend stats endpoints +cd5370f (origin/main, origin/HEAD) v1.1.4: Modern header navigation & real-time dashboard statistics - Removed sidebar completely and moved navigation to header - Added CourseWorx logo and Home icon for dashboard access - Moved Courses and Users links to header with icons - Updated all dashboard cards to show real database counts - Enhanced backend API endpoints for role-specific statistics - Fixed ESLint warnings in CourseContent.js and CourseContentViewer.js - Improved responsive design and user experience +6cad9b6 v1.1.3: Enhanced login experience, trainer profiles, modern course details, and dashboard improvements +dece9c8 v1.1.2: Add trainer assignment system and navigation improvements +15281c5 (tag: v1.1.1) Version 1.1.1 - Enhanced Course Content Management Interface +b4d90c6 (tag: v1.1.0) Release v1.1.0 - Course Content & Enrollment Management +cca9032 (tag: v1.0.0) Release v1.0.0 - Complete Course Management System diff --git a/version.txt b/version.txt index 60a7fa2..b924b02 100644 --- a/version.txt +++ b/version.txt @@ -1,635 +1,57 @@ -CourseWorx v1.1.4 - Modern Header Navigation & Real-Time Dashboard -========================================================== +CourseWorx v1.2.0 -This version modernizes the navigation system by removing the sidebar and implementing a clean header-based navigation with real-time dashboard statistics. +CHANGELOG: -MAJOR FEATURES IMPLEMENTED: -=========================== +v1.2.0 (2025-08-20) +=================== -1. MODERN HEADER NAVIGATION ----------------------------- -- Completely removed sidebar (both mobile and desktop versions) -- Moved all navigation to a clean, sticky header -- Added CourseWorx logo prominently in the header -- Implemented Home icon next to logo for dashboard navigation -- Moved Courses and Users links to header with icons -- Removed Dashboard link from navigation (accessible via logo/Home icon) -- Responsive design with icons-only on mobile, icons+text on desktop -- Future-ready design for icon-only navigation +FEATURES & IMPROVEMENTS: +- โœจ Implemented responsive dropdown menu for course action buttons +- ๐Ÿ”ง Fixed trainer assignment dropdown population issue +- ๐Ÿ› ๏ธ Resolved available trainees API routing conflict +- ๐Ÿ“ฑ Enhanced mobile responsiveness across the application +- โ™ฟ Improved accessibility with ARIA labels and keyboard navigation -2. REAL-TIME DASHBOARD STATISTICS ----------------------------------- -- Updated all dashboard cards to show real database counts -- Enhanced backend API endpoints for role-specific statistics -- Super Admin dashboard shows total users, trainers, trainees, courses -- Trainer dashboard shows my courses, published courses, my students -- Trainee dashboard shows enrolled courses, attendance rate, completed courses -- Added new API queries for enrollment and course statistics -- All cards now display actual data instead of hardcoded values - -3. BACKEND API ENHANCEMENTS ----------------------------- -- Enhanced `/api/enrollments/stats/overview` endpoint: - - Added `myStudents` count for trainers (unique students) - - Added `myEnrollments` count for trainees - - Added `completedCourses` count for trainees -- Enhanced `/api/courses/stats/overview` endpoint: - - Added `myCourses` count for trainers - - Added `myPublishedCourses` count for trainers -- Role-based data filtering and access control -- Improved statistics accuracy and performance - -4. FRONTEND IMPROVEMENTS -------------------------- -- Clean, modern header layout with proper spacing -- User dropdown menu with profile and logout options -- Responsive navigation that adapts to screen size -- Enhanced visual hierarchy and user experience -- Improved accessibility with proper ARIA attributes -- Better mobile experience with touch-friendly navigation - -5. CODE QUALITY & MAINTENANCE ------------------------------ -- Fixed ESLint warnings in CourseContent.js and CourseContentViewer.js -- Removed unused imports and variables -- Cleaned up duplicate imports (PlusIcon) -- Improved code organization and structure -- Enhanced maintainability and readability +BUG FIXES: +- ๐Ÿ› Fixed trainer assignment dropdown showing "No available trainers" +- ๐Ÿ› Resolved 500 Internal Server Error in available-trainees endpoint +- ๐Ÿ› Fixed route ordering issue where /:id was catching /available-trainees +- ๐Ÿ› Corrected API response structure for getAvailableTrainers function +- ๐Ÿ› Fixed setup page redirect not working after Super Admin creation +- ๐Ÿ› Resolved ESLint warnings in AuthContext and Setup components TECHNICAL IMPROVEMENTS: -======================= - -1. LAYOUT SYSTEM ------------------ -- Removed sidebar-based layout completely -- Implemented header-centric navigation -- Responsive design with mobile-first approach -- Clean separation of navigation and content areas -- Improved content area utilization - -2. API INTEGRATION -------------------- -- Enhanced statistics endpoints with role-specific data -- Improved data fetching efficiency -- Better error handling and loading states -- Real-time data updates with React Query -- Optimized API calls for dashboard performance - -3. USER EXPERIENCE -------------------- -- Streamlined navigation with fewer clicks -- Better visual feedback and hover effects -- Improved accessibility and keyboard navigation -- Cleaner, more professional appearance -- Faster access to key features - -4. PERFORMANCE OPTIMIZATIONS ------------------------------ -- Reduced layout complexity by removing sidebar -- Optimized API calls for dashboard statistics -- Improved rendering performance -- Better caching strategies for statistics data -- Enhanced mobile performance - -BUG FIXES & RESOLUTIONS: -======================== - -1. ESLINT WARNINGS -------------------- -- Fixed unused `useAuth` import in CourseContentViewer.js -- Fixed unused `queryClient` variable in CourseContentViewer.js -- Removed duplicate `PlusIcon` import in CourseContent.js -- Updated icon references for consistency -- Cleaned up unused variables and imports - -2. NAVIGATION ISSUES ---------------------- -- Resolved sidebar navigation complexity -- Fixed mobile navigation accessibility -- Improved navigation state management -- Enhanced user menu dropdown functionality -- Better responsive behavior - -3. DASHBOARD ACCURACY ----------------------- -- Fixed hardcoded values in dashboard cards -- Implemented real database counts for all statistics -- Enhanced role-based data filtering -- Improved data accuracy and reliability -- Better error handling for statistics - -DEPENDENCIES & TECHNOLOGIES: -============================ - -Frontend: -- React 18.x -- React Router v6 -- React Query (TanStack Query) -- Tailwind CSS -- Heroicons -- Axios -- React Hot Toast - -Backend: -- Node.js -- Express.js -- Sequelize ORM -- PostgreSQL -- JWT (jsonwebtoken) -- bcryptjs -- multer -- express-validator - -FILE STRUCTURE CHANGES: -====================== - -Frontend: -- Updated Layout.js with header-based navigation -- Enhanced Dashboard.js with real-time statistics -- Fixed ESLint issues in CourseContent.js and CourseContentViewer.js -- Improved component organization and structure - -Backend: -- Enhanced enrollments.js with role-specific statistics -- Updated courses.js with trainer-specific data -- Improved API response structure and accuracy - -CONFIGURATION UPDATES: -====================== - -Navigation: -- Removed sidebar configuration -- Updated header navigation structure -- Enhanced responsive breakpoints -- Improved mobile navigation - -API Endpoints: -- Enhanced statistics endpoints with role-based filtering -- Improved data accuracy and performance -- Better error handling and validation - -SECURITY CONSIDERATIONS: -======================== - -- Maintained role-based access control -- Enhanced API endpoint security -- Improved data filtering and validation -- Better user session management -- Enhanced authentication flow - -DEPLOYMENT READINESS: -===================== - -- Updated navigation system for production -- Enhanced dashboard performance -- Improved mobile responsiveness -- Better user experience across devices -- Optimized API performance - -This version (1.1.4) modernizes the CourseWorx platform with a clean, header-based navigation system and real-time dashboard statistics, providing a more professional and user-friendly experience while maintaining all existing functionality. - -Release Date: [Current Date] -Version: 1.1.4 -Status: Production Ready - -========================================================== - -CourseWorx v1.1.3 - Enhanced Login Experience & Dashboard Improvements -========================================================== - -This version adds comprehensive course content management and enrollment/subscriber functionality to the existing Course Management System. - -MAJOR FEATURES IMPLEMENTED: -=========================== - -1. AUTHENTICATION & AUTHORIZATION --------------------------------- -- JWT-based authentication system -- Role-based access control (Super Admin, Trainer, Trainee) -- Protected routes with role-based permissions -- User session management with localStorage -- Password hashing with bcryptjs -- Login/logout functionality with proper error handling - -2. USER MANAGEMENT SYSTEM -------------------------- -- Complete CRUD operations for users -- User roles: Super Admin, Trainer, Trainee -- User status management (Active/Inactive) -- User search and filtering capabilities -- Pagination for user lists -- CSV import functionality for bulk user creation -- Super Admin password change functionality for any user -- User profile management - -3. COURSE MANAGEMENT SYSTEM ---------------------------- -- Complete CRUD operations for courses -- Course publishing/unpublishing functionality -- Course categories and metadata -- Course image upload functionality -- Course search and filtering -- Course statistics and analytics - -4. COURSE CONTENT MANAGEMENT SYSTEM ------------------------------------ -- Multi-type content support (Documents, Images, Videos, Articles, Quizzes, Certificates) -- File upload system with 100MB limit and type validation -- Content ordering and publishing control -- Quiz system with multiple question types (multiple choice, single choice, true/false, text, file upload) -- Points system for gamification -- Required/optional content marking -- Metadata storage for additional information -- File management with automatic cleanup - -5. ENROLLMENT & SUBSCRIBER MANAGEMENT --------------------------------------- -- Complete enrollment lifecycle (enroll, unenroll, track progress) -- Status tracking (pending, active, completed, cancelled) -- Payment management (pending, paid, failed, refunded) -- Progress tracking (0-100%) with automatic updates -- Course capacity limits and validation -- Certificate issuance tracking -- Enrollment analytics and statistics -- Role-based enrollment access control - -6. FRONTEND USER INTERFACE ---------------------------- -- Modern, responsive UI using Tailwind CSS -- Heroicons for consistent iconography -- Modal system for user add/edit operations -- Loading states and error handling -- Toast notifications for user feedback -- Pagination components -- Search and filter interfaces -- Role-based UI elements - -7. INTERNATIONALIZATION (i18n) -------------------------------- -- English (LTR) and Arabic (RTL) language support -- Dynamic language switching -- Document direction switching (LTR/RTL) -- Translation system using react-i18next -- Localized text for all user-facing content - -8. FILE UPLOAD SYSTEM ----------------------- -- Image upload for course thumbnails -- Slider image upload for homepage -- Multi-type content file uploads (documents, images, videos) -- Multer middleware for file handling -- File validation and size limits (100MB max) -- Organized file storage structure -- Automatic directory creation -- File type validation for different content types - -9. BACKEND API SYSTEM ----------------------- -- RESTful API design -- Express.js server with middleware -- Sequelize ORM with PostgreSQL -- Input validation using express-validator -- Error handling and logging -- CORS configuration -- Environment-based configuration -- Course content management APIs -- Enrollment management APIs - -10. DATABASE SYSTEM -------------------- -- PostgreSQL database with Sequelize ORM -- User model with proper relationships -- Course model with metadata -- CourseContent model with multi-type support -- QuizQuestion model for quiz functionality -- Enhanced Enrollment model with comprehensive tracking -- Attendance tracking -- Assignment management -- Database migrations and seeding - -NEW FEATURES IN v1.1.3: -======================== - -1. ENHANCED LOGIN EXPERIENCE ------------------------------ -- Fixed page reload issue that was hiding error messages -- Improved error message visibility with better styling -- Added delays to prevent immediate redirects after login -- Enhanced error handling with clear visual feedback -- Better user experience with proper error persistence -- Added error icons and improved error message styling - -2. TRAINER PROFILE SYSTEM --------------------------- -- New trainer profile page with comprehensive information -- Clickable instructor sections in course details -- Trainer qualifications and experience display -- Course listings by trainer -- Professional trainer profile layout -- Direct navigation from course pages to trainer profiles - -3. MODERN COURSE DETAIL PAGE ------------------------------ -- Redesigned course detail page with Udemy-like layout -- Left column with course information and content -- Right sidebar with pricing and enrollment options -- Professional course preview with play button -- Enhanced "What you'll learn" section with checkmarks -- Course content structure with expandable sections -- Requirements and description sections -- Course includes section with feature icons - -4. DASHBOARD IMPROVEMENTS --------------------------- -- Made all dashboard cards clickable with proper navigation -- Added hover effects and arrow icons for better UX -- Fixed hardcoded values and improved data display -- Enhanced Quick Actions with proper links -- Course and enrollment items are now clickable -- Better visual feedback with transitions and hover effects - -5. ESLINT & CODE QUALITY -------------------------- -- Fixed all ESLint warnings across components -- Removed unused imports and variables -- Improved code organization and structure -- Enhanced code maintainability -- Cleaned up debugging code and console logs - -NEW FEATURES IN v1.1.2: -======================== - -1. TRAINER ASSIGNMENT SYSTEM ------------------------------ -- Super Admin can assign trainers to courses -- Trainer assignment modal with dropdown selection -- Available trainers API endpoint with proper authentication -- Real-time trainer assignment with immediate UI updates -- Role-based access control (Super Admin only) -- Comprehensive error handling and validation -- Debug information for troubleshooting - -2. NAVIGATION REORGANIZATION ------------------------------ -- Moved logout and profile links to top-right user dropdown -- Cleaned up sidebar navigation (Dashboard, Courses, Users only) -- Modern user dropdown menu with avatar and role display -- Click-outside-to-close functionality for dropdown -- Responsive design for mobile and desktop -- Improved user experience with better navigation hierarchy - -3. ENHANCED USER INTERFACE ---------------------------- -- User avatar with initials in top-right corner -- Dropdown menu with Profile and Logout options -- Clean sidebar with only essential navigation items -- Better visual hierarchy and spacing -- Improved accessibility with proper ARIA attributes -- Mobile-responsive dropdown menu - -4. DEBUGGING & TROUBLESHOOTING -------------------------------- -- Added comprehensive debug information for trainer assignment -- Backend logging for trainer API requests -- Frontend error handling and user feedback -- Authentication and authorization debugging -- API call monitoring and error tracking - -NEW FEATURES IN v1.1.1: -======================== - -1. DEDICATED COURSE CONTENT MANAGEMENT PAGE -------------------------------------------- -- Separated content management from course editing -- Dedicated `/courses/:id/content` route for content management -- Comprehensive content CRUD operations (Create, Read, Update, Delete) -- Enhanced user interface with prominent action buttons -- Content type icons and visual indicators -- Content status management (published/unpublished, required/optional) - -2. ENHANCED USER INTERFACE ---------------------------- -- Prominent "Manage Content" button on course detail pages -- Improved visual hierarchy and button styling -- Better content organization and display -- Enhanced modal interfaces for content creation/editing -- Responsive design improvements -- Loading states and error handling - -3. CONTENT MANAGEMENT WORKFLOW ------------------------------- -- Add content with type-specific forms (Documents, Images, Videos, Articles, Quizzes, Certificates) -- Edit existing content with pre-populated forms -- Delete content with confirmation dialogs -- File upload with type validation and progress tracking -- Content ordering and points system -- Publishing controls and status indicators - -4. NAVIGATION IMPROVEMENTS ---------------------------- -- Direct access to content management from course detail pages -- Back navigation to course detail page -- Role-based visibility for content management features -- Clean separation between course editing and content management - -NEW FEATURES IN v1.1.0: -======================== - -1. COURSE CONTENT MANAGEMENT ----------------------------- -- Multi-type content system (Documents, Images, Videos, Articles, Quizzes, Certificates) -- File upload with type validation and size limits -- Content ordering and publishing control -- Quiz system with multiple question types -- Points system for gamification -- Required/optional content marking -- Metadata storage for additional information - -2. ENROLLMENT & SUBSCRIBER SYSTEM ---------------------------------- -- Complete enrollment lifecycle management -- Status and payment tracking -- Progress monitoring (0-100%) -- Course capacity validation -- Certificate issuance tracking -- Enrollment analytics and statistics -- Role-based access control - -3. ENHANCED API SYSTEM ------------------------ -- Course content management endpoints -- Enrollment management endpoints -- File upload with validation -- Quiz question management -- Progress tracking APIs -- Statistics and analytics endpoints - -TECHNICAL IMPROVEMENTS: -======================= - -1. ROUTING & NAVIGATION ------------------------- -- Fixed homepage accessibility issues -- Proper route protection and redirection -- Nested routing with React Router v6 -- Public and private route separation -- Role-based route access - -2. API INTEGRATION -------------------- -- Axios-based API client with interceptors -- React Query for server state management -- Optimistic updates and caching -- Error handling and retry logic -- Request/response interceptors -- Course content API integration -- Enrollment management API integration - -3. SECURITY ENHANCEMENTS -------------------------- -- JWT token management -- Password hashing and validation -- Role-based access control -- Input sanitization and validation -- CSRF protection considerations - -4. PERFORMANCE OPTIMIZATIONS ------------------------------ -- React Query for efficient data fetching -- Pagination for large datasets -- Image optimization and compression -- Lazy loading considerations -- Caching strategies -- File upload optimization -- Content streaming for large files - -BUG FIXES & RESOLUTIONS: -======================== - -1. CRITICAL FIXES ------------------- -- Fixed homepage routing issue (shadowed routes) -- Resolved user creation API endpoint mismatch -- Fixed user data rendering issues (nested data structure) -- Corrected API base URL configuration -- Resolved modal rendering issues - -2. ESLINT & CODE QUALITY -------------------------- -- Removed unused variables and imports -- Fixed accessibility warnings for anchor tags -- Resolved React child rendering issues -- Cleaned up console logs and debugging code -- Improved code organization and structure - -3. USER EXPERIENCE FIXES -------------------------- -- Fixed non-functional Add/Edit/Delete buttons -- Resolved CSV import BOM issues -- Improved error message display -- Enhanced loading states and feedback -- Fixed modal accessibility issues - -4. BACKEND STABILITY ---------------------- -- Fixed user registration role validation -- Resolved password hashing issues -- Improved error handling and logging -- Fixed database connection issues -- Enhanced API response consistency -- Added comprehensive content validation -- Enhanced file upload error handling - -DEPENDENCIES & TECHNOLOGIES: -============================ - -Frontend: -- React 18.x -- React Router v6 -- React Query (TanStack Query) -- Tailwind CSS -- Heroicons -- Axios -- React Hot Toast -- i18next & react-i18next - -Backend: -- Node.js -- Express.js -- Sequelize ORM -- PostgreSQL -- JWT (jsonwebtoken) -- bcryptjs -- multer (enhanced for multi-type uploads) -- express-validator -- csv-parser - -Development Tools: -- ESLint -- Prettier -- Nodemon -- Concurrently - -FILE STRUCTURE: -============== - -Frontend: -- src/components/ (Reusable UI components) -- src/contexts/ (React Context providers) -- src/pages/ (Page components) -- src/services/ (API services) -- src/i18n/ (Internationalization) - -Backend: -- routes/ (API route handlers) -- models/ (Database models) -- middleware/ (Custom middleware) -- config/ (Configuration files) -- uploads/ (File uploads) -- course-content/ (Course content management) -- enrollments/ (Enrollment management) - -CONFIGURATION: -============== - -Environment Variables: -- Database connection -- JWT secrets -- File upload paths -- API endpoints -- Development/production settings - -Database Schema: -- Users table with role-based access -- Courses table with metadata -- CourseContent table with multi-type support -- QuizQuestion table for quiz functionality -- Enhanced Enrollments table with comprehensive tracking -- Attendance and assignment tables - -SECURITY CONSIDERATIONS: -======================== - -- JWT token expiration and refresh -- Password complexity requirements -- Role-based access control -- Input validation and sanitization -- File upload security -- CORS configuration -- Environment variable protection - -DEPLOYMENT READINESS: -===================== - -- Environment-based configuration -- Database migration scripts -- File upload directory structure -- Error logging and monitoring -- Performance optimization -- Security hardening - -This version (1.1.0) adds comprehensive course content management and enrollment/subscriber functionality to the existing Course Management System, providing a complete foundation for creating rich educational content and managing student enrollments. - -Release Date: [Current Date] -Version: 1.1.3 -Status: Production Ready \ No newline at end of file +- ๐Ÿ”„ Reordered Express.js routes to prevent conflicts +- ๐Ÿ“Š Added comprehensive logging for debugging trainer assignment +- ๐ŸŽฏ Improved API response handling in frontend services +- ๐Ÿš€ Enhanced user experience with smooth dropdown animations +- ๐ŸŽจ Implemented consistent hover effects and transitions + +RESPONSIVE DESIGN: +- ๐Ÿ“ฑ Converted horizontal action buttons to compact 3-dots dropdown +- ๐ŸŽจ Added professional dropdown styling with shadows and rings +- ๐Ÿ”„ Implemented click-outside functionality for dropdown menus +- โŒจ๏ธ Added keyboard navigation support (Enter/Space keys) +- ๐ŸŽฏ Optimized positioning for all screen sizes + +CODE QUALITY: +- ๐Ÿงน Fixed React Hook dependency warnings +- ๐Ÿšซ Removed unused variables and imports +- ๐Ÿ“ Added comprehensive code documentation +- ๐ŸŽฏ Improved error handling and user feedback +- ๐Ÿ” Enhanced debugging capabilities + +PREVIOUS VERSIONS: +================== + +v1.1.0 (2025-08-20) +- Initial CourseWorx application setup +- User authentication and role management +- Course management system +- Basic enrollment functionality +- File upload capabilities + +v1.0.0 (2025-08-20) +- Project initialization +- Basic project structure +- Development environment setup \ No newline at end of file