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
+66 -4
View File
@@ -26,6 +26,7 @@ const { restoreOne, restoreMany } = require("../../utils/courses/restore.util");
const { permanentDeleteOne, permanentDeleteMany } = require("../../utils/courses/permanentDelete.util");
const { TaskCompletion } = require('../../models/task/task_completion.mdl');
const logActivity = require('../../utils/logActivity.util');
const { nextOrderIndex, reorderJunction } = require('../../utils/courses/hierarchy.util');
// ─── URL normalizer — ensures protocol prefix so link_url is never a relative path ──
const normalizeUrl = (url) => {
@@ -36,7 +37,7 @@ const normalizeUrl = (url) => {
// ─── Allowed filter/sort fields ───────────────────────────────────────────────
const TASK_LIST_FIELDS = ['name', 'description', 'createdAt', 'updatedAt', 'deletedAt'];
const TASK_FIELDS = ['name', 'description', 'deadline', 'status', 'createdAt', 'updatedAt', 'deletedAt'];
const TASK_FIELDS = ['name', 'description', 'deadline', 'order_index', 'is_required', 'status', 'createdAt', 'updatedAt', 'deletedAt'];
const FILTERABLE_MODELS = {
TaskList: TaskList,
Task: Task,
@@ -420,6 +421,38 @@ exports.assignGroups = async (req, res) => {
await t.commit();
logActivity(req.user.user_id, 'assign_groups', { entityType: 'task_list', entityId: Number(taskListId), details: { group_ids: newIds } });
// ── Notify every member of the newly-assigned group(s) ─────────────────
if (newIds.length) {
try {
const taskCount = await Task.count({ where: { task_list_id: taskListId } });
const memberRows = await mdl_UserGroupMembers.findAll({
where: { group_id: newIds },
attributes: ['user_id'],
});
const seenUsers = new Set();
const userIds = memberRows.filter(({ user_id }) => {
if (seenUsers.has(user_id)) return false;
seenUsers.add(user_id);
return true;
}).map((m) => m.user_id);
if (userIds.length) {
const now = new Date();
const notify = await renderNotification({ type: 'task_assigned', data: {
taskListName: taskList.name,
taskCount,
} });
await UserNotification.bulkCreate(
userIds.map((user_id) => ({ user_id, ...notify, seen: false, createdAt: now, updatedAt: now })),
{ validate: false }
);
}
} catch (notifyErr) {
console.error('[ADMIN][ASSIGN GROUPS][NOTIFY]', notifyErr);
}
}
return R.success(res, `${newIds.length} group(s) assigned.`, {
assigned_ids: newIds,
already_assigned_ids: existingIds,
@@ -556,7 +589,7 @@ exports.createTask = async (req, res) => {
const t = await sequelize.transaction();
try {
const { taskListId } = req.params;
const { name, description, deadline, requirements = [] } = req.body;
const { name, description, deadline, is_required, requirements = [] } = req.body;
if (!name) return R.error(res, 'Task name is required.', 400);
@@ -566,12 +599,16 @@ exports.createTask = async (req, res) => {
return R.error(res, 'Task list not found.', 404);
}
const order_index = await nextOrderIndex(Task, { task_list_id: taskListId }, t);
const task = await Task.create(
{
task_list_id: taskListId,
name,
description,
deadline: deadline || null,
order_index,
is_required: is_required ?? true,
createdBy: req.user.user_id,
updatedBy: req.user.user_id,
},
@@ -630,10 +667,10 @@ exports.updateTask = async (req, res) => {
return R.error(res, 'Task not found.', 404);
}
const { name, description, deadline, status, requirements } = req.body;
const { name, description, deadline, status, is_required, requirements } = req.body;
await task.update(
{ name, description, deadline: deadline || null, status, updatedBy: req.user.user_id },
{ name, description, deadline: deadline || null, status, is_required, updatedBy: req.user.user_id },
{ transaction: t }
);
@@ -757,6 +794,31 @@ exports.updateTask = async (req, res) => {
}
};
// ─── REORDER ──────────────────────────────────────────────────────────────────
// PATCH /admin/task-lists/:taskListId/tasks/order { task_ids: [orderedIds] }
exports.reorderTasks = async (req, res) => {
const t = await sequelize.transaction();
try {
const { taskListId } = req.params;
const { task_ids = [] } = req.body;
if (!task_ids.length) { await t.rollback(); return R.error(res, 'task_ids is required.', 400); }
const taskList = await TaskList.findByPk(taskListId, { transaction: t });
if (!taskList) { await t.rollback(); return R.error(res, 'Task list not found.', 404); }
await reorderJunction(Task, 'task_list_id', taskListId, 'task_id', task_ids, t);
await t.commit();
logActivity(req.user.user_id, 'reorder_tasks', { entityType: 'task_list', entityId: taskListId, details: { task_ids } });
return R.success(res, 'Task order updated.');
} catch (err) {
await t.rollback();
console.error('[ADMIN][REORDER TASKS]', err);
return R.error(res, 'Could not reorder tasks.', 500);
}
};
// ─── GET ARCHIVED TASK LISTS ──────────────────────────────────────────────────
exports.getArchivedTaskLists = async (req, res) => {