From fd0fbacd7b1cdfed913a82dcf032fd90944812d0 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Wed, 25 Jun 2025 00:10:00 +0200 Subject: [PATCH 01/10] Add new table for module status --- database/schema.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/database/schema.sql b/database/schema.sql index c472877..a4efa45 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -61,6 +61,18 @@ CREATE TABLE enrollments ( UNIQUE(user_id, course_id) ); +CREATE TABLE MODULE_STATUS ( + id SERIAL PRIMARY KEY, + enrollment_id INTEGER NOT NULL + REFERENCES enrollments(id) ON DELETE CASCADE, + module_id INTEGER NOT NULL + REFERENCES modules(id) ON DELETE CASCADE, + status VARCHAR(50) NOT NULL DEFAULT 'not_started', -- 'not_started', 'in_progress', 'completed' + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + UNIQUE(enrollment_id, module_id) +); + -- 5. MODULES & REVISIONS CREATE TABLE modules ( From 54887b14d4fe80f05110936873020541fe031395 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Wed, 25 Jun 2025 00:10:16 +0200 Subject: [PATCH 02/10] Modify table name --- database/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/schema.sql b/database/schema.sql index a4efa45..03e9687 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -61,7 +61,7 @@ CREATE TABLE enrollments ( UNIQUE(user_id, course_id) ); -CREATE TABLE MODULE_STATUS ( +CREATE TABLE module_status ( id SERIAL PRIMARY KEY, enrollment_id INTEGER NOT NULL REFERENCES enrollments(id) ON DELETE CASCADE, From f5650689b24a12b52033d5b9466743275b97bbaf Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Wed, 25 Jun 2025 22:37:08 +0200 Subject: [PATCH 03/10] Add endpoint to check for module status --- routes/courses.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/routes/courses.js b/routes/courses.js index 607c4b9..2c7aa47 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -802,6 +802,25 @@ router.post("/enroll-course", async (req, res) => { [userId, courseId] ); + const enrollmentId = insertRes.rows[0].id; + + const modulesRes = await client.query( + `SELECT id + FROM modules + WHERE course_id = $1`, + [courseId] + ); + + for (const { id: moduleId } of modulesRes.rows) { + await client.query( + `INSERT INTO module_status + (enrollment_id, module_id, status) + VALUES ($1, $2, 'not_started') + ON CONFLICT (enrollment_id, module_id) DO NOTHING`, + [enrollmentId, moduleId] + ); + } + await client.query("COMMIT"); return res.status(201).json({ success: true, @@ -856,6 +875,14 @@ router.post("/unenroll-course", async (req, res) => { return res.status(404).json({ message: "Not enrolled in this course" }); } + const enrollmentId = delRes.rows[0].id; + + await client.query( + `DELETE FROM module_status + WHERE enrollment_id = $1`, + [enrollmentId] + ); + await client.query("COMMIT"); return res.status(200).json({ success: true }); } catch (err) { @@ -1155,4 +1182,59 @@ router.post("/get-latest-quiz-response", async (req, res) => { } }); +router.post("/get-module-status", async (req, res) => { + const { auth } = req.cookies; + if (!auth) return res.status(401).json({ message: "Not authenticated" }); + + let session; + try { + session = JSON.parse(auth); + } catch { + return res.status(400).json({ message: "Invalid session data" }); + } + + const userId = session.userId; + const moduleId = req.body.moduleId; + if (!moduleId) { + return res.status(400).json({ message: "moduleId is required" }); + } + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + const courseIdRes = await client.query( + `SELECT course_id FROM modules WHERE id = $1`, + [moduleId] + ); + + const courseId = courseIdRes.rows[0]?.course_id; + + const enrolmentRes = await client.query( + `SELECT id FROM enrollments + WHERE user_id = $1 AND course_id = $2`, + [userId, courseId] + ); + + const enrollmentId = enrolmentRes.rows[0]?.id; + + const statusRes = await client.query( + `SELECT status FROM module_status + WHERE enrollment_id = $1 AND module_id = $2`, + [enrollmentId, moduleId] + ); + + await client.query("COMMIT"); + return res.status(200).json({ + status: statusRes.rows.length ? statusRes.rows[0].status : "not_started", + }); + } catch (err) { + await client.query("ROLLBACK"); + console.error(err); + return res.status(500).json({ message: "Server error" }); + } finally { + client.release(); + } +}); + module.exports = router; From 0c6badb41f44bfe69c4f0aec9668822c72934fbc Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Wed, 25 Jun 2025 23:03:38 +0200 Subject: [PATCH 04/10] Add endpoints to update module status --- routes/courses.js | 127 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/routes/courses.js b/routes/courses.js index 2c7aa47..ce5f4e3 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -1237,4 +1237,131 @@ router.post("/get-module-status", async (req, res) => { } }); +router.post("/mark-module-started", async (req, res) => { + const { auth } = req.cookies; + if (!auth) return res.status(401).json({ message: "Not authenticated" }); + + let session; + try { + session = JSON.parse(auth); + } catch { + return res.status(400).json({ message: "Invalid session data" }); + } + + const userId = session.userId; + const moduleId = req.body.moduleId; + if (!moduleId) { + return res.status(400).json({ message: "moduleId is required" }); + } + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + const courseIdRes = await client.query( + `SELECT course_id FROM modules WHERE id = $1`, + [moduleId] + ); + + const courseId = courseIdRes.rows[0]?.course_id; + + const enrolmentRes = await client.query( + `SELECT id FROM enrollments + WHERE user_id = $1 AND course_id = $2`, + [userId, courseId] + ); + + const enrollmentId = enrolmentRes.rows[0]?.id; + + const statusRes = await client.query( + `SELECT status FROM module_status + WHERE enrollment_id = $1 AND module_id = $2`, + [enrollmentId, moduleId] + ); + + await client.query( + `UPDATE module_status + SET status = 'in_progress' + WHERE enrollment_id = $1 AND module_id = $2`, + [enrollmentId, moduleId] + ); + + await client.query("COMMIT"); + return res.status(200).json({ status: "in_progress" }); + } catch (err) { + await client.query("ROLLBACK"); + console.error(err); + return res.status(500).json({ message: "Server error" }); + } finally { + client.release(); + } +}); + +router.post("/mark-module-completed", async (req, res) => { + const { auth } = req.cookies; + if (!auth) return res.status(401).json({ message: "Not authenticated" }); + + let session; + try { + session = JSON.parse(auth); + } catch { + return res.status(400).json({ message: "Invalid session data" }); + } + + const userId = session.userId; + const moduleId = req.body.moduleId; + if (!moduleId) { + return res.status(400).json({ message: "moduleId is required" }); + } + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + const courseIdRes = await client.query( + `SELECT course_id FROM modules WHERE id = $1`, + [moduleId] + ); + + const courseId = courseIdRes.rows[0]?.course_id; + + const enrolmentRes = await client.query( + `SELECT id FROM enrollments + WHERE user_id = $1 AND course_id = $2`, + [userId, courseId] + ); + + const enrollmentId = enrolmentRes.rows[0]?.id; + + const statusRes = await client.query( + `SELECT status FROM module_status + WHERE enrollment_id = $1 AND module_id = $2`, + [enrollmentId, moduleId] + ); + + if (statusRes.rows.length && statusRes.rows[0].status !== "in_progress") { + await client.query("ROLLBACK"); + return res.status(400).json({ + message: "Module must be marked as in_progress before completing", + }); + } + + await client.query( + `UPDATE module_status + SET status = 'completed' + WHERE enrollment_id = $1 AND module_id = $2`, + [enrollmentId, moduleId] + ); + + await client.query("COMMIT"); + return res.status(200).json({ status: "in_progress" }); + } catch (err) { + await client.query("ROLLBACK"); + console.error(err); + return res.status(500).json({ message: "Server error" }); + } finally { + client.release(); + } +}); + module.exports = router; From 090fc44e6cf2ebfa1fdf687983230e465e6fe22c Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Wed, 25 Jun 2025 23:09:56 +0200 Subject: [PATCH 05/10] Update module status for quizzes, and update time stamp --- routes/courses.js | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/routes/courses.js b/routes/courses.js index ce5f4e3..3a5cf82 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -971,6 +971,43 @@ async function submitQuizResponse(client, userId, quizId, answers) { } } + const revisionRes = await client.query( + `SELECT revision_id + FROM quizzes + WHERE id = $1`, + [quizId] + ); + + const revisionId = revisionRes.rows[0].revision_id; + + const moduleRes = await client.query( + `SELECT module_id + FROM revisions + WHERE id = $1`, + [revisionId] + ); + + const moduleId = moduleRes.rows[0].module_id; + + const enrollmentRes = await client.query( + `SELECT id + FROM enrollments + WHERE user_id = $1 AND course_id = ( + SELECT course_id FROM modules WHERE id = $2 + )`, + [userId, moduleId] + ); + + const enrollmentId = enrollmentRes.rows[0].id; + + await client.query( + `UPDATE module_status + SET status = 'completed', + completed_at = NOW() + WHERE enrollment_id = $1 AND module_id = $2`, + [enrollmentId, moduleId] + ); + return responseId; } @@ -1281,7 +1318,8 @@ router.post("/mark-module-started", async (req, res) => { await client.query( `UPDATE module_status - SET status = 'in_progress' + SET status = 'in_progress', + started_at = NOW() WHERE enrollment_id = $1 AND module_id = $2`, [enrollmentId, moduleId] ); @@ -1348,7 +1386,8 @@ router.post("/mark-module-completed", async (req, res) => { await client.query( `UPDATE module_status - SET status = 'completed' + SET status = 'completed', + completed_at = NOW() WHERE enrollment_id = $1 AND module_id = $2`, [enrollmentId, moduleId] ); From 3eb6cb3c266cf7618a80ba5958bf6822dba0e36d Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Wed, 25 Jun 2025 23:43:29 +0200 Subject: [PATCH 06/10] Add progress count for courses --- routes/courses.js | 96 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/routes/courses.js b/routes/courses.js index 3a5cf82..91a0002 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -742,10 +742,32 @@ router.get("/all-user-courses", async (req, res) => { // 1) Courses the user is enrolled in const enrolledRes = await client.query( - `SELECT c.id, c.name, c.description - FROM courses c - JOIN enrollments e ON e.course_id = c.id - WHERE e.user_id = $1`, + ` + SELECT + c.id, + c.name, + c.description, + -- total modules in the course + ( + SELECT COUNT(*) + FROM modules m + WHERE m.course_id = c.id + ) AS total_modules, + -- modules completed by this user + ( + SELECT COUNT(*) + FROM enrollments e + JOIN module_status ms ON ms.enrollment_id = e.id + WHERE e.user_id = $1 + AND e.course_id = c.id + AND ms.module_id IN (SELECT id FROM modules WHERE course_id = c.id) + AND ms.status = 'completed' + ) AS completed_modules + FROM courses c + JOIN enrollments e + ON e.course_id = c.id + WHERE e.user_id = $1 + `, [userId] ); @@ -796,8 +818,8 @@ router.post("/enroll-course", async (req, res) => { await client.query("BEGIN"); const insertRes = await client.query( - `INSERT INTO enrollments (user_id, course_id) - VALUES ($1, $2) + `INSERT INTO enrollments (user_id, course_id, started_at) + VALUES ($1, $2, NOW()) RETURNING id, status, started_at`, [userId, courseId] ); @@ -894,6 +916,68 @@ router.post("/unenroll-course", async (req, res) => { } }); +router.post("/complete-course", async (req, res) => { + const { auth } = req.cookies; + if (!auth) return res.status(401).json({ message: "Not authenticated" }); + + let session; + try { + session = JSON.parse(auth); + } catch { + return res.status(400).json({ message: "Invalid session data" }); + } + + const userId = session.userId; + const { courseId } = req.body; + if (!courseId) { + return res.status(400).json({ message: "courseId is required" }); + } + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + const modRes = await client.query( + `SELECT COUNT(*) AS total_modules, + SUM(CASE WHEN ms.status = 'completed' THEN 1 ELSE 0 END) AS completed_modules + FROM modules m + JOIN module_status ms ON ms.module_id = m.id + WHERE m.course_id = $1 + AND ms.enrollment_id = ( + SELECT id FROM enrollments WHERE user_id = $2 AND course_id = $1 + )`, + [courseId, userId] + ); + + const totalModules = modRes.rows[0].total_modules; + const completedModules = modRes.rows[0].completed_modules; + if (totalModules !== completedModules) { + await client.query("ROLLBACK"); + return res.status(400).json({ + message: "Cannot complete course - not all modules are completed", + }); + } + + await client.query( + `UPDATE enrollments + SET status = 'completed', + completed_at = NOW() + WHERE user_id = $1 + AND course_id = $2`, + [userId, courseId] + ); + + await client.query("COMMIT"); + return res.status(200).json({ success: true }); + } catch (err) { + await client.query("ROLLBACK"); + console.error("Error completing course:", err); + return res.status(500).json({ message: "Server error" }); + } finally { + client.release(); + } +}); + router.post("/is-enrolled", async (req, res) => { const { auth } = req.cookies; if (!auth) return res.status(401).json({ message: "Not authenticated" }); From 571df18517b70fb9768dd68bb71c3d9d4e3d2598 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Wed, 25 Jun 2025 23:52:53 +0200 Subject: [PATCH 07/10] Add completed courses --- routes/courses.js | 78 +++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/routes/courses.js b/routes/courses.js index 91a0002..aa56813 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -740,51 +740,75 @@ router.get("/all-user-courses", async (req, res) => { try { await client.query("BEGIN"); - // 1) Courses the user is enrolled in + // 1) “Enrolled” courses (status = 'enrolled') const enrolledRes = await client.query( ` SELECT c.id, c.name, c.description, - -- total modules in the course - ( - SELECT COUNT(*) - FROM modules m - WHERE m.course_id = c.id - ) AS total_modules, - -- modules completed by this user - ( - SELECT COUNT(*) - FROM enrollments e - JOIN module_status ms ON ms.enrollment_id = e.id - WHERE e.user_id = $1 - AND e.course_id = c.id - AND ms.module_id IN (SELECT id FROM modules WHERE course_id = c.id) - AND ms.status = 'completed' - ) AS completed_modules + COUNT(m.id) AS total_modules, + COUNT(ms.id) FILTER (WHERE ms.status = 'completed') + AS completed_modules FROM courses c - JOIN enrollments e - ON e.course_id = c.id - WHERE e.user_id = $1 + JOIN enrollments e + ON e.course_id = c.id + AND e.user_id = $1 + AND e.status = 'enrolled' + LEFT JOIN modules m + ON m.course_id = c.id + LEFT JOIN module_status ms + ON ms.module_id = m.id + AND ms.enrollment_id = e.id + GROUP BY c.id, c.name, c.description; `, [userId] ); - // 2) All other courses in the same org that they're NOT enrolled in + // 2) “Completed” courses (status = 'completed') + const completedRes = await client.query( + ` + SELECT + c.id, + c.name, + c.description, + COUNT(m.id) AS total_modules, + COUNT(ms.id) FILTER (WHERE ms.status = 'completed') + AS completed_modules + FROM courses c + JOIN enrollments e + ON e.course_id = c.id + AND e.user_id = $1 + AND e.status = 'completed' + LEFT JOIN modules m + ON m.course_id = c.id + LEFT JOIN module_status ms + ON ms.module_id = m.id + AND ms.enrollment_id = e.id + GROUP BY c.id, c.name, c.description; + `, + [userId] + ); + + // 3) Others in same org, not enrolled at all const otherRes = await client.query( - `SELECT c.id, c.name, c.description - FROM courses c - WHERE c.organisation_id = $1 - AND c.id NOT IN ( - SELECT course_id FROM enrollments WHERE user_id = $2 - )`, + ` + SELECT c.id, c.name, c.description + FROM courses c + WHERE c.organisation_id = $1 + AND c.id NOT IN ( + SELECT course_id + FROM enrollments + WHERE user_id = $2 + ) + `, [organisationId, userId] ); await client.query("COMMIT"); return res.status(200).json({ enrolled: enrolledRes.rows, + completed: completedRes.rows, other: otherRes.rows, }); } catch (err) { From 4b91a635783cb15e7e334063a0abde3ee9fd5970 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Thu, 26 Jun 2025 00:03:44 +0200 Subject: [PATCH 08/10] Allow course to be unenrolled --- routes/courses.js | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/routes/courses.js b/routes/courses.js index aa56813..c271df9 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -333,6 +333,23 @@ router.post("/add-module", upload.single("file"), async (req, res) => { const module_id = moduleRes.rows[0].id; + const { rows: enrollments } = await client.query( + `SELECT id + FROM enrollments + WHERE course_id = $1`, + [courseId] + ); + + for (const { id: enrollmentId } of enrollments) { + await client.query( + `INSERT INTO module_status + (enrollment_id, module_id, status) + VALUES ($1, $2, 'not_started') + ON CONFLICT (enrollment_id, module_id) DO NOTHING`, + [enrollmentId, module_id] + ); + } + if (type === "quiz") { // revision for this module const revRes = await client.query( @@ -1002,6 +1019,47 @@ router.post("/complete-course", async (req, res) => { } }); +router.post("/uncomplete-course", async (req, res) => { + const { auth } = req.cookies; + if (!auth) return res.status(401).json({ message: "Not authenticated" }); + + let session; + try { + session = JSON.parse(auth); + } catch { + return res.status(400).json({ message: "Invalid session data" }); + } + + const userId = session.userId; + const { courseId } = req.body; + if (!courseId) { + return res.status(400).json({ message: "courseId is required" }); + } + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + await client.query( + `UPDATE enrollments + SET status = 'enrolled', + completed_at = NULL + WHERE user_id = $1 + AND course_id = $2`, + [userId, courseId] + ); + + await client.query("COMMIT"); + return res.status(200).json({ success: true }); + } catch (err) { + await client.query("ROLLBACK"); + console.error("Error completing course:", err); + return res.status(500).json({ message: "Server error" }); + } finally { + client.release(); + } +}); + router.post("/is-enrolled", async (req, res) => { const { auth } = req.cookies; if (!auth) return res.status(401).json({ message: "Not authenticated" }); From 02f7910987ae38e5c491d10da748a1f47334d1ce Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Thu, 26 Jun 2025 00:21:15 +0200 Subject: [PATCH 09/10] manage permissions if course is completed --- routes/courses.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/routes/courses.js b/routes/courses.js index c271df9..eb8a380 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -946,6 +946,19 @@ router.post("/unenroll-course", async (req, res) => { [enrollmentId] ); + await client.query( + `DELETE FROM quiz_responses + WHERE user_id = $1 + AND quiz_id IN ( + SELECT id FROM quizzes WHERE revision_id IN ( + SELECT id FROM revisions WHERE module_id IN ( + SELECT id FROM modules WHERE course_id = $2 + ) + ) + )`, + [userId, courseId] + ); + await client.query("COMMIT"); return res.status(200).json({ success: true }); } catch (err) { @@ -1421,6 +1434,12 @@ router.post("/get-module-status", async (req, res) => { const enrollmentId = enrolmentRes.rows[0]?.id; + const isCourseCompleted = await client.query( + `SELECT 1 FROM enrollments + WHERE id = $1 AND status = 'completed'`, + [enrollmentId] + ); + const statusRes = await client.query( `SELECT status FROM module_status WHERE enrollment_id = $1 AND module_id = $2`, @@ -1430,6 +1449,7 @@ router.post("/get-module-status", async (req, res) => { await client.query("COMMIT"); return res.status(200).json({ status: statusRes.rows.length ? statusRes.rows[0].status : "not_started", + isCourseCompleted: isCourseCompleted.rows.length > 0, }); } catch (err) { await client.query("ROLLBACK"); From ef7859da6ed9eb938047beda9bc0a2acc78cf175 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Thu, 26 Jun 2025 00:36:28 +0200 Subject: [PATCH 10/10] Update module urges user to uncomplete course and do the new modules --- routes/courses.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/routes/courses.js b/routes/courses.js index eb8a380..33e55fc 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -703,6 +703,12 @@ router.put("/update-module", upload.single("file"), async (req, res) => { ); await client.query(`DELETE FROM questions WHERE quiz_id = $1`, [quizId]); + await client.query( + `DELETE FROM quiz_responses + WHERE quiz_id = $1`, + [quizId] + ); + for (let i = 0; i < qs.length; i++) { const { question_text, question_type, options } = qs[i]; const qRes = await client.query( @@ -725,6 +731,34 @@ router.put("/update-module", upload.single("file"), async (req, res) => { } } + if (type) { + const courseIdRes = await client.query( + `SELECT course_id FROM modules WHERE id = $1`, + [moduleId] + ); + + if (!courseIdRes.rows.length) { + return res.status(404).json({ message: "Module not found" }); + } + const courseId = courseIdRes.rows[0].course_id; + + const { rows: enrollments } = await client.query( + `SELECT id + FROM enrollments + WHERE course_id = $1`, + [courseId] + ); + + for (const { id: enrollmentId } of enrollments) { + await client.query( + `UPDATE module_status + SET status = 'not_started' + WHERE enrollment_id = $1 AND module_id = $2`, + [enrollmentId, moduleId] + ); + } + } + await client.query("COMMIT"); return res.status(200).json({ success: true }); } catch (err) {