mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -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 ──────────────────────────────────────────────────────
|
||||
// =============================================================================
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user