diff --git a/public/fallback-8u_CZnDJoQGp2wQUAuYFH.js b/public/fallback-JpVqoPYWhveAylwUcifjM.js similarity index 100% rename from public/fallback-8u_CZnDJoQGp2wQUAuYFH.js rename to public/fallback-JpVqoPYWhveAylwUcifjM.js diff --git a/src/pages/api/lms/progress/index.ts b/src/pages/api/lms/progress/index.ts new file mode 100644 index 000000000..0cf031515 --- /dev/null +++ b/src/pages/api/lms/progress/index.ts @@ -0,0 +1,217 @@ +import { NextApiResponse } from 'next'; +import { requireAuth, AuthenticatedRequest } from '@/lib/rbac'; +import prisma from '@/lib/prisma'; + +/** + * GET /api/lms/progress + * Fetch user's lesson progress (optionally filtered by courseId or moduleId) + * + * Query params: + * - courseId?: string - Filter by course + * - moduleId?: string - Filter by module + * - lessonId?: string - Get specific lesson progress + * + * POST /api/lms/progress + * Update lesson progress (mark as started, completed, or update time spent) + * + * Request body: + * { + * lessonId: string, + * completed?: boolean, + * timeSpent?: number (in minutes) + * } + */ +export default requireAuth(async (req: AuthenticatedRequest, res: NextApiResponse) => { + const userId = req.user!.id; + + // GET - Fetch user's progress + if (req.method === 'GET') { + try { + const { courseId, moduleId, lessonId } = req.query; + + // Build where clause for filtering + const where: any = { userId }; + + if (lessonId) { + where.lessonId = lessonId as string; + } else if (moduleId) { + where.lesson = { + moduleId: moduleId as string, + }; + } else if (courseId) { + where.lesson = { + module: { + courseId: courseId as string, + }, + }; + } + + const progress = await prisma.progress.findMany({ + where, + include: { + lesson: { + select: { + id: true, + title: true, + order: true, + duration: true, + moduleId: true, + module: { + select: { + id: true, + title: true, + order: true, + courseId: true, + }, + }, + }, + }, + }, + orderBy: [ + { lesson: { module: { order: 'asc' } } }, + { lesson: { order: 'asc' } }, + ], + }); + + return res.status(200).json({ progress }); + } catch (error) { + console.error('Error fetching progress:', error); + return res.status(500).json({ error: 'Failed to fetch progress' }); + } + } + + // POST - Update lesson progress + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { lessonId, completed, timeSpent } = req.body; + + // Validation + if (!lessonId) { + return res.status(400).json({ error: 'Lesson ID is required' }); + } + + // Verify lesson exists + const lesson = await prisma.lesson.findUnique({ + where: { id: lessonId }, + include: { + module: { + include: { + course: { + include: { + enrollments: { + where: { + userId, + status: 'ACTIVE', + }, + }, + }, + }, + }, + }, + }, + }); + + if (!lesson) { + return res.status(404).json({ error: 'Lesson not found' }); + } + + // Verify user is enrolled in the course + if (lesson.module.course.enrollments.length === 0) { + return res.status(403).json({ + error: 'You must be enrolled in the course to track progress', + }); + } + + // Check for existing progress record + const existingProgress = await prisma.progress.findUnique({ + where: { + userId_lessonId: { + userId, + lessonId, + }, + }, + }); + + let progressRecord; + + if (existingProgress) { + // Update existing progress + const updateData: any = {}; + + if (typeof completed === 'boolean') { + updateData.completed = completed; + if (completed && !existingProgress.completedAt) { + updateData.completedAt = new Date(); + } else if (!completed) { + updateData.completedAt = null; + } + } + + if (typeof timeSpent === 'number' && timeSpent >= 0) { + updateData.timeSpent = timeSpent; + } + + progressRecord = await prisma.progress.update({ + where: { + id: existingProgress.id, + }, + data: updateData, + include: { + lesson: { + select: { + id: true, + title: true, + order: true, + moduleId: true, + }, + }, + }, + }); + } else { + // Create new progress record + progressRecord = await prisma.progress.create({ + data: { + userId, + lessonId, + completed: completed || false, + timeSpent: timeSpent || 0, + completedAt: completed ? new Date() : null, + }, + include: { + lesson: { + select: { + id: true, + title: true, + order: true, + moduleId: true, + }, + }, + }, + }); + } + + // Update enrollment lastActivity timestamp + await prisma.enrollment.updateMany({ + where: { + userId, + courseId: lesson.module.courseId, + }, + data: { + lastActivity: new Date(), + }, + }); + + res.status(existingProgress ? 200 : 201).json({ + progress: progressRecord, + message: existingProgress + ? 'Progress updated successfully' + : 'Progress tracking started', + }); + } catch (error) { + console.error('Error updating progress:', error); + res.status(500).json({ error: 'Failed to update progress' }); + } +}); diff --git a/src/pages/courses/web-development/[moduleId]/[lessonId].tsx b/src/pages/courses/web-development/[moduleId]/[lessonId].tsx index dda096700..a599ea670 100644 --- a/src/pages/courses/web-development/[moduleId]/[lessonId].tsx +++ b/src/pages/courses/web-development/[moduleId]/[lessonId].tsx @@ -115,13 +115,41 @@ const LessonPage: PageWithLayout = ({ lesson, module }) => { const [completed, setCompleted] = useState(false); const [showAssignment, setShowAssignment] = useState(false); const [isAIAssistantOpen, setIsAIAssistantOpen] = useState(false); + const [updating, setUpdating] = useState(false); + const [moduleProgress, setModuleProgress] = useState({ completed: 0, total: module.totalLessons }); + // Fetch lesson progress and module progress on mount useEffect(() => { - // TODO: Check if lesson is completed from database - // For now, check localStorage for demo - const lessonKey = `lesson_${lesson.id}_completed`; - setCompleted(localStorage.getItem(lessonKey) === "true"); - }, [lesson.id]); + const fetchProgress = async () => { + try { + // Fetch current lesson progress + const lessonResponse = await fetch(`/api/lms/progress?lessonId=${lesson.id}`); + const lessonData = await lessonResponse.json(); + + if (lessonResponse.ok && lessonData.progress.length > 0) { + setCompleted(lessonData.progress[0].completed); + } + + // Fetch module progress + const moduleResponse = await fetch(`/api/lms/progress?moduleId=${module.id}`); + const moduleData = await moduleResponse.json(); + + if (moduleResponse.ok) { + const completedCount = moduleData.progress.filter( + (p: { completed: boolean }) => p.completed + ).length; + setModuleProgress({ + completed: completedCount, + total: module.totalLessons, + }); + } + } catch (error) { + console.error("Error fetching progress:", error); + } + }; + + fetchProgress(); + }, [lesson.id, module.id, module.totalLessons]); // Keyboard shortcut for AI Assistant ('A' key) useEffect(() => { @@ -143,12 +171,36 @@ const LessonPage: PageWithLayout = ({ lesson, module }) => { return () => window.removeEventListener('keydown', handleKeyPress); }, []); - const markAsCompleted = () => { - // TODO: Update database with lesson completion - // For now, use localStorage for demo - const lessonKey = `lesson_${lesson.id}_completed`; - localStorage.setItem(lessonKey, "true"); - setCompleted(true); + const markAsCompleted = async () => { + try { + setUpdating(true); + const response = await fetch("/api/lms/progress", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + lessonId: lesson.id, + completed: true, + }), + }); + + if (response.ok) { + setCompleted(true); + // Refresh module progress + setModuleProgress((prev) => ({ + ...prev, + completed: prev.completed + 1, + })); + } else { + const data = await response.json(); + console.error("Failed to mark as completed:", data.error); + } + } catch (error) { + console.error("Error marking lesson as completed:", error); + } finally { + setUpdating(false); + } }; return ( @@ -243,10 +295,11 @@ const LessonPage: PageWithLayout = ({ lesson, module }) => { )} @@ -272,18 +325,43 @@ const LessonPage: PageWithLayout = ({ lesson, module }) => {