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:
Mahmoud M. Abdalla 2025-08-20 20:57:41 +03:00
parent 8a71d336e5
commit 95f4377170
24 changed files with 1807 additions and 978 deletions

View file

@ -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

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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) {

View file

@ -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' }));

View file

@ -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);
}
},
},
},
},

View file

@ -50,6 +50,5 @@
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6"
},
"proxy": "http://localhost:5000"
}
}

View file

@ -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 />

View file

@ -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 */}

View file

@ -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');

View file

@ -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,

View file

@ -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>

View 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;

View file

@ -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()}

View file

@ -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
View 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;

View 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;

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,10 @@
d7075fd (HEAD -> main) Fix dashboard data access issue - Fixed data access path to use .data wrapper - Updated all dashboard cards to use correct data structure - Updated debug section to show correct data access - Fixed trainer and trainee dashboard data access - The issue was that API response has extra 'data' wrapper
a4aba8e Add comprehensive debugging for dashboard data display - Added detailed console logging for data access - Enhanced debug section with direct value testing - Added test card to verify raw data values - Added loading state debugging - Disabled React Query caching to force fresh data
d0ee0c1 Add debugging to dashboard cards and fix data display - Added console logging to see exact data being passed to dashboard cards - Enhanced debug section to show data types and values - Added specific debugging for SuperAdmin dashboard rendering
ceb1f56 Fix header layout and add debugging for dashboard statistics - Removed 'CourseWorx' text from header - Fixed logo path to use /images/cx-logo.png - Moved navigation items next to Home icon - Added debugging to API calls and dashboard - Added console logging to backend stats endpoints
cd5370f (origin/main, origin/HEAD) v1.1.4: Modern header navigation & real-time dashboard statistics - Removed sidebar completely and moved navigation to header - Added CourseWorx logo and Home icon for dashboard access - Moved Courses and Users links to header with icons - Updated all dashboard cards to show real database counts - Enhanced backend API endpoints for role-specific statistics - Fixed ESLint warnings in CourseContent.js and CourseContentViewer.js - Improved responsive design and user experience
6cad9b6 v1.1.3: Enhanced login experience, trainer profiles, modern course details, and dashboard improvements
dece9c8 v1.1.2: Add trainer assignment system and navigation improvements
15281c5 (tag: v1.1.1) Version 1.1.1 - Enhanced Course Content Management Interface
b4d90c6 (tag: v1.1.0) Release v1.1.0 - Course Content & Enrollment Management
cca9032 (tag: v1.0.0) Release v1.0.0 - Complete Course Management System

View file

@ -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