Files
starr-philproperties/controllers/admin/course_reading_progress.controller.js
T
kennethobsequio bb7e8fde08 add: ver()
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-07-08 11:31:40 +08:00

198 lines
8.6 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name: course_reading_progress.controller.js (admin)
* Type of Program: Controller
* Description: Admin-facing endpoints for viewing course reading progress.
*
* GET /:courseId/reading-progress
* → one entry per user who has touched the course, with lesson/unit counts aggregated
*
* GET /:courseId/reading-progress/users/:userId
* → full lesson + unit breakdown for a single user (loaded on row expand)
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 21, 2026
***********************************************************************************************************************************************************************/
'use strict';
const { Op } = require('sequelize');
const R = require('../../utils/response.util');
const CourseReadingProgress = require('../../models/courses/course_reading_progress.mdl');
const { Course, Unit, Lesson, CourseUnit } = require('../../models/courses/courses.associations');
const mdl_Users = require('../../models/users/users.mdl');
const { countCourseLessons, getCourseUnitIds, flattenUnits } = require('../../utils/courses/hierarchy.util');
const notDeleted = { deletedAt: null };
// =============================================================================
// ── LIST — all users who touched this course ──────────────────────────────────
// =============================================================================
// GET /admin/courses/:courseId/reading-progress
// Returns one summary row per user. Lesson + unit counts are derived by querying
// the course structure server-side so the totals are always accurate.
exports.getCourseReadingProgress = async (req, res) => {
try {
const { courseId } = req.params;
const course = await Course.findOne({
where: { course_id: courseId, ...notDeleted },
attributes: ['course_id'],
});
if (!course) return R.error(res, 'Course not found.', 404);
// Count total lessons and units in the course (structure totals via junctions)
const [unitIds, lessons_total] = await Promise.all([
getCourseUnitIds(courseId),
countCourseLessons(courseId),
]);
const units_total = unitIds.length;
// All progress rows for this course, grouped per user
const rows = await CourseReadingProgress.findAll({
where: { course_id: courseId },
attributes: ['user_id', 'reference_id', 'type', 'status', 'last_accessed_at'],
order: [['last_accessed_at', 'DESC']],
});
if (!rows.length) return R.success(res, 'No reading progress for this course yet.', []);
// Aggregate per user
const userIds = [...new Set(rows.map((r) => r.user_id))];
const users = await mdl_Users.findAll({
where: { user_id: userIds },
attributes: ['user_id', 'email', 'personal_info'],
});
const userMap = Object.fromEntries(users.map((u) => [u.user_id, u]));
// Build per-user summary
const summaryMap = {};
for (const row of rows) {
if (!summaryMap[row.user_id]) {
summaryMap[row.user_id] = {
user_id: row.user_id,
course_status: null,
last_accessed_at: null,
lessons_completed: 0,
units_completed: 0,
};
}
const entry = summaryMap[row.user_id];
// Track latest access across all rows for this user
if (!entry.last_accessed_at || new Date(row.last_accessed_at) > new Date(entry.last_accessed_at)) {
entry.last_accessed_at = row.last_accessed_at;
}
if (row.type === 'course') entry.course_status = row.status;
if (row.type === 'unit' && row.status === 'completed') entry.units_completed++;
if (row.type === 'lesson' && row.status === 'completed') entry.lessons_completed++;
}
const result = Object.values(summaryMap).map((entry) => {
const u = userMap[entry.user_id];
return {
...entry,
user: {
email: u?.email ?? null,
full_name: u?.personal_info?.name?.full_name ?? null,
avatar_url: u?.personal_info?.avatar?.url ?? null,
},
units_total,
lessons_total,
// Fall back to in_progress if the course row hasn't been written yet
course_status: entry.course_status ?? 'in_progress',
};
});
// Sort: completed last, most recent first within each group
result.sort((a, b) => {
if (a.course_status !== b.course_status) {
return a.course_status === 'completed' ? 1 : -1;
}
return new Date(b.last_accessed_at) - new Date(a.last_accessed_at);
});
return R.success(res, 'Course reading progress retrieved.', result);
} catch (err) {
console.error('[ADMIN][COURSE READING PROGRESS][LIST]', err);
return R.error(res, 'Could not retrieve reading progress.', 500);
}
};
// =============================================================================
// ── DETAIL — single user's full lesson/unit breakdown ─────────────────────────
// =============================================================================
// GET /admin/courses/:courseId/reading-progress/users/:userId
// Loaded lazily when the admin expands a user row.
// Returns units with their lessons and the progress status per item.
exports.getUserReadingProgress = async (req, res) => {
try {
const { courseId, userId } = req.params;
const [unitRows, progressRows] = await Promise.all([
Unit.findAll({
where: notDeleted,
attributes: ['unit_id', 'uuid', 'title'],
include: [
{
model: CourseUnit,
as: 'courseLinks',
where: { course_id: courseId },
required: true,
attributes: ['order_index'],
},
{
model: Lesson,
as: 'lessons',
where: notDeleted,
required: false,
attributes: ['lesson_id', 'uuid', 'title'],
through: { attributes: ['order_index'] },
},
],
}),
CourseReadingProgress.findAll({
where: { course_id: courseId, user_id: userId },
attributes: ['reference_id', 'type', 'status', 'completed_at', 'last_accessed_at'],
}),
]);
// Sort by junction order (course-level, then unit-level for lessons)
const units = flattenUnits(unitRows.map((u) => {
const plain = u.toJSON();
plain.CourseUnit = { order_index: plain.courseLinks?.[0]?.order_index ?? 0 };
delete plain.courseLinks;
return plain;
}));
// Build a quick lookup: { [reference_id (uuid)]: status }
const progressMap = Object.fromEntries(
progressRows.map((r) => [r.reference_id, { status: r.status, completed_at: r.completed_at }])
);
const breakdown = units.map((unit) => ({
unit_id: unit.unit_id,
uuid: unit.uuid,
title: unit.title,
status: progressMap[unit.uuid]?.status ?? null,
lessons: (unit.lessons ?? []).map((lesson) => ({
lesson_id: lesson.lesson_id,
uuid: lesson.uuid,
title: lesson.title,
status: progressMap[lesson.uuid]?.status ?? null,
completed_at: progressMap[lesson.uuid]?.completed_at ?? null,
})),
}));
return R.success(res, 'User reading progress retrieved.', breakdown);
} catch (err) {
console.error('[ADMIN][COURSE READING PROGRESS][USER DETAIL]', err);
return R.error(res, 'Could not retrieve user reading progress.', 500);
}
};