diff --git a/backend/routes/courses.js b/backend/routes/courses.js index 8a506e4..9a45e37 100644 --- a/backend/routes/courses.js +++ b/backend/routes/courses.js @@ -471,12 +471,22 @@ router.get('/stats/overview', auth, async (req, res) => { where: { ...whereClause, isFeatured: true } }); + // For trainers, provide specific stats + let myCourses = 0; + let myPublishedCourses = 0; + if (req.user.role === 'trainer') { + myCourses = totalCourses; + myPublishedCourses = publishedCourses; + } + res.json({ stats: { totalCourses, publishedCourses, unpublishedCourses: totalCourses - publishedCourses, - featuredCourses + featuredCourses, + myCourses, + myPublishedCourses } }); } catch (error) { diff --git a/backend/routes/enrollments.js b/backend/routes/enrollments.js index 9600a65..c6fe735 100644 --- a/backend/routes/enrollments.js +++ b/backend/routes/enrollments.js @@ -481,6 +481,11 @@ router.get('/stats/overview', auth, async (req, res) => { whereClause.courseId = trainerCourses.map(c => c.id); } + // If trainee, only show their enrollments + if (req.user.role === 'trainee') { + whereClause.userId = req.user.id; + } + const totalEnrollments = await Enrollment.count({ where: whereClause }); const activeEnrollments = await Enrollment.count({ where: { ...whereClause, status: 'active' } @@ -492,13 +497,49 @@ router.get('/stats/overview', auth, async (req, res) => { where: { ...whereClause, status: 'pending' } }); + // Get unique students count for trainers + let myStudents = 0; + if (req.user.role === 'trainer') { + const trainerCourses = await Course.findAll({ + where: { trainerId: req.user.id }, + attributes: ['id'] + }); + + if (trainerCourses.length > 0) { + const uniqueStudents = await Enrollment.findAll({ + where: { + courseId: trainerCourses.map(c => c.id), + status: { [require('sequelize').Op.in]: ['active', 'completed'] } + }, + attributes: ['userId'], + group: ['userId'] + }); + myStudents = uniqueStudents.length; + } + } + + // Get my enrollments count for trainees + let myEnrollments = 0; + let completedCourses = 0; + if (req.user.role === 'trainee') { + myEnrollments = await Enrollment.count({ + where: { userId: req.user.id } + }); + completedCourses = await Enrollment.count({ + where: { userId: req.user.id, status: 'completed' } + }); + } + res.json({ stats: { totalEnrollments, activeEnrollments, completedEnrollments, pendingEnrollments, - cancelledEnrollments: totalEnrollments - activeEnrollments - completedEnrollments - pendingEnrollments + cancelledEnrollments: totalEnrollments - activeEnrollments - completedEnrollments - pendingEnrollments, + myStudents, + myEnrollments, + completedCourses } }); } catch (error) { diff --git a/frontend/src/components/Layout.js b/frontend/src/components/Layout.js index 3e10f4b..d182e77 100644 --- a/frontend/src/components/Layout.js +++ b/frontend/src/components/Layout.js @@ -7,8 +7,6 @@ import { AcademicCapIcon, UsersIcon, UserIcon, - Bars3Icon, - XMarkIcon, ArrowRightOnRectangleIcon, ChevronDownIcon, } from '@heroicons/react/24/outline'; @@ -17,7 +15,6 @@ const Layout = () => { const { user, logout, isSuperAdmin } = useAuth(); const navigate = useNavigate(); const location = useLocation(); - const [sidebarOpen, setSidebarOpen] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false); @@ -43,7 +40,6 @@ const Layout = () => { }, [userMenuOpen]); const navigation = [ - { name: 'Dashboard', href: '/dashboard', icon: HomeIcon }, { name: 'Courses', href: '/courses', icon: AcademicCapIcon }, ...(isSuperAdmin ? [{ name: 'Users', href: '/users', icon: UsersIcon }] : []), ]; @@ -57,136 +53,101 @@ const Layout = () => { return (
- {/* Mobile sidebar */} -
-
setSidebarOpen(false)} /> -
-
+ {/* Header */} +
+ {/* Logo and Home Icon */} +
+
+ CourseWorx

CourseWorx

-
- +
-
- {/* Desktop sidebar */} -
-
-
-

CourseWorx

+ {/* Navigation Items */} +
+ {navigation.map((item) => ( + + + {item.name} + + ))} +
+ + {/* User dropdown menu */} +
+
+ +
+ + + {/* Dropdown menu */} + {userMenuOpen && ( +
+
+ setUserMenuOpen(false)} + > + + Profile + + +
+
+ )}
-
{/* Main content */} -
-
- -
-
-
-
- - {/* User dropdown menu */} -
- - - {/* Dropdown menu */} - {userMenuOpen && ( -
-
- setUserMenuOpen(false)} - > - - Profile - - -
-
- )} -
-
-
+
+
+
- -
-
- -
-
-
+ {/* Password Change Modal */} { const [contentForm, setContentForm] = useState({ title: '', description: '', type: 'document', order: 0, points: 0, isRequired: true, isPublished: true, articleContent: '', + url: '', // <-- Add url field for image/video }); - const [uploadingFile, setUploadingFile] = useState(false); const [selectedFile, setSelectedFile] = useState(null); // Quiz questions state @@ -99,7 +99,7 @@ const CourseContent = () => { ); const uploadFileMutation = useMutation( - ({ file, contentType }) => courseContentAPI.uploadFile(id, file, contentType), + ({ file, contentType, contentId }) => courseContentAPI.uploadFile(id, contentType, file, contentId), { onSuccess: (data) => { setContentForm(prev => ({ @@ -108,12 +108,10 @@ const CourseContent = () => { fileSize: data.fileSize, fileType: data.fileType })); - setUploadingFile(false); setSelectedFile(null); toast.success('File uploaded successfully!'); }, onError: (error) => { - setUploadingFile(false); toast.error(error.response?.data?.error || 'Failed to upload file'); }, } @@ -156,13 +154,19 @@ const CourseContent = () => { setContentForm({ title: '', description: '', type: 'document', order: 0, points: 0, isRequired: true, isPublished: true, articleContent: '', + url: '', // <-- Add url field for image/video }); setSelectedFile(null); }; const handleAddContent = async (e) => { e.preventDefault(); - createContentMutation.mutate(contentForm); + let data = { ...contentForm }; + // If image/video and url is provided, set fileUrl + if ((contentForm.type === 'image' || contentForm.type === 'video') && contentForm.url) { + data.fileUrl = contentForm.url; + } + createContentMutation.mutate(data); }; const handleEditContent = async (e) => { @@ -190,21 +194,22 @@ const CourseContent = () => { isRequired: content.isRequired, isPublished: content.isPublished, articleContent: content.articleContent || '', + url: content.fileUrl || '', // <-- Set url for image/video }); setShowEditContentModal(true); }; const handleFileUpload = async () => { if (!selectedFile) return; - setUploadingFile(true); - const formData = new FormData(); - formData.append('file', selectedFile); - formData.append('contentType', contentForm.type); - uploadFileMutation.mutate({ file: formData, contentType: contentForm.type }); + uploadFileMutation.mutate({ file: selectedFile, contentType: contentForm.type }); }; const handleFileChange = (e) => { - setSelectedFile(e.target.files[0]); + const file = e.target.files[0]; + setSelectedFile(file); + if (file) { + handleFileUpload(); // Auto-upload on file select + } }; const getContentTypeIcon = (type) => { @@ -602,19 +607,28 @@ const CourseContent = () => { {selectedFile && (

Selected: {selectedFile.name}

-
)}
)} + {['image', 'video'].includes(contentForm.type) && ( +
+ + +

If provided, this will be used instead of an uploaded file.

+
+ )} +
diff --git a/frontend/src/pages/CourseContentViewer.js b/frontend/src/pages/CourseContentViewer.js index 28c6035..c21036f 100644 --- a/frontend/src/pages/CourseContentViewer.js +++ b/frontend/src/pages/CourseContentViewer.js @@ -1,7 +1,6 @@ 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 { useQuery, useMutation } from 'react-query'; import { coursesAPI, courseContentAPI } from '../services/api'; import { ArrowLeftIcon, @@ -21,7 +20,6 @@ import toast from 'react-hot-toast'; const CourseContentViewer = () => { const { id } = useParams(); - const queryClient = useQueryClient(); const [selectedContent, setSelectedContent] = useState(null); const [quizAnswers, setQuizAnswers] = useState({}); const [quizResults, setQuizResults] = useState({}); @@ -100,16 +98,30 @@ const CourseContentViewer = () => { return labels[type] || 'Document'; }; + // Helper to resolve the file URL (uploaded or online) + const resolveFileUrl = (content) => { + // Prefer fileUrl, then url + let url = content.fileUrl || content.url || ''; + if (!url) return null; + if (url.startsWith('http')) return url; + // Otherwise, construct the full URL + const baseUrl = process.env.REACT_APP_API_URL || 'http://localhost:5000'; + return `${baseUrl}${url}`; + }; + const renderContent = (content) => { + const fileUrl = resolveFileUrl(content); + console.log('ContentViewer: resolved fileUrl:', fileUrl); + switch (content.type) { case 'document': return (

{content.title}

- {content.fileUrl && ( + {fileUrl && ( @@ -121,10 +133,10 @@ const CourseContentViewer = () => { {content.description && (

{content.description}

)} - {content.fileUrl ? ( + {fileUrl ? (