mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
813 lines
33 KiB
JavaScript
813 lines
33 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name: tasks.ctrl.js (staff)
|
|
* Type of Program: Controller
|
|
* Description: Staff-level task management — Task Lists and Tasks scoped to the
|
|
* staff member's groups. Aligned with admin task.controller.js:
|
|
* uses transactions, archiveOne/archiveMany, restoreOne/restoreMany,
|
|
* paginate, and consistent R response helpers throughout.
|
|
*
|
|
* Author: Kenneth Obsequio (@lash0000)
|
|
* Date Created: May 17, 2026
|
|
***********************************************************************************************************************************************************************/
|
|
const { Op } = require('sequelize');
|
|
const sequelize = require('../../config/db.config');
|
|
|
|
const { Task, TaskList, TaskRequirement, TaskListGroup } = require('../../models/task/task.mdl');
|
|
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
|
|
const mdl_Users = require('../../models/users/users.mdl');
|
|
const { staffExclude, jsonbSchemas, computedAttributes } = require('../../models/task/task.attributes');
|
|
|
|
const R = require('../../utils/response.util');
|
|
const { paginate } = require('../../utils/paginate.util');
|
|
const { archiveOne, archiveMany } = require('../../utils/courses/archive.util');
|
|
const { restoreOne, restoreMany } = require('../../utils/courses/restore.util');
|
|
const { getFieldValues } = require('../../utils/fieldValues.util');
|
|
|
|
// ─── Allowed filter/sort fields ───────────────────────────────────────────────
|
|
const TASK_LIST_FIELDS = ['name', 'description', 'createdAt', 'updatedAt', 'deletedAt'];
|
|
const TASK_FIELDS = ['name', 'description', 'deadline', 'status', 'createdAt', 'updatedAt', 'deletedAt'];
|
|
|
|
// ─── Reusable group include ───────────────────────────────────────────────────
|
|
const GROUP_INCLUDE = {
|
|
model: mdl_UserGroups,
|
|
as: 'groups',
|
|
attributes: ['group_id', 'name', 'group_code'],
|
|
through: {
|
|
model: TaskListGroup,
|
|
as: 'assignment',
|
|
attributes: ['assignedAt'],
|
|
},
|
|
};
|
|
|
|
// ─── Helper: get group IDs scoped to this staff member ───────────────────────
|
|
async function getStaffGroupIds(staffUserId) {
|
|
const memberships = await mdl_UserGroupMembers.findAll({
|
|
where: { user_id: staffUserId },
|
|
attributes: ['group_id'],
|
|
raw: true,
|
|
});
|
|
return memberships.map((m) => m.group_id);
|
|
}
|
|
|
|
// ─── Helper: assert staff has access to at least one group of a task list ────
|
|
async function assertTaskListAccess(staffUserId, taskListId, t) {
|
|
const scopedGroupIds = await getStaffGroupIds(staffUserId);
|
|
if (!scopedGroupIds.length) return false;
|
|
const access = await TaskListGroup.findOne({
|
|
where: { task_list_id: taskListId, group_id: { [Op.in]: scopedGroupIds } },
|
|
transaction: t,
|
|
});
|
|
return !!access;
|
|
}
|
|
|
|
// =============================================================================
|
|
// ── TASK LISTS ────────────────────────────────────────────────────────────────
|
|
// =============================================================================
|
|
|
|
// ─── GET ALL ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.getTaskLists = async (req, res) => {
|
|
try {
|
|
const staffUserId = req.user.user_id;
|
|
const { group_id } = req.query;
|
|
|
|
let scopedGroupIds = await getStaffGroupIds(staffUserId);
|
|
if (!scopedGroupIds.length)
|
|
return R.success(res, 'Task lists retrieved.', { rows: [], count: 0 });
|
|
|
|
if (group_id) {
|
|
const gid = Number(group_id);
|
|
if (!scopedGroupIds.map(Number).includes(gid))
|
|
return R.error(res, 'No access to this group.', 403);
|
|
scopedGroupIds = [gid];
|
|
}
|
|
|
|
// Find task list IDs accessible to this staff member
|
|
const assignments = await TaskListGroup.findAll({
|
|
where: { group_id: { [Op.in]: scopedGroupIds } },
|
|
attributes: ['task_list_id'],
|
|
raw: true,
|
|
});
|
|
const accessibleIds = [...new Set(assignments.map((a) => a.task_list_id))];
|
|
|
|
const result = await paginate(TaskList, req, {
|
|
excludeAttributes: staffExclude,
|
|
jsonbSchemas,
|
|
computedAttributes,
|
|
auditOptions: { mdl_Users, parentAlias: 'TaskList' },
|
|
allowedFields: TASK_LIST_FIELDS,
|
|
findOptions: {
|
|
distinct: true, // ← count parent rows, not JOIN rows
|
|
col: 'task_list_id', // ← count on PK, not inflated join tuples
|
|
where: { task_list_id: { [Op.in]: accessibleIds } },
|
|
include: [GROUP_INCLUDE],
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Task lists retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[STAFF][GET ALL TASK LISTS]', err);
|
|
return R.error(res, 'Could not retrieve task lists.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET ONE ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.getTaskList = async (req, res) => {
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId)))
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
|
|
const taskList = await TaskList.findByPk(taskListId, {
|
|
attributes: { exclude: staffExclude },
|
|
paranoid: false,
|
|
include: [
|
|
{
|
|
model: Task, as: 'tasks', paranoid: false,
|
|
include: [{
|
|
model: TaskRequirement,
|
|
as: 'requirements',
|
|
paranoid: false,
|
|
attributes: { exclude: staffExclude },
|
|
order: [['order', 'ASC']],
|
|
}],
|
|
},
|
|
GROUP_INCLUDE,
|
|
],
|
|
});
|
|
|
|
if (!taskList) return R.error(res, 'Task list not found.', 404);
|
|
|
|
const data = taskList.toJSON();
|
|
data.group_count = data.groups?.length ?? 0;
|
|
|
|
return R.success(res, 'Task list retrieved.', data);
|
|
} catch (err) {
|
|
console.error('[STAFF][GET TASK LIST]', err);
|
|
return R.error(res, 'Could not retrieve task list.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── CREATE ───────────────────────────────────────────────────────────────────
|
|
|
|
exports.createTaskList = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const staffUserId = req.user.user_id;
|
|
const { name, description, group_ids } = req.body;
|
|
|
|
if (!name) { await t.rollback(); return R.error(res, 'Name is required.', 400); }
|
|
if (!Array.isArray(group_ids) || !group_ids.length) {
|
|
await t.rollback();
|
|
return R.error(res, 'group_ids is required.', 400);
|
|
}
|
|
|
|
// Verify staff has access to all requested groups
|
|
const scopedGroupIds = (await getStaffGroupIds(staffUserId)).map(String);
|
|
const normalizedIds = group_ids.map(String);
|
|
const unauthorized = normalizedIds.filter((id) => !scopedGroupIds.includes(id));
|
|
if (unauthorized.length) {
|
|
await t.rollback();
|
|
return R.error(res, `No access to group(s): ${unauthorized.join(', ')}`, 403);
|
|
}
|
|
|
|
const taskList = await TaskList.create(
|
|
{ name, description, createdBy: staffUserId, updatedBy: staffUserId },
|
|
{ transaction: t }
|
|
);
|
|
|
|
await TaskListGroup.bulkCreate(
|
|
normalizedIds.map((gid) => ({
|
|
task_list_id: taskList.task_list_id,
|
|
group_id: gid,
|
|
assignedAt: new Date(),
|
|
assignedBy: staffUserId,
|
|
})),
|
|
{ transaction: t }
|
|
);
|
|
|
|
await t.commit();
|
|
|
|
const full = await TaskList.findByPk(taskList.task_list_id, {
|
|
attributes: { exclude: staffExclude },
|
|
include: [GROUP_INCLUDE],
|
|
});
|
|
|
|
return R.success(res, 'Task list created successfully.', full, 201);
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][CREATE TASK LIST]', err);
|
|
return R.error(res, 'Could not create task list.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
|
|
|
exports.updateTaskList = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
const taskList = await TaskList.findByPk(taskListId, { transaction: t });
|
|
if (!taskList) { await t.rollback(); return R.error(res, 'Task list not found.', 404); }
|
|
|
|
const { name, description } = req.body;
|
|
await taskList.update({ name, description, updatedBy: staffUserId }, { transaction: t });
|
|
|
|
await t.commit();
|
|
return R.success(res, 'Task list updated successfully.', taskList);
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][UPDATE TASK LIST]', err);
|
|
return R.error(res, 'Could not update task list.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── ARCHIVE (soft-delete) ────────────────────────────────────────────────────
|
|
|
|
exports.archiveTaskList = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
const record = await archiveOne(TaskList, { task_list_id: taskListId }, staffUserId, t);
|
|
if (!record) { await t.rollback(); return R.error(res, 'Task list not found.', 404); }
|
|
|
|
await t.commit();
|
|
return R.success(res, 'Task list archived successfully.');
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][ARCHIVE TASK LIST]', err);
|
|
return R.error(res, 'Could not archive task list.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── RESTORE ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.restoreTaskList = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
const record = await restoreOne(
|
|
TaskList,
|
|
{ task_list_id: taskListId, deletedAt: { [Op.not]: null } },
|
|
staffUserId,
|
|
t
|
|
);
|
|
if (!record) { await t.rollback(); return R.error(res, 'Task list not found or not archived.', 404); }
|
|
|
|
await t.commit();
|
|
return R.success(res, 'Task list restored successfully.', record);
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][RESTORE TASK LIST]', err);
|
|
return R.error(res, 'Could not restore task list.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK ARCHIVE ─────────────────────────────────────────────────────────────
|
|
|
|
exports.bulkArchiveTaskLists = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const staffUserId = req.user.user_id;
|
|
const { ids } = req.body;
|
|
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No task list IDs provided.', 400);
|
|
|
|
// Scope: only IDs the staff can access
|
|
const scopedGroupIds = await getStaffGroupIds(staffUserId);
|
|
const assignments = await TaskListGroup.findAll({
|
|
where: { task_list_id: { [Op.in]: ids }, group_id: { [Op.in]: scopedGroupIds } },
|
|
attributes: ['task_list_id'],
|
|
raw: true,
|
|
});
|
|
const accessibleIds = [...new Set(assignments.map((a) => String(a.task_list_id)))];
|
|
const requestedIds = ids.map(String);
|
|
const unauthorizedIds = requestedIds.filter((id) => !accessibleIds.includes(id));
|
|
|
|
if (!accessibleIds.length)
|
|
return R.error(res, 'No accessible task lists found.', 404);
|
|
|
|
const taskLists = await TaskList.findAll({ where: { task_list_id: accessibleIds } });
|
|
const activeIds = taskLists.filter((tl) => !tl.deletedAt).map((tl) => String(tl.task_list_id));
|
|
|
|
if (!activeIds.length) {
|
|
await t.rollback();
|
|
return R.error(res, 'All selected task lists are already archived.', 400);
|
|
}
|
|
|
|
const count = await archiveMany(TaskList, 'task_list_id', activeIds, staffUserId, t);
|
|
await t.commit();
|
|
return R.success(res, `${count} task list(s) archived successfully.`, {
|
|
archived_ids: activeIds,
|
|
skipped_ids: requestedIds.filter((id) => !activeIds.includes(id)),
|
|
unauthorized_ids: unauthorizedIds,
|
|
});
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][BULK ARCHIVE TASK LISTS]', err);
|
|
return R.error(res, 'Could not archive task lists.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK RESTORE ─────────────────────────────────────────────────────────────
|
|
|
|
exports.bulkRestoreTaskLists = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const staffUserId = req.user.user_id;
|
|
const { ids } = req.body;
|
|
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No task list IDs provided.', 400);
|
|
|
|
// Scope: only IDs the staff can access
|
|
const scopedGroupIds = await getStaffGroupIds(staffUserId);
|
|
const assignments = await TaskListGroup.findAll({
|
|
where: { task_list_id: { [Op.in]: ids }, group_id: { [Op.in]: scopedGroupIds } },
|
|
attributes: ['task_list_id'],
|
|
raw: true,
|
|
});
|
|
const accessibleIds = [...new Set(assignments.map((a) => String(a.task_list_id)))];
|
|
const requestedIds = ids.map(String);
|
|
const unauthorizedIds = requestedIds.filter((id) => !accessibleIds.includes(id));
|
|
|
|
if (!accessibleIds.length)
|
|
return R.error(res, 'No accessible task lists found.', 404);
|
|
|
|
const taskLists = await TaskList.findAll({
|
|
where: { task_list_id: accessibleIds },
|
|
paranoid: false,
|
|
});
|
|
const deletedIds = taskLists.filter((tl) => tl.deletedAt).map((tl) => String(tl.task_list_id));
|
|
|
|
if (!deletedIds.length) {
|
|
await t.rollback();
|
|
return R.error(res, 'All selected task lists are already active.', 400);
|
|
}
|
|
|
|
const count = await restoreMany(TaskList, 'task_list_id', deletedIds, staffUserId, t);
|
|
await t.commit();
|
|
return R.success(res, `${count} task list(s) restored successfully.`, {
|
|
restored_ids: deletedIds,
|
|
skipped_ids: requestedIds.filter((id) => !deletedIds.includes(id)),
|
|
unauthorized_ids: unauthorizedIds,
|
|
});
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][BULK RESTORE TASK LISTS]', err);
|
|
return R.error(res, 'Could not restore task lists.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET ARCHIVED TASK LISTS ──────────────────────────────────────────────────
|
|
|
|
exports.getArchivedTaskLists = async (req, res) => {
|
|
try {
|
|
const staffUserId = req.user.user_id;
|
|
const scopedGroupIds = await getStaffGroupIds(staffUserId);
|
|
|
|
if (!scopedGroupIds.length)
|
|
return R.success(res, 'Archived task lists retrieved.', { rows: [], count: 0 });
|
|
|
|
const assignments = await TaskListGroup.findAll({
|
|
where: { group_id: { [Op.in]: scopedGroupIds } },
|
|
attributes: ['task_list_id'],
|
|
raw: true,
|
|
});
|
|
const accessibleIds = [...new Set(assignments.map((a) => a.task_list_id))];
|
|
|
|
const result = await paginate(TaskList, req, {
|
|
excludeAttributes: staffExclude,
|
|
jsonbSchemas,
|
|
computedAttributes,
|
|
auditOptions: { mdl_Users, parentAlias: 'TaskList' },
|
|
allowedFields: TASK_LIST_FIELDS,
|
|
findOptions: {
|
|
paranoid: false,
|
|
where: {
|
|
task_list_id: { [Op.in]: accessibleIds },
|
|
deletedAt: { [Op.not]: null },
|
|
},
|
|
include: [GROUP_INCLUDE],
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Archived task lists retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[STAFF][GET ARCHIVED TASK LISTS]', err);
|
|
return R.error(res, 'Could not retrieve archived task lists.', 500);
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// ── TASKS ─────────────────────────────────────────────────────────────────────
|
|
// =============================================================================
|
|
|
|
// ─── GET ALL ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.getTasks = async (req, res) => {
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId)))
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
|
|
const result = await paginate(Task, req, {
|
|
excludeAttributes: staffExclude,
|
|
jsonbSchemas,
|
|
computedAttributes,
|
|
auditOptions: { mdl_Users, parentAlias: 'Task' },
|
|
allowedFields: TASK_FIELDS,
|
|
findOptions: {
|
|
where: { task_list_id: taskListId },
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Tasks retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[STAFF][GET ALL TASKS]', err);
|
|
return R.error(res, 'Could not retrieve tasks.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET ONE ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.getTask = async (req, res) => {
|
|
try {
|
|
const { taskListId, taskId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId)))
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
|
|
const task = await Task.findOne({
|
|
where: { task_id: taskId, task_list_id: taskListId },
|
|
attributes: { exclude: staffExclude },
|
|
paranoid: false,
|
|
include: [
|
|
{
|
|
model: TaskRequirement,
|
|
as: 'requirements',
|
|
paranoid: false,
|
|
attributes: { exclude: staffExclude },
|
|
order: [['order', 'ASC']],
|
|
},
|
|
{
|
|
model: TaskList,
|
|
as: 'taskList',
|
|
paranoid: false,
|
|
attributes: { exclude: staffExclude },
|
|
include: [GROUP_INCLUDE],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!task) return R.error(res, 'Task not found.', 404);
|
|
|
|
return R.success(res, 'Task retrieved.', task);
|
|
} catch (err) {
|
|
console.error('[STAFF][GET TASK]', err);
|
|
return R.error(res, 'Could not retrieve task.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── CREATE ───────────────────────────────────────────────────────────────────
|
|
|
|
exports.createTask = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
const { name, description, deadline, requirements = [] } = req.body;
|
|
|
|
if (!name) { await t.rollback(); return R.error(res, 'Task name is required.', 400); }
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
const taskList = await TaskList.findByPk(taskListId, { transaction: t });
|
|
if (!taskList) { await t.rollback(); return R.error(res, 'Task list not found.', 404); }
|
|
|
|
const task = await Task.create(
|
|
{
|
|
task_list_id: taskListId,
|
|
name,
|
|
description,
|
|
deadline: deadline || null,
|
|
status: 'pending',
|
|
createdBy: staffUserId,
|
|
updatedBy: staffUserId,
|
|
},
|
|
{ transaction: t }
|
|
);
|
|
|
|
if (requirements.length) {
|
|
const reqRows = requirements.map((r, i) => ({
|
|
...r,
|
|
task_id: task.task_id,
|
|
order: r.order ?? i,
|
|
reference_id: r.reference_id || null,
|
|
reference_label: r.reference_label || null,
|
|
link_url: r.link_url || null,
|
|
link_label: r.link_label || null,
|
|
createdBy: staffUserId,
|
|
updatedBy: staffUserId,
|
|
}));
|
|
await TaskRequirement.bulkCreate(reqRows, { transaction: t });
|
|
}
|
|
|
|
await t.commit();
|
|
|
|
const full = await Task.findByPk(task.task_id, {
|
|
attributes: { exclude: staffExclude },
|
|
include: [{
|
|
model: TaskRequirement,
|
|
as: 'requirements',
|
|
attributes: { exclude: staffExclude },
|
|
order: [['order', 'ASC']],
|
|
}],
|
|
});
|
|
|
|
return R.success(res, 'Task created successfully.', full, 201);
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][CREATE TASK]', err);
|
|
return R.error(res, 'Could not create task.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
|
|
|
exports.updateTask = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId, taskId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
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 { name, description, deadline, status, requirements } = req.body;
|
|
|
|
await task.update(
|
|
{ name, description, deadline: deadline || null, status, updatedBy: staffUserId },
|
|
{ transaction: t }
|
|
);
|
|
|
|
if (Array.isArray(requirements)) {
|
|
await TaskRequirement.destroy({
|
|
where: { task_id: taskId },
|
|
force: false,
|
|
transaction: t,
|
|
});
|
|
|
|
if (requirements.length) {
|
|
const reqRows = requirements.map((r, i) => ({
|
|
...r,
|
|
task_id: task.task_id,
|
|
order: r.order ?? i,
|
|
reference_id: r.reference_id || null,
|
|
reference_label: r.reference_label || null,
|
|
link_url: r.link_url || null,
|
|
link_label: r.link_label || null,
|
|
createdBy: staffUserId,
|
|
updatedBy: staffUserId,
|
|
}));
|
|
await TaskRequirement.bulkCreate(reqRows, { transaction: t });
|
|
}
|
|
}
|
|
|
|
await t.commit();
|
|
|
|
const full = await Task.findByPk(taskId, {
|
|
attributes: { exclude: staffExclude },
|
|
include: [{
|
|
model: TaskRequirement,
|
|
as: 'requirements',
|
|
attributes: { exclude: staffExclude },
|
|
order: [['order', 'ASC']],
|
|
}],
|
|
});
|
|
|
|
return R.success(res, 'Task updated successfully.', full);
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][UPDATE TASK]', err);
|
|
return R.error(res, 'Could not update task.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── ARCHIVE (soft-delete) ────────────────────────────────────────────────────
|
|
|
|
exports.archiveTask = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId, taskId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
const record = await archiveOne(
|
|
Task,
|
|
{ task_id: taskId, task_list_id: taskListId },
|
|
staffUserId,
|
|
t
|
|
);
|
|
if (!record) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
|
|
|
|
await t.commit();
|
|
return R.success(res, 'Task archived successfully.');
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][ARCHIVE TASK]', err);
|
|
return R.error(res, 'Could not archive task.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── RESTORE ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.restoreTask = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId, taskId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
const record = await restoreOne(
|
|
Task,
|
|
{ task_id: taskId, task_list_id: taskListId, deletedAt: { [Op.not]: null } },
|
|
staffUserId,
|
|
t
|
|
);
|
|
if (!record) { await t.rollback(); return R.error(res, 'Task not found or not archived.', 404); }
|
|
|
|
await t.commit();
|
|
return R.success(res, 'Task restored successfully.', record);
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][RESTORE TASK]', err);
|
|
return R.error(res, 'Could not restore task.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK ARCHIVE ─────────────────────────────────────────────────────────────
|
|
|
|
exports.bulkArchiveTasks = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
const { ids } = req.body;
|
|
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No task IDs provided.', 400);
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
const tasks = await Task.findAll({
|
|
where: { task_id: ids, task_list_id: taskListId },
|
|
});
|
|
if (!tasks.length) return R.error(res, 'No tasks found.', 404);
|
|
|
|
const activeIds = tasks.filter((task) => !task.deletedAt).map((task) => task.task_id);
|
|
if (!activeIds.length) {
|
|
await t.rollback();
|
|
return R.error(res, 'All selected tasks are already archived.', 400);
|
|
}
|
|
|
|
const count = await archiveMany(Task, 'task_id', activeIds, staffUserId, t);
|
|
await t.commit();
|
|
return R.success(res, `${count} task(s) archived successfully.`, {
|
|
archived_ids: activeIds,
|
|
skipped_ids: ids.filter((id) => !activeIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][BULK ARCHIVE TASKS]', err);
|
|
return R.error(res, 'Could not archive tasks.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK RESTORE ─────────────────────────────────────────────────────────────
|
|
|
|
exports.bulkRestoreTasks = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
const { ids } = req.body;
|
|
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No task IDs provided.', 400);
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId, t))) {
|
|
await t.rollback();
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
}
|
|
|
|
const tasks = await Task.findAll({
|
|
where: { task_id: ids, task_list_id: taskListId },
|
|
paranoid: false,
|
|
});
|
|
if (!tasks.length) return R.error(res, 'No tasks found.', 404);
|
|
|
|
const deletedIds = tasks.filter((task) => task.deletedAt).map((task) => task.task_id);
|
|
if (!deletedIds.length) {
|
|
await t.rollback();
|
|
return R.error(res, 'All selected tasks are already active.', 400);
|
|
}
|
|
|
|
const count = await restoreMany(Task, 'task_id', deletedIds, staffUserId, t);
|
|
await t.commit();
|
|
return R.success(res, `${count} task(s) restored successfully.`, {
|
|
restored_ids: deletedIds,
|
|
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[STAFF][BULK RESTORE TASKS]', err);
|
|
return R.error(res, 'Could not restore tasks.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET ARCHIVED TASKS ───────────────────────────────────────────────────────
|
|
|
|
exports.getArchivedTasks = async (req, res) => {
|
|
try {
|
|
const { taskListId } = req.params;
|
|
const staffUserId = req.user.user_id;
|
|
|
|
if (!(await assertTaskListAccess(staffUserId, taskListId)))
|
|
return R.error(res, 'No access to this task list.', 403);
|
|
|
|
const result = await paginate(Task, req, {
|
|
excludeAttributes: staffExclude,
|
|
jsonbSchemas,
|
|
computedAttributes,
|
|
auditOptions: { mdl_Users, parentAlias: 'Task' },
|
|
allowedFields: TASK_FIELDS,
|
|
findOptions: {
|
|
paranoid: false,
|
|
where: {
|
|
task_list_id: taskListId,
|
|
deletedAt: { [Op.not]: null },
|
|
},
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Archived tasks retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[STAFF][GET ARCHIVED TASKS]', err);
|
|
return R.error(res, 'Could not retrieve archived tasks.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── Field value helpers (for filter dropdowns) ───────────────────────────────
|
|
exports.getTaskFieldValues = getFieldValues(Task, 'TASK');
|
|
exports.getTaskListFieldValues = getFieldValues(TaskList, 'TASKLIST'); |