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,196 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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 } = require('../../models/courses/courses.associations');
|
||||
const mdl_Users = require('../../models/users/users.mdl');
|
||||
|
||||
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)
|
||||
const [units, allLessons] = await Promise.all([
|
||||
Unit.findAll({
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: ['unit_id', 'uuid'],
|
||||
}),
|
||||
Lesson.findAll({
|
||||
include: [{
|
||||
model: Unit,
|
||||
as: 'unit',
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: [],
|
||||
required: true,
|
||||
}],
|
||||
where: { ...notDeleted },
|
||||
attributes: ['lesson_id', 'uuid'],
|
||||
}),
|
||||
]);
|
||||
|
||||
const units_total = units.length;
|
||||
const lessons_total = allLessons.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 [units, progressRows] = await Promise.all([
|
||||
Unit.findAll({
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: ['unit_id', 'uuid', 'title', 'order_index'],
|
||||
include: [{
|
||||
model: Lesson,
|
||||
as: 'lessons',
|
||||
where: notDeleted,
|
||||
required: false,
|
||||
attributes: ['lesson_id', 'uuid', 'title', 'order_index'],
|
||||
}],
|
||||
order: [
|
||||
['order_index', 'ASC'],
|
||||
[{ model: Lesson, as: 'lessons' }, 'order_index', 'ASC'],
|
||||
],
|
||||
}),
|
||||
CourseReadingProgress.findAll({
|
||||
where: { course_id: courseId, user_id: userId },
|
||||
attributes: ['reference_id', 'type', 'status', 'completed_at', 'last_accessed_at'],
|
||||
}),
|
||||
]);
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user