mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: course_reading_progress.controller.js (client)
|
||||
* Type of Program: Controller
|
||||
* Description: Tracks user reading progress through a course hierarchy (course → unit → lesson).
|
||||
*
|
||||
* GET /client/courses/in-progress
|
||||
* → courses where the current user has status = 'in_progress', with lesson counts
|
||||
*
|
||||
* GET /client/courses/:courseId/progress
|
||||
* → returns all progress rows for this user + course (flat, frontend builds the map)
|
||||
*
|
||||
* POST /client/courses/:courseId/units/:unitId/lessons/:lessonId/progress
|
||||
* → UPSERT lesson + derives + UPSERTs parent unit + course in one transaction
|
||||
*
|
||||
* Body (POST):
|
||||
* { status: 'in_progress' | 'completed' }
|
||||
* Defaults to 'in_progress' if omitted (on first visit).
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 21, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
'use strict';
|
||||
|
||||
const R = require('../../utils/response.util');
|
||||
const logActivity = require('../../utils/logActivity.util');
|
||||
const { upsertLessonRead } = require('../../services/course_reading_progress.service');
|
||||
const CourseReadingProgress = require('../../models/courses/course_reading_progress.mdl');
|
||||
const Certificate = require('../../models/courses/certificate.mdl');
|
||||
const {
|
||||
Course, Unit, Lesson,
|
||||
UnitQuiz, CourseAssessment, QuizAttempt,
|
||||
} = require('../../models/courses/courses.associations');
|
||||
|
||||
const notDeleted = { deletedAt: null };
|
||||
|
||||
// =============================================================================
|
||||
// ── IN-PROGRESS COURSES — profile learning progress card ──────────────────────
|
||||
// =============================================================================
|
||||
|
||||
// GET /client/courses/in-progress
|
||||
// Returns courses the user has started but not yet completed (no certificate).
|
||||
// Includes both:
|
||||
// • reading in_progress → still working through lessons
|
||||
// • reading completed → finished lessons but quiz / assessment still pending
|
||||
// Excludes any course where the user already holds a certificate.
|
||||
|
||||
exports.getMyInProgressCourses = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.user_id;
|
||||
|
||||
// All course-level progress rows for this user (any reading status)
|
||||
const courseRows = await CourseReadingProgress.findAll({
|
||||
where: { user_id: userId, type: 'course' },
|
||||
attributes: ['course_id', 'status', 'last_accessed_at'],
|
||||
include: [{
|
||||
model: Course,
|
||||
as: 'course',
|
||||
attributes: ['course_id', 'title'],
|
||||
where: notDeleted,
|
||||
required: true,
|
||||
}],
|
||||
order: [['last_accessed_at', 'DESC']],
|
||||
});
|
||||
|
||||
if (!courseRows.length) return R.success(res, 'No courses in progress.', []);
|
||||
|
||||
// Courses the user has already earned a certificate for — exclude these
|
||||
const certificates = await Certificate.findAll({
|
||||
where: { user_id: userId },
|
||||
attributes: ['course_id'],
|
||||
});
|
||||
const certSet = new Set(certificates.map((c) => String(c.course_id)));
|
||||
|
||||
const pending = courseRows.filter((r) => !certSet.has(String(r.course_id)));
|
||||
if (!pending.length) return R.success(res, 'No courses in progress.', []);
|
||||
|
||||
const result = await Promise.all(pending.map(async (row) => {
|
||||
const courseId = row.course_id;
|
||||
const readingDone = row.status === 'completed';
|
||||
|
||||
const [lessons_total, lessons_completed] = await Promise.all([
|
||||
Lesson.count({
|
||||
include: [{
|
||||
model: Unit,
|
||||
as: 'unit',
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
required: true,
|
||||
}],
|
||||
where: notDeleted,
|
||||
}),
|
||||
CourseReadingProgress.count({
|
||||
where: { user_id: userId, course_id: courseId, type: 'lesson', status: 'completed' },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Only compute pending quiz/assessment detail when all lessons are read
|
||||
let pending_quizzes = [];
|
||||
let pending_assessment = null;
|
||||
|
||||
if (readingDone) {
|
||||
// All unit quizzes in this course
|
||||
const unitQuizzes = await UnitQuiz.findAll({
|
||||
attributes: ['quiz_id', 'title', 'is_required', 'passing_score'],
|
||||
include: [{
|
||||
model: Unit,
|
||||
as: 'unit',
|
||||
attributes: ['unit_id', 'title'],
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
required: true,
|
||||
}],
|
||||
where: notDeleted,
|
||||
});
|
||||
|
||||
for (const quiz of unitQuizzes) {
|
||||
const [hasPassed, attemptCount] = await Promise.all([
|
||||
QuizAttempt.findOne({ where: { user_id: userId, quiz_id: quiz.quiz_id, passed: true } }),
|
||||
QuizAttempt.count({ where: { user_id: userId, quiz_id: quiz.quiz_id } }),
|
||||
]);
|
||||
if (!hasPassed) {
|
||||
pending_quizzes.push({
|
||||
quiz_id: quiz.quiz_id,
|
||||
title: quiz.title,
|
||||
unit_title: quiz.unit.title,
|
||||
is_required: quiz.is_required,
|
||||
passing_score: quiz.passing_score,
|
||||
attempt_count: attemptCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Course assessment
|
||||
const assessment = await CourseAssessment.findOne({
|
||||
attributes: ['assessment_id', 'title', 'is_required', 'passing_score'],
|
||||
where: { course_id: courseId },
|
||||
});
|
||||
if (assessment) {
|
||||
const [hasPassed, attemptCount] = await Promise.all([
|
||||
QuizAttempt.findOne({ where: { user_id: userId, assessment_id: assessment.assessment_id, passed: true } }),
|
||||
QuizAttempt.count({ where: { user_id: userId, assessment_id: assessment.assessment_id } }),
|
||||
]);
|
||||
if (!hasPassed) {
|
||||
pending_assessment = {
|
||||
assessment_id: assessment.assessment_id,
|
||||
title: assessment.title,
|
||||
is_required: assessment.is_required,
|
||||
passing_score: assessment.passing_score,
|
||||
attempt_count: attemptCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
course_id: courseId,
|
||||
title: row.course.title,
|
||||
reading_status: row.status,
|
||||
lessons_total,
|
||||
lessons_completed,
|
||||
last_accessed_at: row.last_accessed_at,
|
||||
pending_quizzes,
|
||||
pending_assessment,
|
||||
};
|
||||
}));
|
||||
|
||||
return R.success(res, 'In-progress courses retrieved.', result);
|
||||
} catch (err) {
|
||||
console.error('[CLIENT][COURSE READING PROGRESS][IN PROGRESS]', err);
|
||||
return R.error(res, 'Could not retrieve in-progress courses.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ── GET PROGRESS SUMMARY ──────────────────────────────────────────────────────
|
||||
// =============================================================================
|
||||
|
||||
// GET /client/courses/:courseId/progress/summary
|
||||
// Returns a compact progress snapshot: lesson counts + percentage + course status.
|
||||
// Used by the ReadCourse block to render the inline progress bar without needing
|
||||
// the full flat row list.
|
||||
|
||||
exports.getCourseProgressSummary = async (req, res) => {
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
const userId = req.user.user_id;
|
||||
|
||||
const course = await Course.findOne({
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: ['course_id'],
|
||||
});
|
||||
if (!course) return R.error(res, 'Course not found.', 404);
|
||||
|
||||
const [lessons_total, lessons_completed, courseRow] = await Promise.all([
|
||||
Lesson.count({
|
||||
include: [{
|
||||
model: Unit,
|
||||
as: 'unit',
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
required: true,
|
||||
}],
|
||||
where: notDeleted,
|
||||
}),
|
||||
CourseReadingProgress.count({
|
||||
where: { user_id: userId, course_id: courseId, type: 'lesson', status: 'completed' },
|
||||
}),
|
||||
CourseReadingProgress.findOne({
|
||||
where: { user_id: userId, course_id: courseId, type: 'course' },
|
||||
attributes: ['status'],
|
||||
}),
|
||||
]);
|
||||
|
||||
const percent = lessons_total > 0 ? Math.round((lessons_completed / lessons_total) * 100) : 0;
|
||||
|
||||
return R.success(res, 'Progress summary retrieved.', {
|
||||
lessons_total,
|
||||
lessons_completed,
|
||||
percent,
|
||||
status: courseRow?.status ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[CLIENT][COURSE READING PROGRESS][SUMMARY]', err);
|
||||
return R.error(res, 'Could not retrieve progress summary.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ── GET PROGRESS SNAPSHOT ─────────────────────────────────────────────────────
|
||||
// =============================================================================
|
||||
|
||||
// GET /client/courses/:courseId/progress
|
||||
// Returns all progress rows for this user+course.
|
||||
// Frontend uses this to decorate the sidebar (completed checkmarks, locked states, etc.)
|
||||
|
||||
exports.getCourseProgress = async (req, res) => {
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
const userId = req.user.user_id;
|
||||
|
||||
const course = await Course.findOne({
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: ['course_id'],
|
||||
});
|
||||
if (!course) return R.error(res, 'Course not found.', 404);
|
||||
|
||||
const rows = await CourseReadingProgress.findAll({
|
||||
where: { user_id: userId, course_id: courseId },
|
||||
attributes: ['progress_id', 'reference_id', 'type', 'status', 'completed_at', 'last_accessed_at'],
|
||||
});
|
||||
|
||||
return R.success(res, 'Course progress retrieved.', rows);
|
||||
} catch (err) {
|
||||
console.error('[CLIENT][COURSE READING PROGRESS][GET]', err);
|
||||
return R.error(res, 'Could not retrieve course progress.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ── UPSERT LESSON PROGRESS ────────────────────────────────────────────────────
|
||||
// =============================================================================
|
||||
|
||||
// POST /client/courses/:courseId/units/:unitId/lessons/:lessonId/progress
|
||||
// Body: { status: 'in_progress' | 'completed' }
|
||||
//
|
||||
// Flow:
|
||||
// 1. Resolve course / unit / lesson to get their UUIDs
|
||||
// 2. Delegate to upsertLessonRead — handles lesson + unit + course in one tx
|
||||
|
||||
exports.upsertLessonProgress = async (req, res) => {
|
||||
try {
|
||||
const { courseId, unitId, lessonId } = req.params;
|
||||
const userId = req.user.user_id;
|
||||
const status = req.body.status === 'completed' ? 'completed' : 'in_progress';
|
||||
|
||||
const [course, unit, lesson] = await Promise.all([
|
||||
Course.findOne({
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: ['course_id', 'uuid'],
|
||||
}),
|
||||
Unit.findOne({
|
||||
where: { unit_id: unitId, course_id: courseId, ...notDeleted },
|
||||
attributes: ['unit_id', 'uuid'],
|
||||
}),
|
||||
Lesson.findOne({
|
||||
where: { lesson_id: lessonId, unit_id: unitId, ...notDeleted },
|
||||
attributes: ['lesson_id', 'uuid'],
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!course) return R.error(res, 'Course not found.', 404);
|
||||
if (!unit) return R.error(res, 'Unit not found.', 404);
|
||||
if (!lesson) return R.error(res, 'Lesson not found.', 404);
|
||||
|
||||
const result = await upsertLessonRead(userId, {
|
||||
courseId: course.course_id,
|
||||
courseUuid: course.uuid,
|
||||
unitId: unit.unit_id,
|
||||
unitUuid: unit.uuid,
|
||||
lessonUuid: lesson.uuid,
|
||||
lessonStatus: status,
|
||||
});
|
||||
|
||||
logActivity(userId, 'lesson_read', {
|
||||
entityType: 'lesson',
|
||||
entityId: lesson.lesson_id,
|
||||
details: { lesson_uuid: lesson.uuid, status },
|
||||
});
|
||||
|
||||
return R.success(res, 'Progress updated.', result, 200);
|
||||
} catch (err) {
|
||||
console.error('[CLIENT][COURSE READING PROGRESS][UPSERT]', err);
|
||||
return R.error(res, 'Could not update progress.', 500);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user