courseworx/frontend/src/pages/TrainerStudents.js
Mahmoud M. Abdalla 973625f87d feat: Implement Course Content Section for Trainers (v1.3.0)
MAJOR FEATURE: Complete Course Content Management System

 NEW FEATURES:
- Enhanced Trainer Dashboard with clickable cards
- New Trainer Courses Page (/trainer/courses) with filtering and management
- New Trainer Students Page (/trainer/students) with enrollment management
- Backend API endpoints for trainer-specific data
- Enhanced API services with React Query integration

 TECHNICAL IMPROVEMENTS:
- Created TrainerCourses.js and TrainerStudents.js components
- Updated Dashboard.js with enhanced navigation
- Added new routes in App.js for trainer pages
- Implemented secure trainer-specific backend endpoints
- Added role-based access control and data isolation

 DESIGN FEATURES:
- Responsive design with mobile-first approach
- Beautiful UI with hover effects and transitions
- Consistent styling throughout the application
- Accessibility improvements with ARIA labels

 SECURITY:
- Trainers can only access their own data
- Secure API authentication required
- Data isolation between different trainers

 PERFORMANCE:
- Efficient React Query implementation
- Optimized database queries
- Responsive image handling

 BUG FIXES:
- Fixed phone number login functionality
- Removed temporary debug endpoints
- Cleaned up authentication logging

This release provides trainers with comprehensive tools to manage
their courses and students, significantly improving the user experience
and functionality of the CourseWorx platform.
2025-08-21 03:16:29 +03:00

299 lines
12 KiB
JavaScript

import React, { useState, useRef, useEffect } from 'react';
import { useQuery } from 'react-query';
import { Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { enrollmentsAPI, coursesAPI } from '../services/api';
import {
UsersIcon,
BookOpenIcon,
CalendarIcon,
ChartBarIcon,
EllipsisVerticalIcon,
UserIcon,
EyeIcon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import LoadingSpinner from '../components/LoadingSpinner';
import toast from 'react-hot-toast';
const TrainerStudents = () => {
const { user } = useAuth();
const [selectedCourse, setSelectedCourse] = useState('all');
const [showActionsDropdown, setShowActionsDropdown] = useState(null);
const dropdownRef = useRef(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setShowActionsDropdown(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Fetch trainer's courses
const { data: coursesData, isLoading: coursesLoading } = useQuery(
['trainer-courses'],
() => coursesAPI.getTrainerCourses(user?.id),
{ enabled: !!user?.id }
);
// Fetch enrollments for selected course
const { data: enrollmentsData, isLoading: enrollmentsLoading } = useQuery(
['trainer-enrollments', selectedCourse],
() => selectedCourse === 'all'
? enrollmentsAPI.getTrainerEnrollments(user?.id)
: enrollmentsAPI.getCourseTrainees(selectedCourse),
{ enabled: !!user?.id && !!selectedCourse }
);
const handleRemoveStudent = async (enrollmentId, studentName, courseTitle) => {
if (window.confirm(`Are you sure you want to remove ${studentName} from ${courseTitle}?`)) {
try {
await enrollmentsAPI.delete(enrollmentId);
toast.success(`${studentName} removed from ${courseTitle} successfully!`);
// Refetch the data
window.location.reload();
} catch (error) {
toast.error('Failed to remove student from course');
}
}
};
if (coursesLoading || enrollmentsLoading) return <LoadingSpinner />;
const courses = coursesData?.courses || [];
const enrollments = enrollmentsData?.enrollments || enrollmentsData?.trainees || [];
// Get unique students across all courses
const uniqueStudents = enrollments.reduce((acc, enrollment) => {
const studentId = enrollment.user?.id || enrollment.userId;
if (!acc.find(s => s.id === studentId)) {
acc.push({
id: studentId,
firstName: enrollment.user?.firstName || enrollment.user?.firstName,
lastName: enrollment.user?.lastName || enrollment.user?.lastName,
email: enrollment.user?.email || enrollment.user?.email,
avatar: enrollment.user?.avatar || enrollment.user?.avatar,
enrollments: []
});
}
const student = acc.find(s => s.id === studentId);
student.enrollments.push({
courseId: enrollment.course?.id || enrollment.courseId,
courseTitle: enrollment.course?.title || enrollment.course?.title,
status: enrollment.status,
enrolledAt: enrollment.enrolledAt,
progress: enrollment.progress || 0,
enrollmentId: enrollment.id
});
return acc;
}, []);
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">My Students</h1>
<p className="text-gray-600 mt-2">Manage students enrolled in your courses</p>
</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<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 Students</p>
<p className="text-2xl font-semibold text-gray-900">{uniqueStudents.length}</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<div className="flex-shrink-0">
<BookOpenIcon className="h-8 w-8 text-green-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">{courses.length}</p>
</div>
</div>
</div>
<div className="card">
<div className="flex items-center">
<div className="flex-shrink-0">
<ChartBarIcon className="h-8 w-8 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Total Enrollments</p>
<p className="text-2xl font-semibold text-gray-900">{enrollments.length}</p>
</div>
</div>
</div>
</div>
{/* Course Filter */}
<div className="mb-6">
<label htmlFor="course-filter" className="block text-sm font-medium text-gray-700 mb-2">
Filter by Course
</label>
<select
id="course-filter"
value={selectedCourse}
onChange={(e) => setSelectedCourse(e.target.value)}
className="block w-full max-w-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
>
<option value="all">All Courses</option>
{courses.map((course) => (
<option key={course.id} value={course.id}>
{course.title}
</option>
))}
</select>
</div>
{/* Students List */}
{uniqueStudents.length === 0 ? (
<div className="text-center py-12">
<UsersIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No students</h3>
<p className="mt-1 text-sm text-gray-500">
{selectedCourse === 'all'
? "You don't have any students enrolled in your courses yet."
: "No students are enrolled in this course."
}
</p>
</div>
) : (
<div className="space-y-6">
{uniqueStudents.map((student) => (
<div key={student.id} className="card">
{/* Student Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
{student.avatar ? (
<img
className="h-12 w-12 rounded-full object-cover"
src={student.avatar}
alt={`${student.firstName} ${student.lastName}`}
/>
) : (
<div className="h-12 w-12 rounded-full bg-gray-300 flex items-center justify-center">
<UserIcon className="h-6 w-6 text-gray-600" />
</div>
)}
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
{student.firstName} {student.lastName}
</h3>
<p className="text-sm text-gray-500">{student.email}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Link
to={`/users/${student.id}`}
className="btn-secondary flex items-center space-x-2"
>
<EyeIcon className="h-4 w-4" />
<span>View Profile</span>
</Link>
{/* Actions Dropdown */}
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowActionsDropdown(showActionsDropdown === student.id ? null : student.id)}
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200"
title="Student Actions"
>
<EllipsisVerticalIcon className="h-5 w-5" />
</button>
{showActionsDropdown === student.id && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50">
<div className="py-1">
<button
onClick={() => {
setShowActionsDropdown(null);
// Add more actions here in the future
}}
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
disabled
>
<span className="text-gray-400">More actions coming soon...</span>
</button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Enrollments */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700">Course Enrollments</h4>
{student.enrollments.map((enrollment) => (
<div key={enrollment.enrollmentId} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<h5 className="font-medium text-gray-900">{enrollment.courseTitle}</h5>
<span className={`badge ${
enrollment.status === 'active' ? 'badge-success' :
enrollment.status === 'completed' ? 'badge-primary' : 'badge-warning'
}`}>
{enrollment.status}
</span>
</div>
<div className="flex items-center space-x-4 text-sm text-gray-500">
<div className="flex items-center">
<CalendarIcon className="h-4 w-4 mr-1" />
<span>Enrolled: {new Date(enrollment.enrolledAt).toLocaleDateString()}</span>
</div>
<div className="flex items-center">
<ChartBarIcon className="h-4 w-4 mr-1" />
<span>Progress: {enrollment.progress}%</span>
</div>
</div>
</div>
<button
onClick={() => handleRemoveStudent(
enrollment.enrollmentId,
`${student.firstName} ${student.lastName}`,
enrollment.courseTitle
)}
className="ml-4 p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors duration-200"
title="Remove from course"
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
);
};
export default TrainerStudents;