arcadia-suite-sv/server/lms/routes.ts

313 lines
12 KiB
TypeScript

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;