units,lesson as standalone

This commit is contained in:
2026-07-10 11:44:46 +08:00
parent 86fba50b95
commit e1ffdab190
46 changed files with 1463 additions and 197 deletions
+55 -1
View File
@@ -25,11 +25,13 @@ const { Op } = require('sequelize');
const sequelize = require('../../config/db.config');
const { Task, TaskRequirement } = require('../../models/task/task.mdl');
const { assertTaskUnlocked, checkTaskCompletion, fireTaskCompletedEvent } = require('./task.controller');
const { TaskLinkVisit, TaskProgress } = require('../../models/task/task_progress.mdl');
const { mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
const { Course } = require('../../models/courses/courses.mdl');
const CourseAssessment = require('../../models/courses/course_assessment.mdl');
const QuizAttempt = require('../../models/courses/quiz_attempt.mdl');
const UnitQuiz = require('../../models/courses/unit_quiz.mdl');
const logActivity = require('../../utils/logActivity.util');
const R = require('../../utils/response.util');
const { hydrateReadTaskProgress } = require('../../services/task_reading_progress_sync.service');
@@ -139,6 +141,35 @@ exports.getTaskProgress = async (req, res) => {
await hydrateReadTaskProgress(req.user.user_id, readRequirements);
// pass_quiz completion is computed live from QuizAttempt (same source
// has_passed already uses for unit quizzes) rather than a synced
// TaskProgress row — reference_id on the requirement is the quiz's
// uuid, so resolve to quiz_id first.
const quizRequirements = await TaskRequirement.findAll({
where: { task_id: taskId, type: 'pass_quiz' },
attributes: ['requirement_id', 'reference_id'],
});
const quizPassedRows = [];
if (quizRequirements.length) {
const quizUuids = [...new Set(quizRequirements.map((r) => r.reference_id).filter(Boolean))];
const quizzes = await UnitQuiz.findAll({ where: { uuid: quizUuids }, attributes: ['quiz_id', 'uuid'] });
const quizIdByUuid = new Map(quizzes.map((q) => [q.uuid, q.quiz_id]));
const quizIds = quizzes.map((q) => q.quiz_id);
const passedAttempts = quizIds.length
? await QuizAttempt.findAll({ where: { quiz_id: { [Op.in]: quizIds }, user_id: req.user.user_id, passed: true }, attributes: ['quiz_id'] })
: [];
const passedQuizIds = new Set(passedAttempts.map((a) => String(a.quiz_id)));
for (const r of quizRequirements) {
const quizId = quizIdByUuid.get(r.reference_id);
quizPassedRows.push({
requirement_id: r.requirement_id,
reference_id: r.reference_id,
completed: quizId ? passedQuizIds.has(String(quizId)) : false,
completed_at: null,
});
}
}
const [linkVisits, progress] = await Promise.all([
TaskLinkVisit.findAll({
where: { task_id: taskId, user_id: req.user.user_id },
@@ -150,7 +181,10 @@ exports.getTaskProgress = async (req, res) => {
}),
]);
return R.success(res, 'Task progress retrieved.', { link_visits: linkVisits, progress });
return R.success(res, 'Task progress retrieved.', {
link_visits: linkVisits,
progress: [...progress, ...quizPassedRows],
});
} catch (err) {
console.error('[CLIENT][GET TASK PROGRESS]', err);
return R.error(res, 'Could not retrieve task progress.', 500);
@@ -182,6 +216,15 @@ exports.visitLink = async (req, res) => {
return R.error(res, 'Requirement is not a visit_link type.', 400);
}
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId }, transaction: t });
if (!task) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index))) {
await t.rollback();
return R.error(res, 'Complete the earlier required tasks in this list first.', 409);
}
const wasComplete = await checkTaskCompletion(req.user.user_id, taskId);
const now = new Date();
const [record, created] = await TaskLinkVisit.upsert(
@@ -210,6 +253,10 @@ exports.visitLink = async (req, res) => {
});
}
if (!wasComplete && await checkTaskCompletion(req.user.user_id, taskId)) {
fireTaskCompletedEvent(req.user.user_id, taskId); // fire-and-forget
}
return R.success(
res,
created ? 'Link visited.' : 'Link visit updated.',
@@ -302,6 +349,7 @@ exports.updateProgress = async (req, res) => {
const now = new Date();
const userId = req.user.user_id;
const wasComplete = await checkTaskCompletion(userId, taskId);
// ── Direct UPSERT for read_unit / read_course ─────────────────────────
if (requirement.type === 'read_unit' || requirement.type === 'read_course') {
@@ -323,6 +371,9 @@ exports.updateProgress = async (req, res) => {
}
);
await t.commit();
if (!wasComplete && await checkTaskCompletion(userId, taskId)) {
fireTaskCompletedEvent(userId, taskId); // fire-and-forget
}
return R.success(res, 'Progress updated.', {
requirement_id: requirementId,
reference_id,
@@ -403,6 +454,9 @@ exports.updateProgress = async (req, res) => {
}
await t.commit();
if (!wasComplete && await checkTaskCompletion(userId, taskId)) {
fireTaskCompletedEvent(userId, taskId); // fire-and-forget
}
return R.success(res, 'Progress updated.', {
requirement_id: requirementId,
reference_id,