add more things

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-18 14:30:13 +08:00
parent 90e28cf1d0
commit 4e142fd90d
2 changed files with 138 additions and 0 deletions
@@ -6,6 +6,10 @@
* GET /client/courses/in-progress
* → courses where the current user has status = 'in_progress', with lesson counts
*
* GET /client/courses/completed
* → every completed lesson/unit/course for the current user, course-scoped and
* standalone reads unioned together, most-recently-completed first
*
* GET /client/courses/:courseId/progress/summary
* → compact snapshot: lesson counts + percentage + course status
*
@@ -32,6 +36,8 @@ const logActivity = require('../../utils/logActivity.util');
const { recomputeCascade, recordWatchProgress, recordManualComplete } = require('../../services/completion_requirements.service');
const { recordPlaybackPosition } = require('../../services/playback_position.service');
const CourseReadingProgress = require('../../models/courses/course_reading_progress.mdl');
const UnitReadingProgress = require('../../models/courses/unit_reading_progress.mdl');
const LessonReadingProgress = require('../../models/courses/lesson_reading_progress.mdl');
const Certificate = require('../../models/courses/certificate.mdl');
const {
Course, Unit, Lesson,
@@ -193,6 +199,135 @@ exports.getMyInProgressCourses = async (req, res) => {
}
};
// =============================================================================
// ── COMPLETED CONTENT — "live view" of every finished lesson/unit/course ──────
// =============================================================================
// GET /client/courses/completed
// Unions the two completion systems (see completion_requirements.service.js header):
// - CourseReadingProgress — course-scoped lessons/units/courses (course_id NOT NULL)
// - Unit/LessonReadingProgress, filtered to course_id IS NULL — genuinely standalone
// reads. Course-scoped reads also get a best-effort mirror written into these same
// tables (see recomputeCascade's mirrorLessonRead call) but that mirror always
// carries a course_id, so the IS NULL filter here excludes it and avoids double-
// counting the same completion from both systems.
exports.getMyCompletedContent = async (req, res) => {
try {
const userId = req.user.user_id;
const courseScoped = await CourseReadingProgress.findAll({
where: { user_id: userId, status: 'completed' },
attributes: ['reference_id', 'type', 'completed_at'],
include: [{
model: Course,
as: 'course',
attributes: ['course_id', 'uuid', 'title'],
where: notDeleted,
required: true,
}],
});
const completedCourseRows = courseScoped.filter((r) => r.type === 'course');
const completedUnitRows = courseScoped.filter((r) => r.type === 'unit');
const completedLessonRows = courseScoped.filter((r) => r.type === 'lesson');
const unitUuids = completedUnitRows.map((r) => r.reference_id);
const lessonUuids = completedLessonRows.map((r) => r.reference_id);
const [unitRows, lessonRows, standaloneUnits, standaloneLessons] = await Promise.all([
unitUuids.length
? Unit.findAll({ where: { uuid: unitUuids, ...notDeleted }, attributes: ['unit_id', 'uuid', 'title'] })
: [],
lessonUuids.length
? Lesson.findAll({ where: { uuid: lessonUuids, ...notDeleted }, attributes: ['lesson_id', 'uuid', 'title'] })
: [],
UnitReadingProgress.findAll({
where: { user_id: userId, status: 'completed', course_id: null },
attributes: ['completed_at'],
include: [{
model: Unit, as: 'unit', attributes: ['unit_id', 'uuid', 'title'], where: notDeleted, required: true,
}],
}),
LessonReadingProgress.findAll({
where: { user_id: userId, status: 'completed', course_id: null },
attributes: ['completed_at'],
include: [{
model: Lesson, as: 'lesson', attributes: ['lesson_id', 'uuid', 'title'], where: notDeleted, required: true,
}],
}),
]);
const unitByUuid = Object.fromEntries(unitRows.map((u) => [u.uuid, u]));
const lessonByUuid = Object.fromEntries(lessonRows.map((l) => [l.uuid, l]));
const courseIds = completedCourseRows.map((r) => r.course.course_id);
const certificates = courseIds.length
? await Certificate.findAll({
where: { user_id: userId, course_id: courseIds },
attributes: ['uuid', 'cert_no', 'issued_at', 'score', 'course_id'],
})
: [];
const certByCourseId = Object.fromEntries(certificates.map((c) => [String(c.course_id), c]));
const courses = completedCourseRows.map((r) => {
const cert = certByCourseId[String(r.course.course_id)] ?? null;
return {
course_id: r.course.course_id,
uuid: r.course.uuid,
title: r.course.title,
completed_at: r.completed_at,
certificate: cert ? { uuid: cert.uuid, cert_no: cert.cert_no, issued_at: cert.issued_at, score: cert.score } : null,
};
}).sort((a, b) => new Date(b.completed_at) - new Date(a.completed_at));
const units = [
...completedUnitRows
.filter((r) => unitByUuid[r.reference_id])
.map((r) => ({
unit_id: unitByUuid[r.reference_id].unit_id,
uuid: r.reference_id,
title: unitByUuid[r.reference_id].title,
completed_at: r.completed_at,
course: { course_id: r.course.course_id, title: r.course.title },
})),
...standaloneUnits.map((r) => ({
unit_id: r.unit.unit_id,
uuid: r.unit.uuid,
title: r.unit.title,
completed_at: r.completed_at,
course: null,
})),
].sort((a, b) => new Date(b.completed_at) - new Date(a.completed_at));
const lessons = [
...completedLessonRows
.filter((r) => lessonByUuid[r.reference_id])
.map((r) => ({
lesson_id: lessonByUuid[r.reference_id].lesson_id,
uuid: r.reference_id,
title: lessonByUuid[r.reference_id].title,
completed_at: r.completed_at,
course: { course_id: r.course.course_id, title: r.course.title },
})),
...standaloneLessons.map((r) => ({
lesson_id: r.lesson.lesson_id,
uuid: r.lesson.uuid,
title: r.lesson.title,
completed_at: r.completed_at,
course: null,
})),
].sort((a, b) => new Date(b.completed_at) - new Date(a.completed_at));
return R.success(res, 'Completed content retrieved.', {
courses, units, lessons,
counts: { courses: courses.length, units: units.length, lessons: lessons.length },
});
} catch (err) {
console.error('[CLIENT][COURSE READING PROGRESS][COMPLETED CONTENT]', err);
return R.error(res, 'Could not retrieve completed content.', 500);
}
};
// =============================================================================
// ── GET PROGRESS SUMMARY ──────────────────────────────────────────────────────
// =============================================================================
+3
View File
@@ -9,6 +9,9 @@ router.get('/categories', ctrl.getCategories);
// Profile learning progress card — must come before /:courseId
router.get('/in-progress', progressCtrl.getMyInProgressCourses);
// Live view of every completed lesson/unit/course — must come before /:courseId
router.get('/completed', progressCtrl.getMyCompletedContent);
// UUID lookups (task requirement blocks — must come before /:courseId)
router.get('/uuid/:uuid', ctrl.getCourseByUuid);
router.get('/unit/uuid/:uuid/lessons', ctrl.getLessonsByUnitUuid);