mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
281 lines
13 KiB
JavaScript
281 lines
13 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name: task_completion.controller.js (admin)
|
|
* Type of Program: Controller
|
|
* Description: Admin-level task completion management.
|
|
* Admins can view all completions per task, view a single completion,
|
|
* and archive/restore completions. Completions are created by clients only.
|
|
*
|
|
* Author: Kenneth Obsequio (@lash0000)
|
|
* Date Created: Jun. 13, 2026
|
|
***********************************************************************************************************************************************************************/
|
|
const { Op, Sequelize } = require('sequelize');
|
|
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 { adminExclude } = require('../../models/task/task_completion.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 logActivity = require('../../utils/logActivity.util');
|
|
|
|
// ─── Allowed filter/sort fields ───────────────────────────────────────────────
|
|
const COMPLETION_FIELDS = ['submitted_at', 'createdAt', 'updatedAt', 'deletedAt'];
|
|
|
|
// ─── Reusable include: completion files ───────────────────────────────────────
|
|
// separate: true → Sequelize fetches files in a second SELECT ... WHERE completion_id IN (...)
|
|
// instead of a JOIN, which avoids the subquery alias conflict that occurs when
|
|
// paginate applies LIMIT/OFFSET alongside a hasMany include.
|
|
const FILES_INCLUDE = {
|
|
model: TaskCompletionFile,
|
|
as: 'files',
|
|
attributes: { exclude: adminExclude },
|
|
paranoid: false,
|
|
separate: true,
|
|
order: [['createdAt', 'ASC']],
|
|
};
|
|
|
|
// ─── Reusable include: submitting user ────────────────────────────────────────
|
|
const USER_INCLUDE = {
|
|
model: mdl_Users,
|
|
as: 'user',
|
|
attributes: [
|
|
'user_id',
|
|
'email', // ← direct column, fine as-is
|
|
[
|
|
Sequelize.literal(`("user"."personal_info"->'name'->>'full_name')`),
|
|
'name',
|
|
],
|
|
],
|
|
};
|
|
|
|
// =============================================================================
|
|
// ── COMPLETIONS (nested under task-list → task) ───────────────────────────────
|
|
// =============================================================================
|
|
|
|
// ─── GET ALL ──────────────────────────────────────────────────────────────────
|
|
// GET /admin/task-lists/:taskListId/tasks/:taskId/completions
|
|
|
|
exports.getCompletions = async (req, res) => {
|
|
try {
|
|
const { taskListId, taskId } = req.params;
|
|
|
|
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId } });
|
|
if (!task) return R.error(res, 'Task not found.', 404);
|
|
|
|
const result = await paginate(TaskCompletion, req, {
|
|
excludeAttributes: adminExclude,
|
|
jsonbSchemas: {},
|
|
computedAttributes: [],
|
|
auditOptions: { mdl_Users, parentAlias: 'TaskCompletion' },
|
|
allowedFields: COMPLETION_FIELDS,
|
|
findOptions: {
|
|
where: { task_id: taskId },
|
|
include: [USER_INCLUDE, FILES_INCLUDE],
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Completions retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET ALL COMPLETIONS]', err);
|
|
return R.error(res, 'Could not retrieve completions.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET ONE ──────────────────────────────────────────────────────────────────
|
|
// GET /admin/task-lists/:taskListId/tasks/:taskId/completions/:completionId
|
|
|
|
exports.getCompletion = async (req, res) => {
|
|
try {
|
|
const { taskListId, taskId, completionId } = req.params;
|
|
|
|
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId } });
|
|
if (!task) return R.error(res, 'Task not found.', 404);
|
|
|
|
const completion = await TaskCompletion.findOne({
|
|
where: { completion_id: completionId, task_id: taskId },
|
|
attributes: { exclude: adminExclude },
|
|
paranoid: false,
|
|
include: [USER_INCLUDE, FILES_INCLUDE],
|
|
});
|
|
|
|
if (!completion) return R.error(res, 'Completion not found.', 404);
|
|
|
|
return R.success(res, 'Completion retrieved.', completion);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET COMPLETION]', err);
|
|
return R.error(res, 'Could not retrieve completion.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET ALL BY USER ──────────────────────────────────────────────────────────
|
|
// GET /admin/task-lists/:taskListId/tasks/:taskId/completions/user/:userId
|
|
|
|
exports.getCompletionsByUser = async (req, res) => {
|
|
try {
|
|
const { taskListId, taskId, userId } = req.params;
|
|
|
|
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId } });
|
|
if (!task) return R.error(res, 'Task not found.', 404);
|
|
|
|
const result = await paginate(TaskCompletion, req, {
|
|
excludeAttributes: adminExclude,
|
|
jsonbSchemas: {},
|
|
computedAttributes: [],
|
|
auditOptions: { mdl_Users, parentAlias: 'TaskCompletion' },
|
|
allowedFields: COMPLETION_FIELDS,
|
|
findOptions: {
|
|
where: { task_id: taskId, user_id: userId },
|
|
include: [FILES_INCLUDE],
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'User completions retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET COMPLETIONS BY USER]', err);
|
|
return R.error(res, 'Could not retrieve user completions.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── ARCHIVE ──────────────────────────────────────────────────────────────────
|
|
// DELETE /admin/task-lists/:taskListId/tasks/:taskId/completions/:completionId
|
|
|
|
exports.archiveCompletion = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId, taskId, completionId } = req.params;
|
|
|
|
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 record = await archiveOne(
|
|
TaskCompletion,
|
|
{ completion_id: completionId, task_id: taskId },
|
|
req.user.user_id,
|
|
t
|
|
);
|
|
if (!record) { await t.rollback(); return R.error(res, 'Completion not found.', 404); }
|
|
|
|
await t.commit();
|
|
logActivity(req.user.user_id, 'archive_completion', { entityType: 'task_completion', entityId: Number(completionId), details: { task_id: taskId } });
|
|
return R.success(res, 'Completion archived successfully.');
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[ADMIN][ARCHIVE COMPLETION]', err);
|
|
return R.error(res, 'Could not archive completion.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── RESTORE ──────────────────────────────────────────────────────────────────
|
|
// PATCH /admin/task-lists/:taskListId/tasks/:taskId/completions/:completionId/restore
|
|
|
|
exports.restoreCompletion = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId, taskId, completionId } = req.params;
|
|
|
|
const record = await restoreOne(
|
|
TaskCompletion,
|
|
{ completion_id: completionId, task_id: taskId, deletedAt: { [Op.not]: null } },
|
|
req.user.user_id,
|
|
t
|
|
);
|
|
if (!record) { await t.rollback(); return R.error(res, 'Completion not found or not archived.', 404); }
|
|
|
|
await t.commit();
|
|
logActivity(req.user.user_id, 'restore_completion', { entityType: 'task_completion', entityId: Number(completionId), details: { task_id: taskId } });
|
|
return R.success(res, 'Completion restored successfully.', record);
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[ADMIN][RESTORE COMPLETION]', err);
|
|
return R.error(res, 'Could not restore completion.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK ARCHIVE ─────────────────────────────────────────────────────────────
|
|
// POST /admin/task-lists/:taskListId/tasks/:taskId/completions/bulk-archive
|
|
|
|
exports.bulkArchiveCompletions = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId, taskId } = req.params;
|
|
const { ids } = req.body;
|
|
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No completion IDs provided.', 400);
|
|
|
|
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId } });
|
|
if (!task) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
|
|
|
|
const completions = await TaskCompletion.findAll({
|
|
where: { completion_id: ids, task_id: taskId },
|
|
});
|
|
if (!completions.length) return R.error(res, 'No completions found.', 404);
|
|
|
|
const activeIds = completions
|
|
.filter((c) => !c.deletedAt)
|
|
.map((c) => c.completion_id);
|
|
|
|
if (!activeIds.length)
|
|
return R.error(res, 'All selected completions are already archived.', 400);
|
|
|
|
const count = await archiveMany(TaskCompletion, 'completion_id', activeIds, req.user.user_id, t);
|
|
await t.commit();
|
|
|
|
logActivity(req.user.user_id, 'bulk_archive_completions', { entityType: 'task_completion', details: { ids: activeIds, count, task_id: taskId } });
|
|
return R.success(res, `${count} completion(s) archived successfully.`, {
|
|
archived_ids: activeIds,
|
|
skipped_ids: ids.filter((id) => !activeIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[ADMIN][BULK ARCHIVE COMPLETIONS]', err);
|
|
return R.error(res, 'Could not archive completions.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK RESTORE ─────────────────────────────────────────────────────────────
|
|
// POST /admin/task-lists/:taskListId/tasks/:taskId/completions/bulk-restore
|
|
|
|
exports.bulkRestoreCompletions = async (req, res) => {
|
|
const t = await sequelize.transaction();
|
|
try {
|
|
const { taskListId, taskId } = req.params;
|
|
const { ids } = req.body;
|
|
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No completion IDs provided.', 400);
|
|
|
|
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId } });
|
|
if (!task) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
|
|
|
|
const completions = await TaskCompletion.findAll({
|
|
where: { completion_id: ids, task_id: taskId },
|
|
paranoid: false,
|
|
});
|
|
if (!completions.length) return R.error(res, 'No completions found.', 404);
|
|
|
|
const deletedIds = completions
|
|
.filter((c) => c.deletedAt)
|
|
.map((c) => c.completion_id);
|
|
|
|
if (!deletedIds.length)
|
|
return R.error(res, 'All selected completions are already active.', 400);
|
|
|
|
const count = await restoreMany(TaskCompletion, 'completion_id', deletedIds, req.user.user_id, t);
|
|
await t.commit();
|
|
|
|
logActivity(req.user.user_id, 'bulk_restore_completions', { entityType: 'task_completion', details: { ids: deletedIds, count, task_id: taskId } });
|
|
return R.success(res, `${count} completion(s) restored successfully.`, {
|
|
restored_ids: deletedIds,
|
|
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
await t.rollback();
|
|
console.error('[ADMIN][BULK RESTORE COMPLETIONS]', err);
|
|
return R.error(res, 'Could not restore completions.', 500);
|
|
}
|
|
}; |