mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
merged
This commit is contained in:
@@ -23,10 +23,10 @@ const { filterableFields } = require('../../models/task/task.attributes');
|
|||||||
const TASK_LIST_FIELDS = ['name', 'description', 'createdAt', 'updatedAt', 'deletedAt'];
|
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', 'status', 'createdAt', 'updatedAt', 'deletedAt'];
|
||||||
const FILTERABLE_MODELS = {
|
const FILTERABLE_MODELS = {
|
||||||
TaskList: TaskList,
|
TaskList: TaskList,
|
||||||
Task: Task,
|
Task: Task,
|
||||||
TaskRequirement: TaskRequirement,
|
TaskRequirement: TaskRequirement,
|
||||||
TaskListGroup: TaskListGroup,
|
TaskListGroup: TaskListGroup,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Reusable group include for getTaskList / getArchivedTaskList ─────────────
|
// ─── Reusable group include for getTaskList / getArchivedTaskList ─────────────
|
||||||
@@ -773,21 +773,21 @@ exports.bulkRestoreTasks = async (req, res) => {
|
|||||||
|
|
||||||
// ─── GET TASK FIELD VALUES ───────────────────────────────────────────────────
|
// ─── GET TASK FIELD VALUES ───────────────────────────────────────────────────
|
||||||
exports.getTaskFieldValues = async (req, res) => {
|
exports.getTaskFieldValues = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { field } = req.query;
|
const { field } = req.query;
|
||||||
if (!field) return R.error(res, "Field is required.", 400);
|
if (!field) return R.error(res, "Field is required.", 400);
|
||||||
|
|
||||||
const allowedFields = Object.keys(Task.rawAttributes).filter(
|
const allowedFields = Object.keys(Task.rawAttributes).filter(
|
||||||
(f) => Task.rawAttributes[f].filterable
|
(f) => Task.rawAttributes[f].filterable
|
||||||
);
|
);
|
||||||
const auditByFields = ["createdBy", "updatedBy", "deletedBy"];
|
const auditByFields = ["createdBy", "updatedBy", "deletedBy"];
|
||||||
const dateFields = ["createdAt", "updatedAt", "deletedAt", "deadline"];
|
const dateFields = ["createdAt", "updatedAt", "deletedAt", "deadline"];
|
||||||
|
|
||||||
if (!allowedFields.includes(field))
|
if (!allowedFields.includes(field) && !dateFields.includes(field))
|
||||||
return R.error(res, "Invalid or restricted field.", 400);
|
return R.error(res, "Invalid or restricted field.", 400);
|
||||||
|
|
||||||
if (auditByFields.includes(field)) {
|
if (auditByFields.includes(field)) {
|
||||||
const [rows] = await sequelize.query(`
|
const [rows] = await sequelize.query(`
|
||||||
SELECT DISTINCT u."personal_info"->'name'->>'full_name' AS value
|
SELECT DISTINCT u."personal_info"->'name'->>'full_name' AS value
|
||||||
FROM tasks t
|
FROM tasks t
|
||||||
JOIN users u ON u.user_id = t."${field}"
|
JOIN users u ON u.user_id = t."${field}"
|
||||||
@@ -795,28 +795,78 @@ exports.getTaskFieldValues = async (req, res) => {
|
|||||||
AND u."personal_info"->'name'->>'full_name' IS NOT NULL
|
AND u."personal_info"->'name'->>'full_name' IS NOT NULL
|
||||||
ORDER BY value ASC
|
ORDER BY value ASC
|
||||||
`);
|
`);
|
||||||
return R.success(res, "Field values retrieved.", rows.map((r) => r.value).filter(Boolean));
|
return R.success(res, "Field values retrieved.", rows.map((r) => r.value).filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateFields.includes(field)) {
|
||||||
|
const results = await Task.findAll({
|
||||||
|
attributes: [[Sequelize.fn("DISTINCT", Sequelize.fn("DATE", Sequelize.col(field))), "value"]],
|
||||||
|
where: { [field]: { [Op.ne]: null } },
|
||||||
|
order: [[Sequelize.fn("DATE", Sequelize.col(field)), "DESC"]],
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Task.findAll({
|
||||||
|
attributes: [[Sequelize.fn("DISTINCT", Sequelize.col(field)), "value"]],
|
||||||
|
where: { [field]: { [Op.ne]: null } },
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean).sort());
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[TASK][GET FIELD VALUES]", err);
|
||||||
|
return R.error(res, "Could not retrieve field values.", 500);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (dateFields.includes(field)) {
|
// ─── GET TASK LIST FIELD VALUES ───────────────────────────────────────────────
|
||||||
const results = await Task.findAll({
|
exports.getTaskListFieldValues = async (req, res) => {
|
||||||
attributes: [[Sequelize.fn("DISTINCT", Sequelize.fn("DATE", Sequelize.col(field))), "value"]],
|
try {
|
||||||
where: { [field]: { [Op.ne]: null } },
|
const { field } = req.query;
|
||||||
order: [[Sequelize.fn("DATE", Sequelize.col(field)), "DESC"]],
|
if (!field) return R.error(res, 'Field is required.', 400);
|
||||||
raw: true,
|
|
||||||
});
|
const allowedFields = Object.keys(TaskList.rawAttributes).filter(
|
||||||
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean));
|
(f) => TaskList.rawAttributes[f].filterable
|
||||||
|
);
|
||||||
|
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
|
||||||
|
const dateFields = ['createdAt', 'updatedAt', 'deletedAt'];
|
||||||
|
|
||||||
|
if (!allowedFields.includes(field) && !dateFields.includes(field))
|
||||||
|
return R.error(res, "Invalid or restricted field.", 400);
|
||||||
|
|
||||||
|
if (auditByFields.includes(field)) {
|
||||||
|
const [rows] = await sequelize.query(`
|
||||||
|
SELECT DISTINCT u."personal_info"->'name'->>'full_name' AS value
|
||||||
|
FROM task_lists tl
|
||||||
|
JOIN users u ON u.user_id = tl."${field}"
|
||||||
|
WHERE tl."${field}" IS NOT NULL
|
||||||
|
AND u."personal_info"->'name'->>'full_name' IS NOT NULL
|
||||||
|
ORDER BY value ASC
|
||||||
|
`);
|
||||||
|
return R.success(res, 'Field values retrieved.', rows.map((r) => r.value).filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateFields.includes(field)) {
|
||||||
|
const results = await TaskList.findAll({
|
||||||
|
attributes: [[Sequelize.fn('DISTINCT', Sequelize.fn('DATE', Sequelize.col(field))), 'value']],
|
||||||
|
where: { [field]: { [Op.ne]: null } },
|
||||||
|
order: [[Sequelize.fn('DATE', Sequelize.col(field)), 'DESC']],
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await TaskList.findAll({
|
||||||
|
attributes: [[Sequelize.fn('DISTINCT', Sequelize.col(field)), 'value']],
|
||||||
|
where: { [field]: { [Op.ne]: null } },
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean).sort());
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[TASK LIST][GET FIELD VALUES]', err);
|
||||||
|
return R.error(res, 'Could not retrieve field values.', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Task.findAll({
|
|
||||||
attributes: [[Sequelize.fn("DISTINCT", Sequelize.col(field)), "value"]],
|
|
||||||
where: { [field]: { [Op.ne]: null } },
|
|
||||||
raw: true,
|
|
||||||
});
|
|
||||||
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean).sort());
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[TASK][GET FIELD VALUES]", err);
|
|
||||||
return R.error(res, "Could not retrieve field values.", 500);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
@@ -8,13 +8,13 @@ const adminExclude = [
|
|||||||
const userExclude = [
|
const userExclude = [
|
||||||
...excludeAttributes,
|
...excludeAttributes,
|
||||||
// regular users cannot see audit trails
|
// regular users cannot see audit trails
|
||||||
|
"createdBy", "updatedBy", "deletedBy", "deletedAt",
|
||||||
];
|
];
|
||||||
|
|
||||||
const jsonbSchemas = {};
|
const jsonbSchemas = {};
|
||||||
|
|
||||||
const computedAttributes = [];
|
const computedAttributes = [];
|
||||||
|
|
||||||
const filterableFields = {}
|
const filterableFields = {}
|
||||||
|
|
||||||
module.exports = { adminExclude, userExclude, excludeAttributes, jsonbSchemas, computedAttributes, filterableFields };
|
module.exports = { adminExclude, userExclude, excludeAttributes, jsonbSchemas, computedAttributes, filterableFields };
|
||||||
+11
-17
@@ -5,7 +5,7 @@ const { mdl_UserGroups } = require('../users/user_groups.mdl');
|
|||||||
const TaskList = sequelize.define('TaskList', {
|
const TaskList = sequelize.define('TaskList', {
|
||||||
task_list_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, hidden: true },
|
task_list_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, hidden: true },
|
||||||
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, filterable: true },
|
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, filterable: true },
|
||||||
description: { type: DataTypes.TEXT, allowNull: true, hidden: true },
|
description: { type: DataTypes.TEXT, allowNull: true, hidden: true, filterable: false },
|
||||||
|
|
||||||
// ── Audit trails ────────────────────────────────────────────────────────
|
// ── Audit trails ────────────────────────────────────────────────────────
|
||||||
createdBy: { type: DataTypes.INTEGER, allowNull: true },
|
createdBy: { type: DataTypes.INTEGER, allowNull: true },
|
||||||
@@ -18,8 +18,6 @@ const TaskList = sequelize.define('TaskList', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ─── Junction: TaskList ↔ UserGroups ──────────────────────────────────────────
|
// ─── Junction: TaskList ↔ UserGroups ──────────────────────────────────────────
|
||||||
// Tracks which groups a task list is visible to / assigned to, plus who assigned
|
|
||||||
// it and when. Stored in `task_list_groups` so it lives alongside `task_lists`.
|
|
||||||
const TaskListGroup = sequelize.define('TaskListGroup', {
|
const TaskListGroup = sequelize.define('TaskListGroup', {
|
||||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||||
task_list_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'task_lists', key: 'task_list_id' }, onDelete: 'CASCADE', },
|
task_list_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'task_lists', key: 'task_list_id' }, onDelete: 'CASCADE', },
|
||||||
@@ -39,9 +37,9 @@ const TaskListGroup = sequelize.define('TaskListGroup', {
|
|||||||
|
|
||||||
const Task = sequelize.define('Task', {
|
const Task = sequelize.define('Task', {
|
||||||
task_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, hidden: true },
|
task_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, hidden: true },
|
||||||
task_list_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'task_lists', key: 'task_list_id' }, hidden: true },
|
task_list_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'task_lists', key: 'task_list_id' }, hidden: true, filterable: false },
|
||||||
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, order: 1, filterable: true },
|
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, order: 1, filterable: true },
|
||||||
description: { type: DataTypes.TEXT, allowNull: true, hidden: true, },
|
description: { type: DataTypes.TEXT, allowNull: true, hidden: true, filterable: false },
|
||||||
deadline: { type: DataTypes.DATE, allowNull: true, order: 2, filterable: true },
|
deadline: { type: DataTypes.DATE, allowNull: true, order: 2, filterable: true },
|
||||||
status: { type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'overdue'), defaultValue: 'pending', allowNull: false, filterable: true },
|
status: { type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'overdue'), defaultValue: 'pending', allowNull: false, filterable: true },
|
||||||
|
|
||||||
@@ -61,16 +59,16 @@ const TaskRequirement = sequelize.define('TaskRequirement', {
|
|||||||
type: { type: DataTypes.ENUM('visit_link', 'upload_file', 'read_course', 'read_unit', 'read_lesson'), allowNull: false, filterable: true },
|
type: { type: DataTypes.ENUM('visit_link', 'upload_file', 'read_course', 'read_unit', 'read_lesson'), allowNull: false, filterable: true },
|
||||||
|
|
||||||
// ── visit_link ──────────────────────────────────────────────────────────
|
// ── visit_link ──────────────────────────────────────────────────────────
|
||||||
link_url: { type: DataTypes.STRING, allowNull: true },
|
link_url: { type: DataTypes.STRING, allowNull: true, filterable: false },
|
||||||
link_label: { type: DataTypes.STRING, allowNull: true },
|
link_label: { type: DataTypes.STRING, allowNull: true, filterable: false },
|
||||||
|
|
||||||
// ── upload_file ─────────────────────────────────────────────────────────
|
// ── upload_file ─────────────────────────────────────────────────────────
|
||||||
allowed_file_types: { type: DataTypes.JSONB, allowNull: true },
|
allowed_file_types: { type: DataTypes.JSONB, allowNull: true, filterable: false },
|
||||||
max_file_count: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 1 },
|
max_file_count: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 1, filterable: false },
|
||||||
|
|
||||||
// ── read_course / read_unit / read_lesson ────────────────────────────────
|
// ── read_course / read_unit / read_lesson ────────────────────────────────
|
||||||
reference_id: { type: DataTypes.UUID, allowNull: true, comment: 'course_id | unit_id | lesson_id depending on type' },
|
reference_id: { type: DataTypes.UUID, allowNull: true, comment: 'course_id | unit_id | lesson_id depending on type', filterable: false },
|
||||||
reference_label: { type: DataTypes.STRING, allowNull: true, comment: 'Cached display name so we do not always join' },
|
reference_label: { type: DataTypes.STRING, allowNull: true, comment: 'Cached display name so we do not always join', filterable: false },
|
||||||
order: { type: DataTypes.INTEGER, defaultValue: 0, filterable: true },
|
order: { type: DataTypes.INTEGER, defaultValue: 0, filterable: true },
|
||||||
|
|
||||||
// ── Audit trails ────────────────────────────────────────────────────────
|
// ── Audit trails ────────────────────────────────────────────────────────
|
||||||
@@ -87,13 +85,9 @@ TaskList.hasMany(Task, { foreignKey: 'task_list_id', as: 'tasks' });
|
|||||||
Task.belongsTo(TaskList, { foreignKey: 'task_list_id', as: 'taskList' });
|
Task.belongsTo(TaskList, { foreignKey: 'task_list_id', as: 'taskList' });
|
||||||
Task.hasMany(TaskRequirement, { foreignKey: 'task_id', as: 'requirements' });
|
Task.hasMany(TaskRequirement, { foreignKey: 'task_id', as: 'requirements' });
|
||||||
TaskRequirement.belongsTo(Task, { foreignKey: 'task_id', as: 'task' });
|
TaskRequirement.belongsTo(Task, { foreignKey: 'task_id', as: 'task' });
|
||||||
|
|
||||||
// TaskList ↔ UserGroups (many-to-many through TaskListGroup)
|
// TaskList ↔ UserGroups (many-to-many through TaskListGroup)
|
||||||
TaskList.belongsToMany(mdl_UserGroups, { through: TaskListGroup, foreignKey: 'task_list_id', otherKey: 'group_id', as: 'groups', });
|
TaskList.belongsToMany(mdl_UserGroups, { through: TaskListGroup, foreignKey: 'task_list_id', otherKey: 'group_id', as: 'groups', });
|
||||||
mdl_UserGroups.belongsToMany(TaskList, {
|
mdl_UserGroups.belongsToMany(TaskList, { through: TaskListGroup, foreignKey: 'group_id', otherKey: 'task_list_id', as: 'taskLists', });
|
||||||
through: TaskListGroup, foreignKey: 'group_id',
|
|
||||||
otherKey: 'task_list_id',
|
|
||||||
as: 'taskLists',
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = { Task, TaskList, TaskRequirement, TaskListGroup, mdl_UserGroups };
|
module.exports = { Task, TaskList, TaskRequirement, TaskListGroup, mdl_UserGroups };
|
||||||
@@ -14,6 +14,7 @@ router.get("/:taskListId", controller.getTaskList);
|
|||||||
router.patch("/:taskListId", sensitiveOpsLimiter, controller.updateTaskList);
|
router.patch("/:taskListId", sensitiveOpsLimiter, controller.updateTaskList);
|
||||||
router.delete("/:taskListId", sensitiveOpsLimiter, controller.archiveTaskList);
|
router.delete("/:taskListId", sensitiveOpsLimiter, controller.archiveTaskList);
|
||||||
router.patch("/:taskListId/restore", sensitiveOpsLimiter, controller.restoreTaskList);
|
router.patch("/:taskListId/restore", sensitiveOpsLimiter, controller.restoreTaskList);
|
||||||
|
router.get("/:taskListId/tasks/field-values", controller.getTaskFieldValues);
|
||||||
|
|
||||||
// ─── Task List Groups ─────────────────────────────────────────────────────────
|
// ─── Task List Groups ─────────────────────────────────────────────────────────
|
||||||
router.get("/:taskListId/groups", controller.getTaskListGroups);
|
router.get("/:taskListId/groups", controller.getTaskListGroups);
|
||||||
|
|||||||
Reference in New Issue
Block a user