mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
197 lines
8.4 KiB
JavaScript
197 lines
8.4 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 } = 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);
|
|
}
|
|
};
|