diff --git a/backend/controllers/courseController.ts b/backend/controllers/courseController.ts index 4c5adfe..0bbdca4 100644 --- a/backend/controllers/courseController.ts +++ b/backend/controllers/courseController.ts @@ -25,7 +25,7 @@ export const getCourses = async ( try { const filters = req.query; const courseResponses = await Course.find(filters) - .populate(["ratings"]) + .populate(["speakers"]) .exec(); res.status(200).json({ success: true, @@ -52,9 +52,7 @@ export const getCourseById = async ( if (id) { // Find course by ID and populate related fields - const course = await Course.findById(id) - .populate(["ratings", "managers"]) - .exec(); + const course = await Course.findById(id).populate(["speakers"]).exec(); if (!course) { res.status(404).json({ @@ -526,6 +524,84 @@ export const getCourseProgress = async ( } }; +// @desc Get progress for a single user in a course +// @route GET /api/courses/:courseId/progress/:userId +// @access Public +export const getUserCourseProgress = async ( + req: Request, + res: Response +): Promise => { + try { + const { courseId, userId } = req.params; + + // Validate input + if (!courseId || !userId) { + res.status(400).json({ + success: false, + message: "Course ID and User ID are required.", + }); + return; + } + + // Check if the course exists + const course = await Course.findById(courseId); + if (!course) { + res.status(404).json({ + success: false, + message: "Course not found.", + }); + return; + } + + // Check if the user is enrolled in the course + const isEnrolled = course.students.some( + (id) => id.toString() === userId.toString() + ); + if (!isEnrolled) { + res.status(403).json({ + success: false, + message: "User is not enrolled in this course.", + }); + return; + } + + // Check for existing progress + let progress = await Progress.findOne({ course: courseId, user: userId }) + .populate("user") + .populate("course"); + + // If no progress exists, create one + if (!progress) { + progress = await new Progress({ + user: userId, + course: courseId, + isComplete: false, + completedComponents: { + webinar: false, + survey: false, + certificate: false, + }, + dateCompleted: null, + }).save(); + + // repopulate after save + progress = await Progress.findById(progress._id) + .populate("user") + .populate("course"); + } + + res.status(200).json({ + success: true, + progress, + }); + } catch (error: any) { + res.status(500).json({ + success: false, + message: error.message || "Internal server error.", + }); + } +}; + // @desc Update user's progress in a course // @route PUT /api/courses/:courseId/progress/:userId // @access Public @@ -605,3 +681,81 @@ export const updateUserProgress = async ( }); } }; + +// @desc Batch update user progress in a course +// @route PUT /api/courses/:courseId/progress/batch +// @access Public +export const batchUpdateUserProgress = async ( + req: Request, + res: Response +): Promise => { + try { + const { courseId } = req.params; + const updates = req.body.updates; // Array of updates + + if (!Array.isArray(updates) || updates.length === 0) { + res.status(400).json({ + success: false, + message: "No updates provided.", + }); + return; + } + + const results: any[] = []; + + for (const update of updates) { + const { userId, webinarComplete, surveyComplete, certificateComplete } = + update; + + console.log("Processing update for user:", userId); + + const progress = await Progress.findOne({ + course: courseId, + user: userId, + }); + + if (!progress) { + console.warn(`Progress not found for user ${userId}`); + results.push({ + userId, + success: false, + message: "Progress record not found.", + }); + continue; + } + + const completedComponents = { + webinar: Boolean(webinarComplete), + survey: Boolean(surveyComplete), + certificate: Boolean(certificateComplete), + }; + + progress.completedComponents = completedComponents; + + const allComplete = Object.values(completedComponents).every(Boolean); + progress.isComplete = allComplete; + if (allComplete && !progress.dateCompleted) { + progress.dateCompleted = new Date(); + } + + await progress.save(); + + results.push({ + userId, + success: true, + }); + } + + res.status(200).json({ + success: true, + message: "Batch update complete.", + results, + }); + } catch (error: any) { + console.error("Error in batchUpdateUserProgress:", error); + res.status(500).json({ + success: false, + message: error.message || "Internal server error.", + }); + } +}; diff --git a/backend/controllers/speakerController.ts b/backend/controllers/speakerController.ts index 7623cc4..1ca6142 100644 --- a/backend/controllers/speakerController.ts +++ b/backend/controllers/speakerController.ts @@ -49,12 +49,8 @@ export const createSpeaker = async ( res: Response ): Promise => { try { - const { name, title, email, company, bio, disclosures } = req.body; - - // Store image URL if an image is uploaded - const imageUrl = req.file - ? `http://localhost:5001/uploads/${req.file.filename}` - : null; + console.log(req.body); + const { name, title, email, company, bio, image } = req.body; const speaker = new Speaker({ name, @@ -62,13 +58,13 @@ export const createSpeaker = async ( email, company, bio, - disclosures: disclosures ? disclosures.split(",") : [], - image: imageUrl, + image, }); await speaker.save(); res.status(201).json({ speaker, message: "Speaker created successfully" }); } catch (error) { + console.error(error); res.status(500).json({ message: error instanceof Error ? error.message : "An unknown error occurred", @@ -87,11 +83,6 @@ export const updateSpeaker = async ( const { id: speakerId } = req.params; const updates = req.body; - // If a new image is uploaded, store its URL - if (req.file) { - updates.image = `http://localhost:5001/uploads/${req.file.filename}`; - } - const updatedSpeaker = await Speaker.findByIdAndUpdate(speakerId, updates, { new: true, }); diff --git a/backend/controllers/userController.ts b/backend/controllers/userController.ts index e36614d..16fdeee 100644 --- a/backend/controllers/userController.ts +++ b/backend/controllers/userController.ts @@ -212,11 +212,17 @@ export const register = async (req: Request, res: Response): Promise => { user: new mongoose.Types.ObjectId(userId), course: new mongoose.Types.ObjectId(courseId), isComplete: false, - completedComponents: {}, + completedComponents: { + webinar: false, + survey: false, + certificate: false, + }, dateCompleted: null, }); await progress.save(); + console.log("progress", progress); + if (!course.students.includes(new mongoose.Types.ObjectId(userId))) { course.students.push(new mongoose.Types.ObjectId(userId)); await course.save(); diff --git a/backend/models/courseModel.ts b/backend/models/courseModel.ts index c9708b5..bcfdb1a 100644 --- a/backend/models/courseModel.ts +++ b/backend/models/courseModel.ts @@ -25,8 +25,12 @@ export interface ICourse extends Document { regStart: Date; regEnd: Date; //Virtual Training - Live Meeting, In-Person Training, Virtual Training - On Demand, Virtual Training - Live Webinar - productType: string; - productInfo: string + productType: + | "Virtual Training - Live Meeting" + | "In-Person Training" + | "Virtual Training - On Demand" + | "Virtual Training - Live Webinar"; + productInfo: string; shortUrl: string; draft: boolean; } @@ -78,8 +82,18 @@ const CourseSchema: Schema = new Schema( ], regStart: { type: Date, required: false }, regEnd: { type: Date, required: false }, - productType: { type: String, required: false }, - productInfo: {type:String, required: false}, + productType: { + type: String, + required: false, + enum: [ + "Virtual Training - Live Meeting", + "In-Person Training", + "Virtual Training - On Demand", + "Virtual Training - Live Webinar", + "", + ], + }, + productInfo: { type: String, required: false }, shortUrl: { type: String, required: false }, draft: { type: Boolean, required: true, default: true }, }, diff --git a/backend/routes/courseRoutes.ts b/backend/routes/courseRoutes.ts index 7922f4a..d778342 100644 --- a/backend/routes/courseRoutes.ts +++ b/backend/routes/courseRoutes.ts @@ -8,6 +8,8 @@ import { getCourseUsers, getCourseProgress, updateUserProgress, + batchUpdateUserProgress, + getUserCourseProgress, } from "../controllers/courseController"; const router = express.Router(); @@ -33,7 +35,11 @@ router.get("/:courseId/users", getCourseUsers); // GET progress for all users in a course router.get("/:courseId/progress", getCourseProgress); +router.get("/:courseId/progress/single/:userId", getUserCourseProgress); + // PUT update user's progress in a course -router.put("/:courseId/progress/:userId", updateUserProgress); +router.put("/:courseId/progress/single/:userId", updateUserProgress); + +router.put("/:courseId/progress/batch", batchUpdateUserProgress); export default router; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6aeefa4..db1519d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,6 +33,7 @@ "react-scripts": "^5.0.1", "react-select": "^5.10.1", "react-select-country-list": "^2.2.3", + "react-youtube": "^10.1.0", "source-map-loader": "^5.0.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4", @@ -11559,6 +11560,12 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==", + "license": "MIT" + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -14560,6 +14567,23 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-youtube": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", + "integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "3.1.3", + "prop-types": "15.8.1", + "youtube-player": "5.5.2" + }, + "engines": { + "node": ">= 14.x" + }, + "peerDependencies": { + "react": ">=0.14.1" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15487,6 +15511,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/sister": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", + "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==", + "license": "BSD-3-Clause" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -17867,6 +17897,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/youtube-player": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz", + "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==", + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + } + }, + "node_modules/youtube-player/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/youtube-player/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/zustand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index f7a2fe3..7d80b36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "react-scripts": "^5.0.1", "react-select": "^5.10.1", "react-select-country-list": "^2.2.3", + "react-youtube": "^10.1.0", "source-map-loader": "^5.0.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4", diff --git a/frontend/src/components/AdminCoursePreview/AdminCoursePreview.tsx b/frontend/src/components/AdminCoursePreview/AdminCoursePreview.tsx index dfdd353..a93340c 100644 --- a/frontend/src/components/AdminCoursePreview/AdminCoursePreview.tsx +++ b/frontend/src/components/AdminCoursePreview/AdminCoursePreview.tsx @@ -8,6 +8,7 @@ import { ShieldCheck, Star, Trash2, + LayoutDashboard, } from "lucide-react"; import AdminCourseDeleteModal from "./AdminCourseDeleteModal"; import { Link } from "react-router-dom"; @@ -180,6 +181,9 @@ function AdminCoursePreview({ + + + - - -
-
-
- Webinar - -
- -
- Survey - -
- -
- Certificate - -
-
- -
- - -
-
- - - ); +const EditProgressModal: React.FC = ({ + isOpen, + onClose, + progress, + onSave, +}) => { + const [editedProgress, setEditedProgress] = useState(progress); + + // Update local state when progress prop changes + useEffect(() => { + setEditedProgress(progress); + }, [progress]); + + if (!isOpen) return null; + + const handleComponentChange = ( + component: "webinar" | "survey" | "certificate", + checked: boolean + ) => { + console.log("Component change:", component, checked); + const updatedProgress = { + ...editedProgress, + completedComponents: { + ...editedProgress.completedComponents, + [component]: checked, + }, + }; + + // Check if all components are complete + const allComplete = Object.values( + updatedProgress.completedComponents + ).every(Boolean); + updatedProgress.isComplete = allComplete; + if (allComplete && !updatedProgress.dateCompleted) { + updatedProgress.dateCompleted = new Date(); + } + + setEditedProgress(updatedProgress); + }; + + const handleSave = () => { + console.log("Save button clicked"); + console.log("Current edited progress:", editedProgress); + onSave(editedProgress); + }; + + return ( +
+
+
+
+

Edit User Progress

+
+

+ Product: + {progress.course.className} +

+

+ User: + {progress.user.name} +

+
+
+ +
+ +
+
+
+ Workshop + +
+ +
+ Survey + +
+ +
+ Certificate + +
+
+ +
+ + +
+
+
+
+ ); }; -const ProductProgressReport: React.FC = () => { - const [selectedCourse, setSelectedCourse] = useState(null); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); - const [selectedUserType, setSelectedUserType] = useState('All'); - const [excludeFinished, setExcludeFinished] = useState(false); - const [userProgress, setUserProgress] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage] = useState(15); - const [allCourses, setAllCourses] = useState([]); - const [editModalOpen, setEditModalOpen] = useState(false); - const [selectedProgress, setSelectedProgress] = useState(null); - - // Load courses from API - useEffect(() => { - const fetchAllCourses = async () => { - try { - const response = await apiClient.get('/courses'); - const courses = response.data.data.map((course: Course) => ({ - value: course._id, - label: course.className, - course: course - })); - setAllCourses(courses); - } catch (error) { - console.error('Error fetching courses:', error); - setAllCourses([]); - } - }; - - fetchAllCourses(); - }, []); - - // Load course options with search - const loadCourseOptions = async (inputValue: string) => { - if (!inputValue) { - return allCourses; - } - - const filteredCourses = allCourses.filter(course => - course.label.toLowerCase().includes(inputValue.toLowerCase()) - ); - return filteredCourses; - }; - - // Fetch progress from API - const fetchProgress = async () => { - if (!selectedCourse?.value) return; - - setIsLoading(true); - try { - // Get enrolled users for the course - const usersResponse = await apiClient.get(`/courses/${selectedCourse.value}/users`); - const enrolledUsers = usersResponse.data.users; - - // Get progress data for the course - const progressResponse = await apiClient.get(`/courses/${selectedCourse.value}/progress`); - const courseProgress = progressResponse.data.progress || []; - - // Combine user data with progress data - const combinedProgress: UserProgress[] = enrolledUsers.map((user: User) => { - const userProgress = courseProgress.find((p: any) => p.user?._id === user._id); - - // Default progress values - const defaultProgress = { - isComplete: false, - completedComponents: { - webinar: false, - survey: false, - certificate: false - }, - dateCompleted: null, - createdAt: new Date() - }; - - // Merge user progress with defaults - const mergedProgress = userProgress ? { - isComplete: userProgress.isComplete || false, - completedComponents: { - webinar: userProgress.completedComponents?.webinar || false, - survey: userProgress.completedComponents?.survey || false, - certificate: userProgress.completedComponents?.certificate || false - }, - dateCompleted: userProgress.dateCompleted || null, - createdAt: userProgress.createdAt || new Date() - } : defaultProgress; - - return { - user, - course: selectedCourse.course, - ...mergedProgress - }; - }); - - let filteredProgress = combinedProgress; - - // Apply filters - if (selectedUserType !== 'All') { - filteredProgress = filteredProgress.filter(p => p.user.role === selectedUserType); - } - - if (startDate) { - const start = new Date(startDate); - filteredProgress = filteredProgress.filter(p => - p.registeredDate && p.registeredDate >= start - ); - } - if (endDate) { - const end = new Date(endDate); - end.setHours(23, 59, 59, 999); - filteredProgress = filteredProgress.filter(p => - p.registeredDate && p.registeredDate <= end - ); - } - - if (excludeFinished) { - filteredProgress = filteredProgress.filter(p => !p.isComplete); - } - - setUserProgress(filteredProgress); - } catch (error) { - console.error('Error fetching data:', error); - setUserProgress([]); - } finally { - setIsLoading(false); - } - }; - - // Fetch progress when filters change - useEffect(() => { - if (selectedCourse) { - fetchProgress(); - } - }, [selectedCourse, startDate, endDate, selectedUserType, excludeFinished]); - - const handleSaveProgress = async (updatedProgress: UserProgress) => { - try { - const response = await apiClient.put( - `/courses/${updatedProgress.course._id}/progress/${updatedProgress.user._id}`, - { - webinarComplete: updatedProgress.completedComponents.webinar, - surveyComplete: updatedProgress.completedComponents.survey, - certificateComplete: updatedProgress.completedComponents.certificate, - isComplete: updatedProgress.isComplete, - dateCompleted: updatedProgress.dateCompleted - } - ); - - if (response.data.success) { - await fetchProgress(); - setEditModalOpen(false); - } else { - console.error('Failed to update progress:', response.data.message); - } - } catch (error) { - console.error('Error in handleSaveProgress:', error); - } - }; - - const handleDownloadCSV = () => { - if (!userProgress.length) return; - - const headers = [ - 'Name', - 'Email', - 'User Type', - 'Registered', - 'Completed', - 'Webinar', - 'Survey', - 'Certificate' - ]; - - const rows = userProgress.map(progress => [ - progress.user.name, - progress.user.email, - progress.user.role, - progress.registeredDate ? format(progress.registeredDate, 'MM/dd/yyyy h:mm a') : 'N/A', - progress.dateCompleted ? format(progress.dateCompleted, 'MM/dd/yyyy h:mm a') : 'N/A', - progress.completedComponents.webinar ? 'Complete' : 'Incomplete', - progress.completedComponents.survey ? 'Complete' : 'Incomplete', - progress.completedComponents.certificate ? 'Complete' : 'Incomplete' - ]); - - const csvContent = [ - headers.join(','), - ...rows.map(row => row.join(',')) - ].join('\n'); - - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); - link.setAttribute('href', url); - link.setAttribute('download', `${selectedCourse?.label}_progress_report.csv`); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - - const displayedProgress = userProgress - .slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); - - const totalPages = Math.ceil(userProgress.length / itemsPerPage); - - // Custom styles for react-select - const selectStyles = { - control: (base: any) => ({ - ...base, - minHeight: '42px', - borderRadius: '0.5rem', - borderColor: '#E5E7EB', - boxShadow: 'none', - '&:hover': { - borderColor: '#8757a3' - } - }), - option: (base: any, state: { isSelected: boolean; isFocused: boolean }) => ({ - ...base, - backgroundColor: state.isSelected - ? '#8757a3' - : state.isFocused - ? '#F3F4F6' - : 'white', - color: state.isSelected ? 'white' : '#374151', - '&:active': { - backgroundColor: '#8757a3' - } - }), - input: (base: any) => ({ - ...base, - color: '#374151' - }), - placeholder: (base: any) => ({ - ...base, - color: '#9CA3AF' - }), - singleValue: (base: any) => ({ - ...base, - color: '#374151' - }), - menu: (base: any) => ({ - ...base, - borderRadius: '0.5rem', - boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)' - }) - }; - - const handleEditClick = (progress: UserProgress) => { - setSelectedProgress(progress); - setEditModalOpen(true); - }; - - return ( -
-
-
-
-

Product Progress Report

-
- -
- {/* Course Search */} -
- setSelectedUserType(e.target.value)} - className="border rounded-lg px-3 py-1.5" - > - - - - - - - - - - - - -
- -
- Start Date - setStartDate(e.target.value)} - className="border rounded-lg px-3 py-1.5" - /> -
- -
- End Date - setEndDate(e.target.value)} - className="border rounded-lg px-3 py-1.5" - /> -
- -
- setExcludeFinished(e.target.checked)} - className="rounded border-gray-300" - /> - -
- - -
- - {/* Table Container */} -
- {isLoading ? ( -
-
-
- ) : !selectedCourse ? ( -
- Select a course to view progress report -
- ) : displayedProgress.length === 0 ? ( -
- No progress records found for the selected filters -
- ) : ( -
- - - - - - - - - - - - - - - - {displayedProgress.map((progress) => ( - - - - - - - - - - - - ))} - -
NameEmailTypeRegisteredCompletedWebinarSurveyCertificateEdit
{progress.user.name}{progress.user.email}{progress.user.role} - {progress.registeredDate - ?
- {format(progress.registeredDate, 'MM/dd/yy')} - {format(progress.registeredDate, 'h:mm a')} EST -
- : 'N/A' - } -
- {progress.dateCompleted - ?
- {format(progress.dateCompleted, 'MM/dd/yy')} - {format(progress.dateCompleted, 'h:mm a')} EST -
- : 'N/A' - } -
- {progress.completedComponents.webinar ? ( -
- - {format(new Date(), 'MM/dd/yy')} -
- ) : ( - - )} -
- {progress.completedComponents.survey ? ( -
- - {format(new Date(), 'MM/dd/yy')} -
- ) : ( - - )} -
- {progress.completedComponents.certificate ? ( -
- - {format(new Date(), 'MM/dd/yy')} -
- ) : ( - - )} -
- -
-
- )} -
- - {/* Pagination */} - {displayedProgress.length > 0 && ( -
- - {currentPage} - {Array.from({ length: Math.min(3, totalPages - currentPage) }, (_, i) => ( - - ))} - {currentPage + 3 < totalPages && ...} - {currentPage + 3 < totalPages && ( - - )} - -
- )} -
-
-
- - {/* Add the EditProgressModal component */} - {selectedProgress && ( - setEditModalOpen(false)} - progress={selectedProgress} - onSave={handleSaveProgress} - /> - )} - - ); +interface ProductProgressReportProps { + fixedCourseId: boolean; +} + +const ProductProgressReport: React.FC = ({ + fixedCourseId, +}) => { + const [selectedCourse, setSelectedCourse] = useState( + null + ); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [selectedUserType, setSelectedUserType] = useState("All"); + const [excludeFinished, setExcludeFinished] = useState(false); + const [userProgress, setUserProgress] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(15); + const [allCourses, setAllCourses] = useState([]); + const [editModalOpen, setEditModalOpen] = useState(false); + const [selectedProgress, setSelectedProgress] = useState( + null + ); + + const { id: courseId } = useParams(); + + // Load courses from API + useEffect(() => { + const fetchAllCourses = async () => { + try { + const response = await apiClient.get("/courses"); + const courses = response.data.data.map((course: Course) => ({ + value: course._id, + label: course.className, + course: course, + })); + setAllCourses(courses); + } catch (error) { + console.error("Error fetching courses:", error); + setAllCourses([]); + } + }; + if (!fixedCourseId) fetchAllCourses(); + }, []); + + useEffect(() => { + const fetchFixedCourse = async () => { + if (!fixedCourseId) return; + + try { + const response = await apiClient.get(`/courses/${courseId}`); + const course = response.data.data; + setSelectedCourse({ + value: course._id, + label: course.className, + course: course, + }); + } catch (err) { + console.error("Failed to load fixed course", err); + } + }; + + fetchFixedCourse(); + }, [fixedCourseId]); + + // Load course options with search + const loadCourseOptions = async (inputValue: string) => { + if (!inputValue) { + return allCourses; + } + + const filteredCourses = allCourses.filter((course) => + course.label.toLowerCase().includes(inputValue.toLowerCase()) + ); + return filteredCourses; + }; + + // Fetch progress from API + const fetchProgress = async () => { + if (!selectedCourse?.value) return; + + setIsLoading(true); + try { + // Get enrolled users for the course + const usersResponse = await apiClient.get( + `/courses/${selectedCourse.value}/users` + ); + const enrolledUsers = usersResponse.data.users; + + // Get progress data for the course + const progressResponse = await apiClient.get( + `/courses/${selectedCourse.value}/progress` + ); + const courseProgress = progressResponse.data.progress || []; + + // Combine user data with progress data + const combinedProgress: UserProgress[] = enrolledUsers.map( + (user: User) => { + const userProgress = courseProgress.find( + (p: any) => p.user?._id === user._id + ); + + // Default progress values + const defaultProgress = { + isComplete: false, + completedComponents: { + webinar: false, + survey: false, + certificate: false, + }, + dateCompleted: null, + createdAt: new Date(), + }; + + // Merge user progress with defaults + const mergedProgress = userProgress + ? { + isComplete: userProgress.isComplete || false, + completedComponents: { + webinar: userProgress.completedComponents?.webinar || false, + survey: userProgress.completedComponents?.survey || false, + certificate: + userProgress.completedComponents?.certificate || false, + }, + dateCompleted: userProgress.dateCompleted || null, + createdAt: userProgress.createdAt || new Date(), + } + : defaultProgress; + + return { + user, + course: selectedCourse.course, + ...mergedProgress, + }; + } + ); + + let filteredProgress = combinedProgress; + + // Apply filters + if (selectedUserType !== "All") { + filteredProgress = filteredProgress.filter( + (p) => p.user.role === selectedUserType + ); + } + + if (startDate) { + const start = new Date(startDate); + filteredProgress = filteredProgress.filter( + (p) => p.registeredDate && p.registeredDate >= start + ); + } + if (endDate) { + const end = new Date(endDate); + end.setHours(23, 59, 59, 999); + filteredProgress = filteredProgress.filter( + (p) => p.registeredDate && p.registeredDate <= end + ); + } + + if (excludeFinished) { + filteredProgress = filteredProgress.filter((p) => !p.isComplete); + } + + setUserProgress(filteredProgress); + } catch (error) { + console.error("Error fetching data:", error); + setUserProgress([]); + } finally { + setIsLoading(false); + } + }; + + // Fetch progress when filters change + useEffect(() => { + if (selectedCourse) { + fetchProgress(); + } + }, [selectedCourse, startDate, endDate, selectedUserType, excludeFinished]); + + const handleSaveProgress = (updatedProgress: UserProgress) => { + setUserProgress((prevProgress) => + prevProgress.map((p) => + p.user._id === updatedProgress.user._id + ? { ...p, ...updatedProgress } + : p + ) + ); + setEditModalOpen(false); + }; + + const handleSaveAllProgress = async () => { + if (!selectedCourse?.value) return; + + try { + const payload = userProgress.map((p) => ({ + userId: p.user._id, + webinarComplete: p.completedComponents.webinar, + surveyComplete: p.completedComponents.survey, + certificateComplete: p.completedComponents.certificate, + isComplete: p.isComplete, + dateCompleted: p.dateCompleted, + })); + + const response = await apiClient.put( + `/courses/${selectedCourse.value}/progress/batch`, + { updates: payload } + ); + + if (response.data.success) { + alert("All progress saved successfully."); + } else { + console.error("Batch update failed:", response.data.message); + alert("Batch update failed."); + } + } catch (error) { + console.error("Error during batch update:", error); + alert("Error occurred while saving progress."); + } + }; + + const handleDownloadCSV = () => { + if (!userProgress.length) return; + + const headers = [ + "Name", + "Email", + "User Type", + "Registered", + "Completed", + "Webinar", + "Survey", + "Certificate", + ]; + + const rows = userProgress.map((progress) => [ + progress.user.name, + progress.user.email, + progress.user.role, + progress.registeredDate + ? format(progress.registeredDate, "MM/dd/yyyy h:mm a") + : "N/A", + progress.dateCompleted + ? format(progress.dateCompleted, "MM/dd/yyyy h:mm a") + : "N/A", + progress.completedComponents.webinar ? "Complete" : "Incomplete", + progress.completedComponents.survey ? "Complete" : "Incomplete", + progress.completedComponents.certificate ? "Complete" : "Incomplete", + ]); + + const csvContent = [ + headers.join(","), + ...rows.map((row) => row.join(",")), + ].join("\n"); + + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute( + "download", + `${selectedCourse?.label}_progress_report.csv` + ); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const displayedProgress = userProgress.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + const totalPages = Math.ceil(userProgress.length / itemsPerPage); + + // Custom styles for react-select + const selectStyles = { + control: (base: any) => ({ + ...base, + minHeight: "42px", + borderRadius: "0.5rem", + borderColor: "#E5E7EB", + boxShadow: "none", + "&:hover": { + borderColor: "#8757a3", + }, + }), + option: ( + base: any, + state: { isSelected: boolean; isFocused: boolean } + ) => ({ + ...base, + backgroundColor: state.isSelected + ? "#8757a3" + : state.isFocused + ? "#F3F4F6" + : "white", + color: state.isSelected ? "white" : "#374151", + "&:active": { + backgroundColor: "#8757a3", + }, + }), + input: (base: any) => ({ + ...base, + color: "#374151", + }), + placeholder: (base: any) => ({ + ...base, + color: "#9CA3AF", + }), + singleValue: (base: any) => ({ + ...base, + color: "#374151", + }), + menu: (base: any) => ({ + ...base, + borderRadius: "0.5rem", + boxShadow: + "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", + }), + }; + + const handleEditClick = (progress: UserProgress) => { + setSelectedProgress(progress); + setEditModalOpen(true); + }; + + const [userTypes, setUserTypes] = useState<{ _id: string; name: string }[]>( + [] + ); + + useEffect(() => { + const fetchUserTypes = async () => { + try { + const response = await apiClient.get("/user-types"); + setUserTypes(response.data.data); + } catch (error) { + console.error("Error fetching user types:", error); + } + }; + fetchUserTypes(); + }, []); + + return ( +
+
+
+
+

Product Progress Report

+
+ +
+ {/* Course Search */} + {!fixedCourseId && ( +
+ +
+
+ + +
+
+ + +
+
+ ); } -export default function InPersonComponent({inPersonData, setInPersonData}:InPersonComponentProps) { - const handleChange = (e:any) => { - const { name, value } = e.target; - setInPersonData({ ...inPersonData, [name]: value }); - }; - return ( -
-
- - -
-
- - -
-
)} \ No newline at end of file diff --git a/frontend/src/pages/Admin/WorkshopCreation/OnDemand.tsx b/frontend/src/pages/Admin/WorkshopCreation/OnDemand.tsx index bde91b5..12408d5 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/OnDemand.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/OnDemand.tsx @@ -1,38 +1,59 @@ -import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; +import React, { Dispatch, SetStateAction, useEffect } from "react"; import apiClient from "../../../services/apiClient"; -import {getCleanCourseData} from "../../../store/useCourseEditStore"; +import { + getCleanCourseData, + useCourseEditStore, +} from "../../../store/useCourseEditStore"; interface OnDemandComponentProps { onDemandData: any; setOnDemandData: Dispatch>; } + +function isValidYouTubeUrl(url: string): boolean { + return /^https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+$/.test(url); +} + export default function OnDemandComponent({ onDemandData, setOnDemandData, }: OnDemandComponentProps) { - const handleChange = (e: any) => { - const { name, value } = e.target; - setOnDemandData({ ...onDemandData, [name]: value }); - }; const course = getCleanCourseData(); + const hydrated = useCourseEditStore.persist.hasHydrated(); - const createVideo = async () => { - try { - const response = await apiClient.post("/videos", { - title: course.className, - description: course.courseDescription, - videoUrl: onDemandData.embeddingLink, - courseId: course._id, - published: true, // could be toggled later - }); - } catch (error) { - console.error(error); + useEffect(() => { + if ( + hydrated && + course?.productType === "Virtual Training - On Demand" && + typeof course.productInfo === "string" && + isValidYouTubeUrl(course.productInfo) + ) { + setOnDemandData((prev: any) => ({ + ...prev, + embeddingLink: course.productInfo, + })); } + }, [hydrated, course.productInfo, course.productType, setOnDemandData]); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setOnDemandData((prev: any) => ({ ...prev, [name]: value })); }; + + // const createVideo = async () => { + // try { + // await apiClient.put(`/courses/${course._id}`, { + // productType: "Virtual Training - On Demand", + // productInfo: onDemandData.embeddingLink, + // }); + // } catch (error) { + // console.error(error); + // } + // }; + return ( -
- +
+ - + {/* */}
); } diff --git a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx index f2802c2..e52e784 100644 --- a/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx +++ b/frontend/src/pages/Admin/WorkshopCreation/WorkshopCreation.tsx @@ -9,11 +9,12 @@ import NewMeeting from "./ModalComponents/NewMeeting"; import ExistingWebinarList from "./ModalComponents/ExistingWebinarList"; import NewWebinar from "./ModalComponents/NewWebinar"; import SaveCourseButton from "../../../components/SaveCourseButtons"; -import apiClient from "../../../services/apiClient"; -import { getCleanCourseData } from "../../../store/useCourseEditStore"; +import { + getCleanCourseData, + useCourseEditStore, +} from "../../../store/useCourseEditStore"; export default function WorkshopCreation() { - const [webinarData, setWebinarData] = useState({ serviceType: "webinar", meetingURL: "string", @@ -27,7 +28,7 @@ export default function WorkshopCreation() { const [inPersonData, setInPersonData] = useState({ serviceType: "in-person", startTime: null, - duration: 0, + duration: "", location: "", }); @@ -36,7 +37,9 @@ export default function WorkshopCreation() { embeddingLink: "", }); - const [openModal, setOpenModal] = useState<"NewWebinar" | "ExistingWebinar" | "NewMeeting" | "ExistingMeeting" | null>(null); + const [openModal, setOpenModal] = useState< + "NewWebinar" | "ExistingWebinar" | "NewMeeting" | "ExistingMeeting" | null + >(null); const handleChange = (e: any) => { const { name, value } = e.target; @@ -44,23 +47,18 @@ export default function WorkshopCreation() { }; const course = getCleanCourseData(); - console.log(course) function getInitialType() { - if(course.productType === "Virtual Training - On Demand"){ - return "on demand" - } - else if(course.productType === "Virtual Training - Live Meeting"){ - return "meeting" - } - else if(course.productType === "Virtual Training - Live Webinar"){ - return "webinar" - } - else if(course.productType === "In-Person Training"){ - return "in-person" - } - else{ - return "meeting" + if (course.productType === "Virtual Training - On Demand") { + return "on demand"; + } else if (course.productType === "Virtual Training - Live Meeting") { + return "meeting"; + } else if (course.productType === "Virtual Training - Live Webinar") { + return "webinar"; + } else if (course.productType === "In-Person Training") { + return "in-person"; + } else { + return "meeting"; } } @@ -76,11 +74,156 @@ export default function WorkshopCreation() { minTime: 0, }); + useEffect(() => { + const mappedType = (() => { + switch (course.productType) { + case "Virtual Training - On Demand": + return "on-demand"; + case "Virtual Training - Live Meeting": + return "meeting"; + case "Virtual Training - Live Webinar": + return "webinar"; + case "In-Person Training": + return "in-person"; + default: + return "meeting"; + } + })(); + + // Only update type if it's not already set (prevents override if user changes it) + if (!formData.type) { + setFormData((prev) => ({ + ...prev, + type: mappedType, + })); + } + }, [course.productType]); + const handleSubmit = (event: any) => { event.preventDefault(); console.log("Form submitted:", formData); }; + const setField = useCourseEditStore((state) => state.setField); + + const [hasHydratedFormData, setHasHydratedFormData] = useState(false); + + useEffect(() => { + if (!hasHydratedFormData) { + // Hydrate formData type + const mappedType = (() => { + switch (course.productType) { + case "Virtual Training - On Demand": + return "on-demand"; + case "Virtual Training - Live Meeting": + return "meeting"; + case "Virtual Training - Live Webinar": + return "webinar"; + case "In-Person Training": + return "in-person"; + default: + return "meeting"; + } + })(); + + setFormData((prev) => ({ + ...prev, + type: mappedType, + })); + + // Hydrate type-specific data + if ( + mappedType === "in-person" && + typeof course.productInfo === "string" + ) { + try { + const parsed = JSON.parse(course.productInfo); + setInPersonData({ + serviceType: "in-person", + startTime: parsed.startTime || "", + duration: + parsed.duration !== undefined ? parsed.duration.toString() : "", + location: parsed.location || "", + }); + } catch (e) { + console.error("Failed to parse in-person data", e); + } + } + + if ( + mappedType === "on-demand" && + typeof course.productInfo === "string" + ) { + setOnDemandData({ + serviceType: "on demand", + embeddingLink: course.productInfo || "", + }); + } + + if (mappedType === "meeting") { + setMeetingData({ + serviceType: "meeting", + meetingID: course.productInfo || "", + }); + } + + if (mappedType === "webinar") { + setWebinarData({ + serviceType: "webinar", + meetingURL: course.productInfo || "", + }); + } + + setHasHydratedFormData(true); // ✅ lock hydration + } + }, [hasHydratedFormData, course]); + + // Watch onDemandData + useEffect(() => { + if (formData.type === "on-demand") { + setField("productType", "Virtual Training - On Demand"); + setField("productInfo", onDemandData.embeddingLink || ""); + } + }, [onDemandData, formData.type]); + + // Watch meetingData + useEffect(() => { + if (formData.type === "meeting") { + setField("productType", "Virtual Training - Live Meeting"); + setField("productInfo", meetingData.meetingID || ""); + } + }, [meetingData, formData.type]); + + // Watch webinarData + useEffect(() => { + if (formData.type === "webinar") { + setField("productType", "Virtual Training - Live Webinar"); + setField("productInfo", webinarData.meetingURL || ""); + } + }, [webinarData, formData.type]); + + // Watch inPersonData + useEffect(() => { + console.log("formData.type:", JSON.stringify(inPersonData)); + if (formData.type === "in-person") { + setField("productType", "In-Person Training"); + setField( + "productInfo", + JSON.stringify(inPersonData) // duration stays string + ); + } + }, [inPersonData, formData.type]); + + const productType = useCourseEditStore((state) => state.productType); + const productInfo = useCourseEditStore((state) => state.productInfo); + + useEffect(() => { + console.log("🧠 Zustand updated:", { + productType, + productInfo, + }); + }, [productType, productInfo]); + return (
@@ -115,8 +258,7 @@ export default function WorkshopCreation() { value="meeting" checked={formData.type === "meeting"} onChange={handleChange} - required - />{" "} + /> Meeting
@@ -163,7 +306,7 @@ export default function WorkshopCreation() { inPersonData={inPersonData} setInPersonData={setInPersonData} /> - ): formData.type === "meeting" ? ( + ) : formData.type === "meeting" ? ( )}
- + {/* New Meeting Modal */} @@ -229,4 +375,4 @@ export default function WorkshopCreation() {
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/CartPage/cart.tsx b/frontend/src/pages/CartPage/cart.tsx index 039b8cf..941e4ca 100644 --- a/frontend/src/pages/CartPage/cart.tsx +++ b/frontend/src/pages/CartPage/cart.tsx @@ -31,7 +31,7 @@ export default function Cart() { }; return ( -
+

Checkout

{cartItems.map((item, index) => ( diff --git a/frontend/src/pages/Catalog/Catalog.tsx b/frontend/src/pages/Catalog/Catalog.tsx index 3c8e7cc..822a7e9 100644 --- a/frontend/src/pages/Catalog/Catalog.tsx +++ b/frontend/src/pages/Catalog/Catalog.tsx @@ -29,26 +29,50 @@ export default function Catalog({ setCartItemCount }: CatalogProps) { return storedUser ? JSON.parse(storedUser).cart || [] : []; }); - // Read query parameters from the URL and apply filters useEffect(() => { - async function fetchData() { + const fetchData = async () => { try { setLoading(true); - const response = await apiClient.get("/courses"); - console.log(response.data.data) - let courses = response.data.data.filter((course:Course) => new Date(course.regStart).getTime() < new Date().getTime()); - setCourses(courses); - setFilteredCourses(courses); - setLoading(false); + + const user = JSON.parse(localStorage.getItem("user") || "{}"); + const userId = user._id; + + // Get all open courses + const courseResponse = await apiClient.get("/courses"); + const openCourses: Course[] = courseResponse.data.data.filter( + (course: Course) => + new Date(course.regStart).getTime() < new Date().getTime() + ); + + // Get user's progresses (i.e. registered courses) + const progressResponse = await apiClient.get( + `/progress/progress/${userId}` + ); + const registeredCourseIds = new Set( + progressResponse.data.progresses.map((p: any) => p.course._id) + ); + + // Filter out courses the user is already registered for + const unregisteredCourses = openCourses.filter( + (course) => !registeredCourseIds.has(course._id) + ); + + setCourses(unregisteredCourses); + setFilteredCourses(unregisteredCourses); } catch (error) { - console.error(error); + console.error("Error loading catalog:", error); + } finally { + setLoading(false); } - } + }; + fetchData(); }, []); + // Initialize cart state from localStorage when the component mounts useEffect(() => { - const storedCart = JSON.parse(localStorage.getItem("user") || "{}").cart || []; + const storedCart = + JSON.parse(localStorage.getItem("user") || "{}").cart || []; setCart(storedCart); // Initialize cart state with stored data setCartItemCount(storedCart.length); // Set cart item count based on localStorage cart }, []); // Empty dependency array means it only runs once when the component mounts @@ -81,8 +105,8 @@ export default function Catalog({ setCartItemCount }: CatalogProps) { filtered = filtered.filter((course) => course.ratings.length > 0 ? course.ratings.reduce((sum, r) => sum + r.rating, 0) / - course.ratings.length >= - parseInt(selectedRating) + course.ratings.length >= + parseInt(selectedRating) : false ); } @@ -95,7 +119,9 @@ export default function Catalog({ setCartItemCount }: CatalogProps) { if (selectedFormat !== "All") { filtered = filtered.filter((course) => - selectedFormat === "Live" ? course.productType !== "Virtual Training - On Demand" : course.productType === "Virtual Training - On Demand" + selectedFormat === "Live" + ? course.productType !== "Virtual Training - On Demand" + : course.productType === "Virtual Training - On Demand" ); } diff --git a/frontend/src/pages/Catalog/CatalogCourseComponent.tsx b/frontend/src/pages/Catalog/CatalogCourseComponent.tsx index 7db6d9d..1e66fce 100644 --- a/frontend/src/pages/Catalog/CatalogCourseComponent.tsx +++ b/frontend/src/pages/Catalog/CatalogCourseComponent.tsx @@ -82,7 +82,7 @@ export default function CatalogCourseComponent({
{/* Description */} -

{course.courseDescription}

+

{course.discussion}

{/* Instructor */}

diff --git a/frontend/src/pages/Dashboard/dashboard.tsx b/frontend/src/pages/Dashboard/dashboard.tsx index 9687bfd..5d1873d 100644 --- a/frontend/src/pages/Dashboard/dashboard.tsx +++ b/frontend/src/pages/Dashboard/dashboard.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { Course } from "../../shared/types/course"; import { fetchUserProgress } from "../../services/progressService"; import DashboardItem from "./dashboard-item"; +import { Link } from "react-router-dom"; export default function Dashboard() { const [courses, setCourses] = useState([]); @@ -17,10 +18,9 @@ export default function Dashboard() { setLoading(true); const progresses = (await fetchUserProgress()).progresses; console.log(progresses); - setIncompleteCourses( - progresses.filter((p: any) => !p.course.isComplete) - ); - setCompletedCourses(progresses.filter((p: any) => p.course.isComplete)); + setIncompleteCourses(progresses.filter((p: any) => !p.isComplete)); + setCompletedCourses(progresses.filter((p: any) => p.isComplete)); + console.log(progresses); setLoading(false); } catch (error) { console.error(error); @@ -57,16 +57,23 @@ export default function Dashboard() { {!loading ? ( completedCourses.length > 0 ? (

    - {completedCourses.map((course) => ( + {completedCourses.map((progress) => (
  • -

    {course.className}

    -

    {course.description}

    -

    - Instructor: {course.instructor} +

    + {progress.course.className} +

    +

    + Instructor: {progress.course.instructor}

    + + Enter Course +
  • ))}
diff --git a/frontend/src/pages/UserAuth/Register.tsx b/frontend/src/pages/UserAuth/Register.tsx index f134f0e..e472a89 100644 --- a/frontend/src/pages/UserAuth/Register.tsx +++ b/frontend/src/pages/UserAuth/Register.tsx @@ -155,10 +155,22 @@ const Register: React.FC = () => { const [userTypes, setUserTypes] = useState([]); + // const fetchUserTypes = async () => { + // try { + // const response = await apiClient.get("/user-types"); + // setUserTypes(response.data.data); + // } catch (error) { + // console.error(error); + // } + // }; + const fetchUserTypes = async () => { try { const response = await apiClient.get("/user-types"); - setUserTypes(response.data.data); + const filtered = response.data.data.filter( + (ut: UserType) => ut.name !== "Staff" + ); + setUserTypes(filtered); } catch (error) { console.error(error); } diff --git a/frontend/src/pages/courseDetailPage/SurveyModal.tsx b/frontend/src/pages/courseDetailPage/SurveyModal.tsx index f618dba..f92d621 100644 --- a/frontend/src/pages/courseDetailPage/SurveyModal.tsx +++ b/frontend/src/pages/courseDetailPage/SurveyModal.tsx @@ -1,102 +1,154 @@ import apiClient from "../../services/apiClient"; -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import SurveyQuestion from "./SurveyQuestion"; interface SurveyModalProps { - isOpen: boolean, - onClose: any, - surveyId: string + isOpen: boolean; + onClose: any; + courseId: string | null; + setSurveyCompleted: any; } -export default function SurveyModal({ isOpen, onClose, surveyId }:SurveyModalProps){ - const [questionNumber, setQuestionNumber] = useState(0); - const [surveyQuestions, setSurveyQuestions] = useState([]); - const [responses, setResponses] = useState<{ [key: string]: string }>({}); - - useEffect(() => { - const populateQuestions = async () => { - let tempQuestions: any[] = [] - try { - const questionIds = (await apiClient.get("surveys")).data.questions - console.log(questionIds) - setSurveyQuestions(questionIds) - } catch (error) { - console.error("Error: ", error); - } - }; - - populateQuestions(); - }, []); - - if (!isOpen) return null; - - async function addQuestion() { - try { - const response = await apiClient.post("survey", { - question: "Do you have any other feedback for the course?", - isMCQ: false, - }); - console.log(response.status) - } catch (error) { - console.error("Failed to check admin status", error); - } - } - - const handleAnswerChange = (questionId: string, answer: string) => { - setResponses((prev) => ({ - ...prev, - [questionId]: answer, - })); - }; - - - async function handleNextQuestion() { - if (questionNumber < surveyQuestions.length - 1) { - setQuestionNumber(questionNumber + 1); - } else { - let responseIds = [] - for(let id of Object.keys(responses)){ - const response = await apiClient.post("questionResponses", { - userId: JSON.parse(localStorage.user)._id, - questionId: id, - answer: (responses[`${id}`]).toString(), - }) - responseIds.push(response.data._id) - } - const response = await apiClient.post("surveyResponses", { - userId: JSON.parse(localStorage.user)._id, - answers: responseIds, - }) - - let surveyResponseId = response.data._id - // Beloved EM please add the updating the course to append the survey response once the other changes from Kevin is added for this. - - onClose(); - } - } - - return ( -
-
-

Survey

-
- {surveyQuestions.length > 0 ? ( - ) : ( -

Loading questions...

- )} -
- -
-
- ); -}; +export default function SurveyModal({ + isOpen, + onClose, + courseId, + setSurveyCompleted, +}: SurveyModalProps) { + const [questionNumber, setQuestionNumber] = useState(0); + const [surveyQuestions, setSurveyQuestions] = useState([]); + const [responses, setResponses] = useState<{ [key: string]: string }>({}); + useEffect(() => { + const populateQuestions = async () => { + let tempQuestions: any[] = []; + try { + const questionIds = (await apiClient.get("surveys")).data.questions; + console.log(questionIds); + setSurveyQuestions(questionIds); + } catch (error) { + console.error("Error: ", error); + } + }; + + populateQuestions(); + }, []); + + if (!isOpen) return null; + + // async function addQuestion() { + // try { + // const response = await apiClient.post("survey", { + // question: "Do you have any other feedback for the course?", + // isMCQ: false, + // }); + // console.log(response.status); + // } catch (error) { + // console.error("Failed to check admin status", error); + // } + // } + + const handleAnswerChange = (questionId: string, answer: string) => { + setResponses((prev) => ({ + ...prev, + [questionId]: answer, + })); + }; + + async function handleNextQuestion() { + if (questionNumber < surveyQuestions.length - 1) { + setQuestionNumber(questionNumber + 1); + } else { + try { + let responseIds = []; + for (let id of Object.keys(responses)) { + const response = await apiClient.post("questionResponses", { + userId: JSON.parse(localStorage.user)._id, + questionId: id, + answer: responses[`${id}`].toString(), + }); + responseIds.push(response.data._id); + } + const response = await apiClient.post("surveyResponses", { + userId: JSON.parse(localStorage.user)._id, + answers: responseIds, + }); + + // let surveyResponseId = response.data._id + // Beloved EM please add the updating the course to append the survey response once the other changes from Kevin is added for this. + + const progressResponse = await apiClient.put( + `/courses/${courseId}/progress/single/${JSON.parse(localStorage.user)._id}`, + { + surveyComplete: true, + webinarComplete: true, + } + ); + setSurveyCompleted(true); + console.log("progress updated"); + } catch (error) { + console.error(error); + } + + onClose(); + } + } + + function handlePreviousQuestion() { + if (questionNumber > 0) { + setQuestionNumber(questionNumber - 1); + } + } + + const currentQuestionId = surveyQuestions[questionNumber]?._id ?? null; + const isCurrentAnswered = !!( + currentQuestionId && (responses[currentQuestionId] ?? "").trim() !== "" + ); + + return ( +
+
+ +

Survey

+
+ {surveyQuestions.length > 0 ? ( + + ) : ( +

Loading questions...

+ )} +
+ + + + +
+
+ ); +} diff --git a/frontend/src/pages/courseDetailPage/courseDetailsPage.tsx b/frontend/src/pages/courseDetailPage/courseDetailsPage.tsx index 71efcae..d1ac0f1 100644 --- a/frontend/src/pages/courseDetailPage/courseDetailsPage.tsx +++ b/frontend/src/pages/courseDetailPage/courseDetailsPage.tsx @@ -10,6 +10,9 @@ import { Course } from "../../shared/types/course"; import { Rating } from "../../shared/types/rating"; import apiClient from "../../services/apiClient"; import SurveyModal from "./SurveyModal"; +import YouTube from "react-youtube"; +import { Progress } from "../../shared/types/progress"; +import { ZoomMeeting } from "../../shared/types"; const FaStar = _FaStar as ComponentType<{ size?: number; color?: string }>; const FaStarHalfAlt = _FaStarHalfAlt as ComponentType<{ @@ -49,95 +52,65 @@ const CoursePage = ({ setCartItemCount }: CatalogProps) => { const navigate = useNavigate(); const [courseDetailsData, setCourseDetailsData] = useState( null - // { - // _id: "", - // className: "Introduction to Computer Science", - // courseDescription: `When it comes to your child's case closing, are you hearing terms or phrases like, "least drastic alternative", APR, RGAP, intervention, etc. and feeling lost in the acronyms and language? Are you facing an APR and worried about ongoing support or post-permanency legal ramifications? Do you find yourself feeling unsure about how to advocate for your rights or desires in a potential APR? Are you wondering if you have to accept an APR? Has your county told you the requirements to qualify for RGAP (post-APR financial assistance)? If you answered yes to any of these questions, join us as attorney, Tim Eirich, helps us make sense of all things APR and RGAP! - // - // Hours earned: 2.0 - // - // Feedback from this class: - // - // "Incredibly helpful information. This should be required so that all foster parents are informed and not taken advantage of." - // - // "Tim was EXCELLENT and provided insight into complicated legal matters." - // - // "All of Tim's trainings are excellent, and I'm grateful that he partners with Foster Source to equip foster and kinship parents with the knowledge that they need to advocate for themselves and the children in their care."`, - // instructorName: "Dr. Alice Johnson", - // creditNumber: 3, - // discussion: "An interactive discussion about computational thinking.", - // components: ["Lectures", "Labs", "Quizzes"], - // handouts: ["syllabus.pdf", "lecture1.pdf", "assignment1.pdf"], - // ratings: [], - // isLive: false, - // cost: 100, - // categories: ["Technology", "Category", "Misc"], - // thumbnailPath: "", - // instructorDescription: `Sarah has her degree in social work from Metropolitan State University with an emphasis in child and adolescent mental health. - // - // She has worked for Denver Department of Human Services Child Welfare for over 3 years as an ongoing social caseworker and currently holds a senior caseworker position in placement navigation. She has worked as a counselor at a residential treatment program for youth corrections, as a counselor for dual diagnosis adult men at a halfway house, and an independent living specialist for the disabled community/outreach specialist for individuals experiencing homelessness. - // - // Sarah writes: - // - // In my spare time, I spend most of my time with my two teenage daughters. I am a huge advocate for social justice issues which I spend a lot of my time supporting through peaceful protests, education, volunteer work, etc. I love camping, crafting, karaoke, road trip adventures, and dancing in my living room. My favorite place in the entire world is the Mojave Desert.`, - // instructorRole: "Moderator", - // lengthCourse: 2, - // time: new Date("2025-10-15T00:00:00.000Z"), - // isInPerson: true, - // students: [], - // regStart: new Date("2025-10-10T00:00:00.000Z"), - // regEnd: new Date("2025-10-12T00:00:00.000Z"), - // } ); const [starRating, setStarRating] = useState(-1); const [isAdded, setIsAdded] = useState(false); const [ratingsPageOpen, setRatingsPageOpen] = useState(false); const [numStarsRatingPage, setNumStarsRatingpage] = useState(0); - const [isAdmin, setIsAdmin] = useState(false); + const [hasProgress, setHasProgress] = useState(null); + const [progress, setProgress] = useState(null); + const [workshopCompleted, setWorkshopCompleted] = useState(false); + const [surveyCompleted, setSurveyCompleted] = useState(false); + const [certificateCompleted, setCertificateCompleted] = + useState(false); useEffect(() => { - const checkAdminStatus = async () => { + const fetchProgress = async () => { + if (!courseId || !user?._id) return; try { - const response = await apiClient.get("users/is-admin", { - headers: { - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - }); - setIsAdmin(response.data.isAdmin); + const response = await apiClient.get( + `/courses/${courseId}/progress/single/${user._id}` + ); + const progress = response.data.progress; + if (!response.data.progress) { + setHasProgress(false); + } else { + setHasProgress(true); + setProgress(progress); + setWorkshopCompleted(progress.completedComponents.webinar); + setSurveyCompleted(progress.completedComponents.survey); + setCertificateCompleted(progress.completedComponents.certificate); + console.log("progress", response.data); + } } catch (error) { - console.error("Failed to check admin status", error); + setHasProgress(false); + console.error("Error checking progress:", error); } }; - checkAdminStatus(); - }, []); + fetchCourse(); + checkCourseInCart(); + fetchProgress(); + }, [courseId]); + + const productType = courseDetailsData?.productType; const navigateToCourseEdit = () => { - navigate(`/courses/edit`); // Change to the desired route + navigate(`/courses/edit`); }; - //================ Working axios request ====================== const fetchCourse = async () => { if (!courseId) return; try { const response = await apiClient.get(`courses/${courseId}`); response.data.data.time = new Date(response.data.data.time); + console.log(response.data.data); setCourseDetailsData(response.data.data); } catch (error) { console.error(error); } }; - // useEffect(() => { - // const id = queryParams.get("courseId"); - // setCourseId(id || ""); - // }, [location.search]); - - useEffect(() => { - fetchCourse(); - checkCourseInCart(); - }, [courseId]); - useEffect(() => { if (courseDetailsData) { if (courseDetailsData.ratings.length !== 0) { @@ -193,12 +166,17 @@ const CoursePage = ({ setCartItemCount }: CatalogProps) => {
- + {!hasProgress ? ( + + ) : ( +
+ )} +

@@ -206,20 +184,20 @@ const CoursePage = ({ setCartItemCount }: CatalogProps) => {

{/* Course Details + Rating */} -
+
{/* Stars */} -

+ {/*

{starRating === -1 ? ( "No ratings yet" ) : ( )} -

+

*/}

{courseDetailsData.creditNumber} Credits

-

+ {/*

Live Web Event{" "} {new Date(courseDetailsData.time).toLocaleDateString()} at{" "} {new Date(courseDetailsData.time).toLocaleTimeString( @@ -230,7 +208,7 @@ const CoursePage = ({ setCartItemCount }: CatalogProps) => { hour12: true, } )} -

+

*/}
@@ -238,27 +216,36 @@ const CoursePage = ({ setCartItemCount }: CatalogProps) => {
- + {/* {!hasProgress ? ( + + ) : ( + <> + )} */} + + {/* {hasProgress ? ( + + ) : ( + <> + )} */} - {/* Pop-Up Modal */} {ratingsPageOpen && (
@@ -323,26 +310,19 @@ const CoursePage = ({ setCartItemCount }: CatalogProps) => {
)} - {isAdmin && ( - - )}

-
+
{/*Overview Rectangle*/}
-

Overview

-

+

Overview

+

{courseDetailsData.courseDescription .split("\n") .map((line, index) => ( @@ -353,37 +333,51 @@ const CoursePage = ({ setCartItemCount }: CatalogProps) => { {/* Content overview */}

-

Content(s)

+

Content

{/*Speaker discription rectangle*/}
-
Speaker(s)
+
Speaker(s)
-
-
- {`A -
-
- {courseDetailsData.instructorName} -
- - {/*Needs to be complete*/} -
- {courseDetailsData.instructorRole} + {courseDetailsData.speakers.map((speaker, index) => ( +
+
+ {`A +
+
+ {(speaker as any).name} +
+
+ {(speaker as any).title} +
+
+ {(speaker as any).bio} +
- -
+ ))}

{courseDetailsData.instructorDescription .split("\n") @@ -404,8 +398,8 @@ const CategoryPills = ({ categories }: { categories: string[] }) => {

    {categories.map((category, index) => (
  • -
    -

    +

    +

    {category}

    @@ -421,35 +415,85 @@ const DisplayBar = ({ isSurveyModalOpen, setIsSurveyModalOpen, productInfo, + productType, + hasProgress, + workshop, + survey, + setWorkshopCompleted, + setSurveyCompleted, + setCertificateCompleted, + certificate, + courseName, + creditHours, }: { creditHours: number; time: Date; isSurveyModalOpen: boolean; setIsSurveyModalOpen: any; productInfo: string; + productType: string; + hasProgress: boolean | null; + workshop: boolean; + survey: boolean; + certificate: boolean; + setWorkshopCompleted: any; + setSurveyCompleted: any; + setCertificateCompleted: any; + courseName: string; }) => { - const [currentPage, setCurrentPage] = useState("Webinar"); + const [currentPage, setCurrentPage] = useState("Workshop"); const [surveyColor, setSurveyColor] = useState("#D9D9D9"); const [certificateColor, setCertificateColor] = useState("#D9D9D9"); - const [survey, setSurvey] = useState(false); const [videoLink, setVideoLink] = useState(""); + const [videoCompleted, setVideoCompleted] = useState(false); + const [meeting, setMeeting] = useState(null); - useEffect(() => { - const webinarEnd = new Date(time); - webinarEnd.setHours(webinarEnd.getHours() + 2); // 2 hours after the current time - const checkTime = () => { - const currentTime = new Date(); - if (currentTime.getTime() > webinarEnd.getTime()) { - setSurvey(true); + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + const courseId = searchParams.get("courseId"); + + const user = JSON.parse(localStorage.getItem("user") || "{}"); + + const onPlayerReady = (event: any) => { + event.target.playVideo(); + }; + + const onStateChange = async (event: any) => { + console.log(event.data); + if (event.data === 0) { + setVideoCompleted(true); + + try { + const response = await apiClient.put( + `/courses/${courseId}/progress/single/${user._id}`, + { + webinarComplete: true, + } + ); + console.log("✅ Webinar completion updated:", response.data); + setWorkshopCompleted(true); + } catch (err) { + console.error("❌ Failed to update webinar progress:", err); } - }; + } + }; + + // useEffect(() => { + // const webinarEnd = new Date(time); + // webinarEnd.setHours(webinarEnd.getHours() + 2); // 2 hours after the current time + // const checkTime = () => { + // const currentTime = new Date(); + // if (currentTime.getTime() > webinarEnd.getTime()) { + // setSurvey(true); + // } + // }; - checkTime(); // Run immediately when component mounts + // checkTime(); // Run immediately when component mounts - const interval = setInterval(checkTime, 1000 * 60); // Run every 1 minute + // const interval = setInterval(checkTime, 1000 * 60); // Run every 1 minute - return () => clearInterval(interval); // Cleanup when component unmounts - }, []); + // return () => clearInterval(interval); // Cleanup when component unmounts + // }, []); const testNetwork = async () => { try { @@ -467,7 +511,53 @@ const DisplayBar = ({ }; /* TODO: Needs to be complete once certificate page is out */ - const handleAccessCertificate = () => {}; + const handleAccessCertificate = async () => { + if (!certificate) { + setCertificateCompleted(true); + + try { + await apiClient.put( + `/courses/${courseId}/progress/single/${user._id}`, + { + webinarComplete: true, + surveyComplete: true, + certificateComplete: true, + } + ); + } catch (error) { + console.error("Error updating progress after certificate:", error); + } + } + + try { + const response = await apiClient.post( + "/certificatePDFs", + { + certificateType: "completion", // or "attendance" + participantName: user.name, + courseInfo: `${courseName} (${creditHours} credits)`, + completionDate: new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }), + }, + { + responseType: "blob", // IMPORTANT for binary PDF data + } + ); + + // Create a blob URL and trigger download + const blob = new Blob([response.data], { type: "application/pdf" }); + const link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = "certificate.pdf"; + link.click(); + } catch (error) { + console.error("Error generating certificate:", error); + alert("Failed to generate certificate. Please try again."); + } + }; const getYouTubeEmbedUrl = (url: string) => { const match = url.match( @@ -477,162 +567,324 @@ const DisplayBar = ({ return videoId ? `https://www.youtube.com/embed/${videoId}` : null; }; - const retrieveVideo = async () => { + const getYoutubeVideoId = (url: string): string => { + if (!url) return ""; + + const regex = + /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; + const match = url.match(regex); + return match ? match[1] : ""; + }; + + const retrieveVideo = () => { try { - const response = await apiClient.get(`/videos`, { - params: { - _id: productInfo, - }, - }); - setVideoLink(getYouTubeEmbedUrl(response.data.data[0].videoUrl)); + setVideoLink(getYouTubeEmbedUrl(productInfo)); } catch (error) { console.error(error); } }; + const retrieveMeeting = async () => { + if ( + productType === "Virtual Training - Live Meeting" || + productType === "Virtual Training - Live Webinar" + ) + try { + const response = await apiClient.get("zoom/meetings"); + const meetings = response.data.meetings; + + const matchingMeeting = meetings.find( + (meeting: any) => String(meeting.id) === String(productInfo) + ); + + if (matchingMeeting) { + setMeeting(matchingMeeting); + } else { + console.warn("No matching meeting found for ID:", productInfo); + } + } catch (error) { + console.error(error); + } + }; + + function formatMeetingTime(dateStr: string, timezone: string) { + try { + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + }); + return formatter.format(new Date(dateStr)); + } catch (error) { + console.error("Invalid time zone:", timezone, error); + // Fallback to local time + return new Date(dateStr).toLocaleString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + }); + } + } + useEffect(() => { retrieveVideo(); + retrieveMeeting(); }, [productInfo]); - return ( -
    -
    - {/* Workshop -> Survey -> Certificate */} -
    - {/* Workshop Button */} - - {/* Survey Button */} - - {/* Certificate Button */} - + {/* Survey Button */} + -
    - {currentPage === "Workshop" && ( -
    - {/*
    -
    Date: {new Date(time).toLocaleDateString()}
    -
    - Time:{" "} - {new Date(time).toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - })} -
    -
    Length: {lengthCourse} hours
    -
    -
    - - - - -
    */} - -
    - )} - {currentPage === "Survey" && ( -
    -
    -

    - Complete webinar to access survey + Certificate

    - - setIsSurveyModalOpen(false)} - surveyId={"67d79d830a42d191ebb55049"} - > -
    +
    - )} - {currentPage === "Certificate" && ( -
    - Once you have completed the course, your certificate will be - accessible here. -
    -

    - Complete webinar to access certificate -

    - + {currentPage === "Workshop" && ( +
    + {productType === "Virtual Training - On Demand" ? ( +
    + {/* */} + +
    + ) : productType === "Virtual Training - Live Meeting" ? ( +
    +

    Meeting ID: {productInfo}

    +

    + Meeting Date & Time:{" "} + {formatMeetingTime( + meeting?.start_time || "", + meeting?.timezone || "" + )} + , Timezone: {meeting?.timezone} +

    + + Join Zoom Meeting + +
    + ) : productType === "Virtual Training - Live Webinar" ? ( +
    +

    + Meeting Date & Time:{" "} + {formatMeetingTime( + meeting?.start_time || "", + meeting?.timezone || "" + )} + , Timezone: {meeting?.timezone} +

    +

    Webinar URL:

    + + Join Webinar + +
    + ) : productType === "In-Person Training" ? ( + (() => { + try { + const parsed = JSON.parse(productInfo); + return ( +
    +

    + Location:{" "} + + {parsed.location} + +

    +

    + Start Time:{" "} + {new Date(parsed.startTime).toLocaleString()} +

    +

    Duration: {parsed.duration} minutes

    +
    + ); + } catch (e) { + return ( +

    + Invalid in-person course info. +

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

    Unknown course type.

    + )} + {workshop ? ( +

    + Completed! Please proceed to survey +

    + ) : ( + <> + )}
    -
    - )} + )} + {currentPage === "Survey" && ( +
    +
    +

    + Complete workshop to access survey. (For in person events, + this may take a couple of days to update.) +

    + + setIsSurveyModalOpen(false)} + courseId={courseId} + setSurveyCompleted={setSurveyCompleted} + > +
    +
    + )} + {currentPage === "Certificate" && ( +
    + Once you have completed the course, your certificate will be + accessible here. +
    +

    + Complete webinar to access certificate +

    + +
    +
    + )} +
    -
    - ); + ); + else return
    ; }; /* Displays the stars, credit numbers, and live event time */ diff --git a/frontend/src/routes/RoutesAndLayouts.tsx b/frontend/src/routes/RoutesAndLayouts.tsx index 0c69ee1..6e27c8b 100644 --- a/frontend/src/routes/RoutesAndLayouts.tsx +++ b/frontend/src/routes/RoutesAndLayouts.tsx @@ -9,7 +9,6 @@ import Login from "../pages/UserAuth/Login"; import Register from "../pages/UserAuth/Register"; import ResetPassword from "../pages/UserAuth/resetPassword"; import ResetPasswordForm from "../pages/UserAuth/resetPasswordForm"; -import authService from "../services/authService"; import CoursePage from "../pages/courseDetailPage/courseDetailsPage"; import DiscountPage from "../pages/Admin/DiscountPage/Discount"; import SpeakerPage from "../pages/Admin/SpeakerPage/Speaker"; @@ -21,16 +20,12 @@ import ComponentPage from "../pages/Admin/ComponentPage/Component"; import SurveyPage from "../pages/Admin/SurveyPage/Survey"; import WorkshopCreation from "../pages/Admin/WorkshopCreation/WorkshopCreation"; import RegistrationPage from "../pages/Admin/RegistrationPage/RegistrationPage"; -import AdminPage from "../pages/Admin/AdminPage"; -import path from "path"; import EmailPage from "../pages/Admin/EmailPage/EmailPage"; -import apiClient from "../services/apiClient"; import { AdminSidebar } from "../components/AdminSidebar/AdminSidebar"; import EditCourse from "../pages/Admin/EditCoursePage/editCoursePage"; import EditSideBar from "../components/EditCourseSidebar/editCoursePageSideBar"; import Registrants from "../pages/Admin/RegistrantsPage/Registrants"; import SurveySummary from "../pages/Admin/SurveySummaryPage/SurveySummary"; -import CourseManagerPage from "../pages/Admin/CourseManagerPage/CourseManagerPage"; import UserManagementPage from "../pages/Admin/UserManagementPage/Users"; import UserTypesPage from "../pages/Admin/UserTypesPage/UserTypesPage"; import ProductProgressReport from "../pages/Admin/ProductSummaryPage/ProductProgressReport"; @@ -63,7 +58,7 @@ function RoutesAndLayout({ PrivateRoute: any; AdminRoute: any; }) { - const location = useLocation(); // ✅ safe now + const location = useLocation(); const isAuthRoute = location.pathname.startsWith("/login") || @@ -87,12 +82,12 @@ function RoutesAndLayout({ ) : (
    -
    + {/*
    -
    +
    */}
    )}
    } /> } /> - } /> } /> } /> - } /> - } /> } /> } /> - } /> } /> } /> + + + + + } + > + } /> + } /> } /> } /> + } + /> {/* ===== user management routes ===== */} @@ -319,7 +325,7 @@ function RoutesAndLayout({ path="admin/reports/progress" element={ - + } /> diff --git a/frontend/src/shared/types.tsx b/frontend/src/shared/types.tsx index 2bd1093..9bd3042 100644 --- a/frontend/src/shared/types.tsx +++ b/frontend/src/shared/types.tsx @@ -34,3 +34,12 @@ export interface UserType { userCount: number; cost: number; } + +export interface ZoomMeeting { + duration: number; // in minutes + id: number; // Zoom meeting ID (integer) + join_url: string; + start_time: string; // ISO timestamp + timezone: string; // e.g., "America/Denver" + topic: string; +} diff --git a/frontend/src/shared/types/course.tsx b/frontend/src/shared/types/course.tsx index 89c3778..adcb3ce 100644 --- a/frontend/src/shared/types/course.tsx +++ b/frontend/src/shared/types/course.tsx @@ -2,23 +2,33 @@ import { Rating } from "./rating"; import { User } from "./user"; export type Course = { + _id: string; handouts: string[]; ratings: Rating[]; className: string; discussion: string; + isLive: boolean; categories: string[]; creditNumber: number; courseDescription: string; thumbnailPath: string; - _id: string; + bannerPath: string; cost: number; instructorName: string; instructorDescription: string; instructorRole: string; time: Date; students: string[]; + managers: string[]; + speakers: string[]; regStart: Date; regEnd: Date; - productType: string; + productType: + | "Virtual Training - Live Meeting" + | "In-Person Training" + | "Virtual Training - On Demand" + | "Virtual Training - Live Webinar"; productInfo: string; + shortUrl: string; + draft: boolean; }; diff --git a/frontend/src/store/useCourseEditStore.ts b/frontend/src/store/useCourseEditStore.ts index c19c760..84c3a70 100644 --- a/frontend/src/store/useCourseEditStore.ts +++ b/frontend/src/store/useCourseEditStore.ts @@ -26,6 +26,7 @@ interface CourseEditState { regStart: Date; regEnd?: Date; productType: string; + productInfo: string; shortUrl?: string; draft: boolean; @@ -76,6 +77,7 @@ const initialState: Omit< regStart: new Date(), regEnd: undefined, productType: "", + productInfo: "", shortUrl: undefined, draft: true, }; @@ -114,7 +116,6 @@ export const useCourseEditStore = create()( name: "course-edit-store", storage: createJSONStorage(() => sessionStorage), - // ✅ Correct usage here onRehydrateStorage: (state) => { return () => { state?.setHydrated(); // this is how Zustand recommends setting hydration flag