import { Router, type Request, type Response } from "express"; import { db } from "../../db/index"; import { sql } from "drizzle-orm"; const router = Router(); function requireAuth(req: any, res: any, next: any) { if (!req.isAuthenticated()) { return res.status(401).json({ error: "Authentication required" }); } next(); } router.use(requireAuth); router.get("/courses", async (req: Request, res: Response) => { try { const { category, featured, published } = req.query; let conditions = ["1=1"]; if (category) conditions.push(`category = $1`); if (featured === "true") conditions.push("is_featured = true"); if (published !== "false") conditions.push("is_published = true"); const courses = await db.execute(sql` SELECT c.*, (SELECT COUNT(*) FROM lms_modules WHERE course_id = c.id) as module_count, (SELECT COUNT(*) FROM lms_lessons l JOIN lms_modules m ON l.module_id = m.id WHERE m.course_id = c.id) as lesson_count FROM lms_courses c WHERE is_published = true ORDER BY c.is_featured DESC, c.created_at DESC `); res.json(courses.rows || courses); } catch (error) { console.error("Error fetching courses:", error); res.status(500).json({ error: "Failed to fetch courses" }); } }); router.get("/courses/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); if (isNaN(id)) return res.status(400).json({ error: "Invalid ID" }); const courseResult = await db.execute(sql` SELECT * FROM lms_courses WHERE id = ${id} `); const course = (courseResult.rows || courseResult)[0]; if (!course) { return res.status(404).json({ error: "Course not found" }); } const modulesResult = await db.execute(sql` SELECT m.*, (SELECT json_agg(l ORDER BY l.sort_order) FROM lms_lessons l WHERE l.module_id = m.id) as lessons FROM lms_modules m WHERE m.course_id = ${id} ORDER BY m.sort_order `); res.json({ ...course, modules: modulesResult.rows || modulesResult, }); } catch (error) { console.error("Error fetching course:", error); res.status(500).json({ error: "Failed to fetch course" }); } }); router.post("/courses", async (req: Request, res: Response) => { try { const { title, description, category, instructor_name, duration_hours, difficulty, price, is_free, thumbnail_url } = req.body; if (!title || typeof title !== 'string') { return res.status(400).json({ error: "Title is required" }); } const code = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 50); const safeDescription = description || ''; const safeCategory = category || ''; const safeInstructor = instructor_name || ''; const safeDuration = parseFloat(duration_hours) || 0; const safeDifficulty = ['beginner', 'intermediate', 'advanced'].includes(difficulty) ? difficulty : 'beginner'; const safePrice = parseFloat(price) || 0; const safeFree = Boolean(is_free); const safeThumbnail = thumbnail_url || ''; const result = await db.execute(sql` INSERT INTO lms_courses (code, title, description, category, instructor_name, duration_hours, difficulty, price, is_free, thumbnail_url, is_published) VALUES (${code}, ${title}, ${safeDescription}, ${safeCategory}, ${safeInstructor}, ${safeDuration}, ${safeDifficulty}, ${safePrice}, ${safeFree}, ${safeThumbnail}, false) RETURNING * `); res.status(201).json((result.rows || result)[0]); } catch (error) { console.error("Error creating course:", error); res.status(500).json({ error: "Failed to create course" }); } }); router.put("/courses/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); if (isNaN(id)) return res.status(400).json({ error: "Invalid ID" }); const { title, description, category, instructor_name, duration_hours, difficulty, price, is_free, is_published, is_featured, thumbnail_url } = req.body; const result = await db.execute(sql` UPDATE lms_courses SET title = COALESCE(${title}, title), description = COALESCE(${description}, description), category = COALESCE(${category}, category), instructor_name = COALESCE(${instructor_name}, instructor_name), duration_hours = COALESCE(${duration_hours ? parseFloat(duration_hours) : null}, duration_hours), difficulty = COALESCE(${difficulty}, difficulty), price = COALESCE(${price ? parseFloat(price) : null}, price), is_free = COALESCE(${is_free}, is_free), is_published = COALESCE(${is_published}, is_published), is_featured = COALESCE(${is_featured}, is_featured), thumbnail_url = COALESCE(${thumbnail_url}, thumbnail_url), updated_at = CURRENT_TIMESTAMP WHERE id = ${id} RETURNING * `); res.json((result.rows || result)[0]); } catch (error) { console.error("Error updating course:", error); res.status(500).json({ error: "Failed to update course" }); } }); router.delete("/courses/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); if (isNaN(id)) return res.status(400).json({ error: "Invalid ID" }); await db.execute(sql`DELETE FROM lms_courses WHERE id = ${id}`); res.json({ success: true }); } catch (error) { console.error("Error deleting course:", error); res.status(500).json({ error: "Failed to delete course" }); } }); router.post("/courses/:courseId/modules", async (req: Request, res: Response) => { try { const courseId = parseInt(req.params.courseId); if (isNaN(courseId)) return res.status(400).json({ error: "Invalid course ID" }); const { title, description, sort_order } = req.body; if (!title) return res.status(400).json({ error: "Title is required" }); const result = await db.execute(sql` INSERT INTO lms_modules (course_id, title, description, sort_order) VALUES (${courseId}, ${title}, ${description || ''}, ${parseInt(sort_order) || 0}) RETURNING * `); res.status(201).json((result.rows || result)[0]); } catch (error) { console.error("Error creating module:", error); res.status(500).json({ error: "Failed to create module" }); } }); router.post("/modules/:moduleId/lessons", async (req: Request, res: Response) => { try { const moduleId = parseInt(req.params.moduleId); if (isNaN(moduleId)) return res.status(400).json({ error: "Invalid module ID" }); const { title, description, content_type, content_url, content_text, duration_minutes, sort_order, is_free_preview } = req.body; if (!title) return res.status(400).json({ error: "Title is required" }); const safeContentType = ['video', 'text', 'pdf', 'quiz'].includes(content_type) ? content_type : 'video'; const result = await db.execute(sql` INSERT INTO lms_lessons (module_id, title, description, content_type, content_url, content_text, duration_minutes, sort_order, is_free_preview) VALUES (${moduleId}, ${title}, ${description || ''}, ${safeContentType}, ${content_url || ''}, ${content_text || ''}, ${parseInt(duration_minutes) || 0}, ${parseInt(sort_order) || 0}, ${Boolean(is_free_preview)}) RETURNING * `); res.status(201).json((result.rows || result)[0]); } catch (error) { console.error("Error creating lesson:", error); res.status(500).json({ error: "Failed to create lesson" }); } }); router.get("/enrollments", async (req: Request, res: Response) => { try { const userId = (req as any).user?.id; const enrollments = await db.execute(sql` SELECT e.*, c.title as course_title, c.thumbnail_url, c.instructor_name FROM lms_enrollments e JOIN lms_courses c ON e.course_id = c.id ORDER BY e.created_at DESC `); res.json(enrollments.rows || enrollments); } catch (error) { console.error("Error fetching enrollments:", error); res.status(500).json({ error: "Failed to fetch enrollments" }); } }); router.post("/courses/:courseId/enroll", async (req: Request, res: Response) => { try { const courseId = parseInt(req.params.courseId); if (isNaN(courseId)) return res.status(400).json({ error: "Invalid course ID" }); const userId = (req as any).user?.id || req.body.userId; if (!userId || typeof userId !== 'string') { return res.status(400).json({ error: "User ID required" }); } const existingResult = await db.execute(sql` SELECT id FROM lms_enrollments WHERE course_id = ${courseId} AND user_id = ${userId} `); if ((existingResult.rows || existingResult).length > 0) { return res.status(400).json({ error: "Already enrolled" }); } const lessonsResult = await db.execute(sql` SELECT COUNT(*) as count FROM lms_lessons l JOIN lms_modules m ON l.module_id = m.id WHERE m.course_id = ${courseId} `); const totalLessons = parseInt((lessonsResult.rows || lessonsResult)[0]?.count) || 0; const result = await db.execute(sql` INSERT INTO lms_enrollments (course_id, user_id, total_lessons) VALUES (${courseId}, ${userId}, ${totalLessons}) RETURNING * `); await db.execute(sql` UPDATE lms_courses SET enrollment_count = enrollment_count + 1 WHERE id = ${courseId} `); res.status(201).json((result.rows || result)[0]); } catch (error) { console.error("Error enrolling:", error); res.status(500).json({ error: "Failed to enroll" }); } }); router.post("/lessons/:lessonId/complete", async (req: Request, res: Response) => { try { const lessonId = parseInt(req.params.lessonId); if (isNaN(lessonId)) return res.status(400).json({ error: "Invalid lesson ID" }); const enrollmentId = parseInt(req.body.enrollmentId); if (isNaN(enrollmentId)) return res.status(400).json({ error: "Invalid enrollment ID" }); await db.execute(sql` INSERT INTO lms_lesson_progress (enrollment_id, lesson_id, status, completed_at) VALUES (${enrollmentId}, ${lessonId}, 'completed', CURRENT_TIMESTAMP) ON CONFLICT (enrollment_id, lesson_id) DO UPDATE SET status = 'completed', completed_at = CURRENT_TIMESTAMP `); const progressResult = await db.execute(sql` SELECT (SELECT COUNT(*) FROM lms_lesson_progress WHERE enrollment_id = ${enrollmentId} AND status = 'completed') as completed, (SELECT total_lessons FROM lms_enrollments WHERE id = ${enrollmentId}) as total `); const row = (progressResult.rows || progressResult)[0]; const completed = parseInt(row?.completed) || 0; const total = parseInt(row?.total) || 1; const progressPercent = total > 0 ? Math.round((completed / total) * 100) : 0; await db.execute(sql` UPDATE lms_enrollments SET progress_percent = ${progressPercent}, completed_lessons = ${completed}, completed_at = ${progressPercent >= 100 ? sql`CURRENT_TIMESTAMP` : sql`NULL`} WHERE id = ${enrollmentId} `); res.json({ progress_percent: progressPercent, completed_lessons: completed }); } catch (error) { console.error("Error completing lesson:", error); res.status(500).json({ error: "Failed to complete lesson" }); } }); router.get("/stats", async (req: Request, res: Response) => { try { const stats = await db.execute(sql` SELECT (SELECT COUNT(*) FROM lms_courses WHERE is_published = true) as total_courses, (SELECT COUNT(*) FROM lms_enrollments) as total_enrollments, (SELECT COUNT(*) FROM lms_enrollments WHERE progress_percent = 100) as completions, (SELECT COUNT(*) FROM lms_lessons) as total_lessons `); res.json((stats.rows || stats)[0]); } catch (error) { console.error("Error fetching stats:", error); res.status(500).json({ error: "Failed to fetch stats" }); } }); export default router;