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
@@ -14,6 +14,9 @@ const sequelize = require('../../config/db.config');
const { TaskCompletion, TaskCompletionFile } = require('../../models/task/task_completion.mdl');
const { Task } = require('../../models/task/task.mdl');
const mdl_Users = require('../../models/users/users.mdl');
const UserNotification = require('../../models/notifications/user_notification.mdl');
const { renderNotification } = require('../../services/notificationTemplate.service');
const { checkTaskCompletion, fireTaskCompletedEvent } = require('../client/task.controller');
const { adminExclude } = require('../../models/task/task_completion.attributes');
const R = require('../../utils/response.util');
@@ -140,6 +143,69 @@ exports.getCompletionsByUser = async (req, res) => {
}
};
// ─── REVIEW ───────────────────────────────────────────────────────────────────
// PATCH /admin/task-lists/:taskListId/tasks/:taskId/completions/:completionId/review
// Approves/rejects a submission for a requirement flagged requires_review.
// Notifies the submitting learner via the existing template pattern (same shape
// as updateTask's task_requirements_updated notify block).
exports.reviewSubmission = async (req, res) => {
const t = await sequelize.transaction();
try {
const { taskListId, taskId, completionId } = req.params;
const { status, review_note } = req.body;
if (!['approved', 'rejected'].includes(status)) {
await t.rollback();
return R.error(res, 'status must be "approved" or "rejected".', 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); }
const completion = await TaskCompletion.findOne({
where: { completion_id: completionId, task_id: taskId },
transaction: t,
});
if (!completion) { await t.rollback(); return R.error(res, 'Completion not found.', 404); }
const wasComplete = status === 'approved' ? await checkTaskCompletion(completion.user_id, taskId) : false;
await completion.update({
status,
review_note: review_note || null,
reviewed_by: req.user.user_id,
reviewed_at: new Date(),
updatedBy: req.user.user_id,
}, { transaction: t });
await t.commit();
if (status === 'approved' && !wasComplete && await checkTaskCompletion(completion.user_id, taskId)) {
fireTaskCompletedEvent(completion.user_id, taskId); // fire-and-forget
}
logActivity(req.user.user_id, 'review_task_submission', {
entityType: 'task_completion', entityId: completionId, details: { task_id: taskId, status },
});
try {
const notify = await renderNotification({ type: 'task_submission_reviewed', data: {
taskName: task.name, status, review_note: review_note || null,
} });
await UserNotification.create({ user_id: completion.user_id, ...notify, seen: false });
} catch (notifyErr) {
console.error('[ADMIN][REVIEW SUBMISSION][NOTIFY]', notifyErr);
}
return R.success(res, 'Submission reviewed.', completion);
} catch (err) {
await t.rollback();
console.error('[ADMIN][REVIEW SUBMISSION]', err);
return R.error(res, 'Could not review submission.', 500);
}
};
// ─── ARCHIVE ──────────────────────────────────────────────────────────────────
// DELETE /admin/task-lists/:taskListId/tasks/:taskId/completions/:completionId