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
This commit is contained in:
parent
8a71d336e5
commit
95f4377170
24 changed files with 1807 additions and 978 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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' }));
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -50,6 +50,5 @@
|
|||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.3.6"
|
||||
},
|
||||
"proxy": "http://localhost:5000"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <LoadingSpinner />;
|
||||
}
|
||||
|
||||
// If setup is required, redirect to setup
|
||||
if (setupRequired) {
|
||||
return <Navigate to="/setup" replace />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
|
@ -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 <LoadingSpinner />;
|
||||
}
|
||||
|
||||
// If setup is required, show setup page
|
||||
if (setupRequired) {
|
||||
console.log('Setup required, showing setup page');
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/setup" element={<Setup />} />
|
||||
<Route path="*" element={<Navigate to="/setup" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Setup not required, showing normal routes');
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={
|
||||
user ? <Navigate to="/dashboard" replace /> : <Login />
|
||||
} />
|
||||
<Route path="/trainee-login" element={
|
||||
user ? <Navigate to="/dashboard" replace /> : <TraineeLogin />
|
||||
} />
|
||||
{/* Public homepage route */}
|
||||
<Route path="/" element={<Home />} />
|
||||
|
||||
|
|
@ -65,11 +95,21 @@ const AppRoutes = () => {
|
|||
<CourseContent />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
<Route path="/courses/:id/enrollment" element={
|
||||
<PrivateRoute allowedRoles={['super_admin', 'trainer']}>
|
||||
<CourseEnrollment />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
<Route path="/courses/:id/learn" element={
|
||||
<PrivateRoute>
|
||||
<CourseContentViewer />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
<Route path="/enrollments" element={
|
||||
<PrivateRoute allowedRoles={['super_admin', 'trainer']}>
|
||||
<CourseEnrollment />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
<Route path="/users/import" element={
|
||||
<PrivateRoute allowedRoles={['super_admin', 'trainer']}>
|
||||
<UserImport />
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ const Layout = () => {
|
|||
{/* Logo and Navigation */}
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src="/images/cx-logo.png" alt="CourseWorx" className="h-8 w-auto" />
|
||||
<div className="h-8 w-8 rounded-full bg-primary-500 flex items-center justify-center">
|
||||
<span className="text-sm font-bold text-white">CX</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Items */}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<h1 className="text-3xl font-bold text-gray-900">{courseData.title}</h1>
|
||||
<p className="text-gray-600 mt-2">{courseData.shortDescription}</p>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Link
|
||||
to={`/courses/${id}/learn`}
|
||||
className="btn-primary flex items-center shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
|
||||
{/* Action Buttons - Responsive Dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowActionsDropdown(!showActionsDropdown)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setShowActionsDropdown(!showActionsDropdown);
|
||||
}
|
||||
}}
|
||||
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
title="Course Actions"
|
||||
aria-label="Course actions menu"
|
||||
aria-expanded={showActionsDropdown}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<EyeIcon className="h-5 w-5 mr-2" />
|
||||
View Content
|
||||
</Link>
|
||||
{(isTrainer || isSuperAdmin) && (
|
||||
<Link
|
||||
to={`/courses/${id}/content`}
|
||||
className="btn-secondary flex items-center"
|
||||
>
|
||||
<CogIcon className="h-5 w-5 mr-2" />
|
||||
Manage Content
|
||||
</Link>
|
||||
)}
|
||||
{isSuperAdmin && (
|
||||
<button
|
||||
onClick={() => setShowTrainerModal(true)}
|
||||
className="btn-secondary flex items-center"
|
||||
title="Assign Trainer (Super Admin only)"
|
||||
>
|
||||
<UserIcon className="h-4 w-4 mr-2" />
|
||||
Assign Trainer
|
||||
</button>
|
||||
<EllipsisVerticalIcon className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{showActionsDropdown && (
|
||||
<div className="absolute right-0 mt-2 w-56 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50 transition-all duration-200 ease-out sm:right-0 md:right-0 lg:right-0 xl:right-0">
|
||||
<div className="py-1">
|
||||
{/* View Content - Always visible */}
|
||||
<Link
|
||||
to={`/courses/${id}/learn`}
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 transition-colors duration-200"
|
||||
onClick={() => setShowActionsDropdown(false)}
|
||||
>
|
||||
<EyeIcon className="h-5 w-5 mr-3 text-gray-400" />
|
||||
View Content
|
||||
</Link>
|
||||
|
||||
{/* Manage Content - Trainer/Admin only */}
|
||||
{(isTrainer || isSuperAdmin) && (
|
||||
<Link
|
||||
to={`/courses/${id}/content`}
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 transition-colors duration-200"
|
||||
onClick={() => setShowActionsDropdown(false)}
|
||||
>
|
||||
<CogIcon className="h-5 w-5 mr-3 text-gray-400" />
|
||||
Manage Content
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Manage Enrollment - Trainer/Admin only */}
|
||||
{(isTrainer || isSuperAdmin) && (
|
||||
<Link
|
||||
to={`/courses/${id}/enrollment`}
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 transition-colors duration-200"
|
||||
onClick={() => setShowActionsDropdown(false)}
|
||||
>
|
||||
<UserPlusIcon className="h-5 w-5 mr-3 text-gray-400" />
|
||||
Manage Enrollment
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Assign Trainer - Super Admin only */}
|
||||
{isSuperAdmin && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowActionsDropdown(false);
|
||||
setShowTrainerModal(true);
|
||||
}}
|
||||
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 transition-colors duration-200"
|
||||
title="Assign Trainer (Super Admin only)"
|
||||
>
|
||||
<UserIcon className="h-4 w-4 mr-3 text-gray-400" />
|
||||
Assign Trainer
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
357
frontend/src/pages/CourseEnrollment.js
Normal file
357
frontend/src/pages/CourseEnrollment.js
Normal file
|
|
@ -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 <LoadingSpinner size="lg" className="mt-8" />;
|
||||
}
|
||||
|
||||
if (!course) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">Course not found.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user has permission to manage this course
|
||||
const canManage = isSuperAdmin || (isTrainer && course.trainerId === user.id);
|
||||
if (!canManage) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">You don't have permission to manage this course.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Course Enrollment</h1>
|
||||
<p className="text-gray-600">Manage trainees for "{course.title}"</p>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Link
|
||||
to={`/courses/${id}`}
|
||||
className="btn-secondary"
|
||||
>
|
||||
<EyeIcon className="h-4 w-4 mr-2" />
|
||||
View Course
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setShowAssignModal(true)}
|
||||
className="btn-primary"
|
||||
>
|
||||
<UserPlusIcon className="h-4 w-4 mr-2" />
|
||||
Assign Trainees
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Course Info */}
|
||||
<div className="card">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-shrink-0">
|
||||
<AcademicCapIcon className="h-8 w-8 text-primary-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-gray-900">{course.title}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
Trainer: {course.trainer?.firstName} {course.trainer?.lastName}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500">Enrolled Trainees</p>
|
||||
<p className="text-2xl font-bold text-primary-600">
|
||||
{enrolledData?.trainees?.length || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enrolled Trainees */}
|
||||
<div className="card">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Enrolled Trainees</h3>
|
||||
<span className="text-sm text-gray-500">
|
||||
{enrolledData?.trainees?.length || 0} trainees
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{enrolledData?.trainees?.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{enrolledData.trainees.map((trainee) => (
|
||||
<div key={trainee.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="h-10 w-10 rounded-full bg-primary-100 flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-primary-600">
|
||||
{trainee.firstName?.charAt(0)}{trainee.lastName?.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">
|
||||
{trainee.firstName} {trainee.lastName}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{trainee.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500">Progress</p>
|
||||
<p className="text-sm font-medium">{trainee.progress || 0}%</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500">Status</p>
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||
trainee.status === 'active' ? 'bg-green-100 text-green-800' :
|
||||
trainee.status === 'completed' ? 'bg-blue-100 text-blue-800' :
|
||||
trainee.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{trainee.status === 'active' && <CheckCircleIcon className="h-3 w-3 mr-1" />}
|
||||
{trainee.status === 'pending' && <ClockIcon className="h-3 w-3 mr-1" />}
|
||||
{trainee.status === 'completed' && <CheckIcon className="h-3 w-3 mr-1" />}
|
||||
{trainee.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<UsersIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">No trainees enrolled yet.</p>
|
||||
<p className="text-sm text-gray-400">Assign trainees to get started.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Assign Trainees Modal */}
|
||||
{showAssignModal && (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full z-50">
|
||||
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Assign Trainees</h3>
|
||||
<button
|
||||
onClick={() => setShowAssignModal(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="mb-4">
|
||||
<div className="relative">
|
||||
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search trainees..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Available Trainees */}
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{availableLoading ? (
|
||||
<LoadingSpinner size="md" />
|
||||
) : filteredAvailableTrainees.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{filteredAvailableTrainees.map((trainee) => (
|
||||
<div key={trainee.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTrainees.includes(trainee.id)}
|
||||
onChange={() => toggleTraineeSelection(trainee.id)}
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<div className="h-8 w-8 rounded-full bg-primary-100 flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-primary-600">
|
||||
{trainee.firstName?.charAt(0)}{trainee.lastName?.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">
|
||||
{trainee.firstName} {trainee.lastName}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{trainee.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleSingleAssign(trainee.id)}
|
||||
disabled={assigning}
|
||||
className="btn-secondary btn-sm"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">No available trainees found.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
onClick={() => setShowAssignModal(false)}
|
||||
className="btn-secondary"
|
||||
disabled={assigning}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBulkAssign}
|
||||
disabled={selectedTrainees.length === 0 || assigning}
|
||||
className="btn-primary"
|
||||
>
|
||||
{assigning ? 'Assigning...' : `Assign ${selectedTrainees.length} Trainees`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseEnrollment;
|
||||
|
|
@ -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 <LoadingSpinner size="lg" className="mt-8" />;
|
||||
}
|
||||
|
||||
// Debug section to show raw API responses
|
||||
const debugSection = (
|
||||
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<h3 className="text-lg font-medium text-yellow-800 mb-2">Debug Information</h3>
|
||||
<div className="text-sm text-yellow-700">
|
||||
<p><strong>User Stats:</strong> {JSON.stringify(userStats)}</p>
|
||||
<p><strong>Course Stats:</strong> {JSON.stringify(courseStats)}</p>
|
||||
<p><strong>User Role:</strong> {user?.role}</p>
|
||||
<p><strong>Is Super Admin:</strong> {isSuperAdmin ? 'Yes' : 'No'}</p>
|
||||
<p><strong>Card Values:</strong></p>
|
||||
<ul className="ml-4">
|
||||
<li>Total Users: {userStats?.data?.stats?.totalUsers} (type: {typeof userStats?.data?.stats?.totalUsers})</li>
|
||||
<li>Trainers: {userStats?.data?.stats?.trainers} (type: {typeof userStats?.data?.stats?.trainers})</li>
|
||||
<li>Trainees: {userStats?.data?.stats?.trainees} (type: {typeof userStats?.data?.stats?.trainees})</li>
|
||||
<li>Total Courses: {courseStats?.data?.stats?.totalCourses} (type: {typeof courseStats?.data?.stats?.totalCourses})</li>
|
||||
</ul>
|
||||
<p><strong>Direct Test Values:</strong></p>
|
||||
<ul className="ml-4">
|
||||
<li>userStats?.data?.stats?.totalUsers: "{userStats?.data?.stats?.totalUsers}"</li>
|
||||
<li>userStats?.data?.stats?.trainers: "{userStats?.data?.stats?.trainers}"</li>
|
||||
<li>userStats?.data?.stats?.trainees: "{userStats?.data?.stats?.trainees}"</li>
|
||||
<li>courseStats?.data?.stats?.totalCourses: "{courseStats?.data?.stats?.totalCourses}"</li>
|
||||
</ul>
|
||||
<p><strong>Loading States:</strong></p>
|
||||
<ul className="ml-4">
|
||||
<li>userStatsLoading: {userStatsLoading ? 'true' : 'false'}</li>
|
||||
<li>courseStatsLoading: {courseStatsLoading ? 'true' : 'false'}</li>
|
||||
</ul>
|
||||
const renderSuperAdminDashboard = () => (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<UsersIcon className="h-8 w-8 text-primary-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Total Users</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{userStats?.data?.stats?.totalUsers || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<AcademicCapIcon className="h-8 w-8 text-green-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Trainers</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{userStats?.data?.stats?.trainers || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<UserGroupIcon className="h-8 w-8 text-blue-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Trainees</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{userStats?.data?.stats?.trainees || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<BookOpenIcon className="h-8 w-8 text-purple-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Total Courses</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{courseStats?.data?.stats?.totalCourses || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{/* Test Card to verify data */}
|
||||
<div className="card bg-red-50 border-red-200">
|
||||
<h3 className="text-lg font-medium text-red-800 mb-2">TEST CARD - Raw Data</h3>
|
||||
<div className="text-sm text-red-700">
|
||||
<p>userStats?.data?.stats?.totalUsers: {userStats?.data?.stats?.totalUsers}</p>
|
||||
<p>userStats?.data?.stats?.trainers: {userStats?.data?.stats?.trainers}</p>
|
||||
<p>userStats?.data?.stats?.trainees: {userStats?.data?.stats?.trainees}</p>
|
||||
<p>courseStats?.data?.stats?.totalCourses: {courseStats?.data?.stats?.totalCourses}</p>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Recent Users</h3>
|
||||
<div className="space-y-3">
|
||||
{/* Add recent users list here */}
|
||||
<p className="text-gray-500 text-sm">No recent users to display</p>
|
||||
</div>
|
||||
</div>
|
||||
<SliderImageUpload />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<UsersIcon className="h-8 w-8 text-primary-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Total Users</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{userStats?.data?.stats?.totalUsers || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">System Overview</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Active Users</span>
|
||||
<span className="text-sm font-medium">{userStats?.data?.stats?.activeUsers || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<AcademicCapIcon className="h-8 w-8 text-green-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Trainers</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{userStats?.data?.stats?.trainers || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Published Courses</span>
|
||||
<span className="text-sm font-medium">{courseStats?.data?.stats?.publishedCourses || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<UserGroupIcon className="h-8 w-8 text-blue-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Trainees</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{userStats?.data?.stats?.trainees || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Total Enrollments</span>
|
||||
<span className="text-sm font-medium">{enrollmentStats?.data?.stats?.totalEnrollments || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<BookOpenIcon className="h-8 w-8 text-purple-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Total Courses</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{courseStats?.data?.stats?.totalCourses || 0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Recent Users</h3>
|
||||
<div className="space-y-3">
|
||||
{/* Add recent users list here */}
|
||||
<p className="text-gray-500 text-sm">No recent users to display</p>
|
||||
</div>
|
||||
</div>
|
||||
<SliderImageUpload />
|
||||
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">System Overview</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Active Users</span>
|
||||
<span className="text-sm font-medium">{userStats?.data?.stats?.activeUsers || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Published Courses</span>
|
||||
<span className="text-sm font-medium">{courseStats?.data?.stats?.publishedCourses || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Total Enrollments</span>
|
||||
<span className="text-sm font-medium">{enrollmentStats?.data?.stats?.totalEnrollments || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Featured Courses</span>
|
||||
<span className="text-sm font-medium">{courseStats?.data?.stats?.featuredCourses || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600">Featured Courses</span>
|
||||
<span className="text-sm font-medium">{courseStats?.data?.stats?.featuredCourses || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTrainerDashboard = () => (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -484,9 +397,6 @@ const Dashboard = () => {
|
|||
<p className="text-gray-600">Welcome back, {user?.firstName}! Here's what's happening.</p>
|
||||
</div>
|
||||
|
||||
{/* Debug section */}
|
||||
{debugSection}
|
||||
|
||||
{isSuperAdmin && renderSuperAdminDashboard()}
|
||||
{isTrainer && renderTrainerDashboard()}
|
||||
{isTrainee && renderTraineeDashboard()}
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
Email address
|
||||
<label htmlFor="identifier" className="sr-only">
|
||||
Email or Phone
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
id="identifier"
|
||||
name="identifier"
|
||||
type="text"
|
||||
autoComplete="identifier"
|
||||
required
|
||||
className="input-field rounded-t-lg"
|
||||
placeholder="Email address"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email or Phone"
|
||||
value={identifier}
|
||||
onChange={(e) => setIdentifier(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
|
|
@ -143,17 +143,6 @@ const Login = () => {
|
|||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
Demo Accounts:
|
||||
</p>
|
||||
<div className="mt-2 space-y-1 text-xs text-gray-500">
|
||||
<p>Super Admin: admin@courseworx.com / admin123</p>
|
||||
<p>Trainer: trainer@courseworx.com / trainer123</p>
|
||||
<p>Trainee: trainee@courseworx.com / trainee123</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
283
frontend/src/pages/Setup.js
Normal file
283
frontend/src/pages/Setup.js
Normal file
|
|
@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div className="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-primary-100">
|
||||
<svg
|
||||
className="h-8 w-8 text-primary-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
CourseWorx Setup
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Create your Super Admin account to get started
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-gray-700">
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-gray-700">
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
required
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium text-gray-700">
|
||||
Phone Number <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
required
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
required
|
||||
className="mt-1 block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
|
||||
Confirm Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
required
|
||||
className="mt-1 block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4 border border-red-200">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">
|
||||
Setup Error
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingSpinner size="sm" />
|
||||
) : (
|
||||
'Complete Setup'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Setup;
|
||||
164
frontend/src/pages/TraineeLogin.js
Normal file
164
frontend/src/pages/TraineeLogin.js
Normal file
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="flex justify-center">
|
||||
<div className="h-12 w-12 rounded-full bg-primary-500 flex items-center justify-center">
|
||||
<AcademicCapIcon className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Trainee Login
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Access your enrolled courses
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="identifier" className="block text-sm font-medium text-gray-700">
|
||||
Email or Phone Number
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<UserIcon className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
id="identifier"
|
||||
name="identifier"
|
||||
type="text"
|
||||
required
|
||||
value={formData.identifier}
|
||||
onChange={handleChange}
|
||||
className="appearance-none block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
placeholder="Enter your email or phone number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<LockClosedIcon className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
required
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className="appearance-none block w-full pl-10 pr-10 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">
|
||||
Need help?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
Contact your trainer or administrator for access
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TraineeLogin;
|
||||
|
|
@ -436,8 +436,8 @@ const Users = () => {
|
|||
<input type="email" name="email" value={userForm.email} onChange={handleFormChange} className="input-field w-full" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Phone</label>
|
||||
<input type="text" name="phone" value={userForm.phone} onChange={handleFormChange} className="input-field w-full" />
|
||||
<label className="block text-sm font-medium mb-1">Phone <span className="text-red-500">*</span></label>
|
||||
<input type="tel" name="phone" value={userForm.phone} onChange={handleFormChange} className="input-field w-full" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Role</label>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
npm run start
|
||||
|
|
|
|||
10
tat -an findstr 5000
Normal file
10
tat -an findstr 5000
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[33md7075fd[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mmain[m[33m)[m 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
|
||||
[33ma4aba8e[m 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
|
||||
[33md0ee0c1[m 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
|
||||
[33mceb1f56[m 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
|
||||
[33mcd5370f[m[33m ([m[1;31morigin/main[m[33m, [m[1;31morigin/HEAD[m[33m)[m 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
|
||||
[33m6cad9b6[m v1.1.3: Enhanced login experience, trainer profiles, modern course details, and dashboard improvements
|
||||
[33mdece9c8[m v1.1.2: Add trainer assignment system and navigation improvements
|
||||
[33m15281c5[m[33m ([m[1;33mtag: [m[1;33mv1.1.1[m[33m)[m Version 1.1.1 - Enhanced Course Content Management Interface
|
||||
[33mb4d90c6[m[33m ([m[1;33mtag: [m[1;33mv1.1.0[m[33m)[m Release v1.1.0 - Course Content & Enrollment Management
|
||||
[33mcca9032[m[33m ([m[1;33mtag: [m[1;33mv1.0.0[m[33m)[m Release v1.0.0 - Complete Course Management System
|
||||
680
version.txt
680
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
|
||||
- 🔄 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
|
||||
Loading…
Reference in a new issue