mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
453 lines
20 KiB
JavaScript
453 lines
20 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name: user_groups.controller.js (admin)
|
|
* Type of Program: Controller
|
|
* Description: Admin-level user group management — CRUD + membership.
|
|
*
|
|
* Author: rgrgogu
|
|
* Date Created: Oct. 6, 2025
|
|
***********************************************************************************************************************************************************************
|
|
* Change History:
|
|
* DATE AUTHOR LOG DESCRIPTION
|
|
* Oct. 6, 2025 rgrgogu 001 Initial creation - STAR Phase 1
|
|
* May 23, 2026 rgrgogu 002 Added group_code to createGroup / updateGroup; added generateGroupCode helper
|
|
***********************************************************************************************************************************************************************/
|
|
const sequelize = require('../../config/db.config');
|
|
const { Op, Sequelize } = require('sequelize');
|
|
|
|
const mdl_Users = require('../../models/users/users.mdl');
|
|
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
|
|
|
|
const R = require('../../utils/response.util');
|
|
const { paginate } = require('../../utils/paginate.util');
|
|
const { getFieldValues } = require('../../utils/fieldValues.util');
|
|
|
|
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
|
|
const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.attributes');
|
|
const logActivity = require('../../utils/logActivity.util');
|
|
|
|
// ─── Helper — generate a unique group code ────────────────────────────────────
|
|
/**
|
|
* Generates a unique group_code in the format: <SLUG>-<4-char hex>
|
|
* e.g. "SALES-A3F1", "ONBOARD-Q1-9C2D"
|
|
* Retries up to 5 times in the unlikely event of a collision.
|
|
*/
|
|
const generateGroupCode = async (name) => {
|
|
const slug = name.toUpperCase().trim().replace(/\s+/g, '-').replace(/[^A-Z0-9\-]/g, '').slice(0, 20);
|
|
for (let i = 0; i < 5; i++) {
|
|
const suffix = Math.random().toString(16).slice(2, 6).toUpperCase();
|
|
const code = `${slug}-${suffix}`;
|
|
const exists = await mdl_UserGroups.findOne({ where: { group_code: code }, paranoid: false });
|
|
if (!exists) return code;
|
|
}
|
|
throw new Error('Could not generate a unique group code after 5 attempts.');
|
|
};
|
|
|
|
// ─── GET ALL ──────────────────────────────────────────────────────────────────
|
|
exports.getGroups = async (req, res) => {
|
|
try {
|
|
const result = await paginate(mdl_UserGroups, req, {
|
|
excludeAttributes: groupExclude,
|
|
jsonbSchemas: groupSchemas,
|
|
computedAttributes: groupComputed,
|
|
context: 'list',
|
|
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
|
});
|
|
|
|
return R.success(res, 'Groups retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET GROUPS]', err);
|
|
return R.error(res, 'Could not retrieve groups.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET ONE ──────────────────────────────────────────────────────────────────
|
|
exports.getGroup = async (req, res) => {
|
|
try {
|
|
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
|
if (!group) return R.error(res, 'Group not found.', 404);
|
|
|
|
const members = await paginate(mdl_Users, req, {
|
|
excludeAttributes: usersExclude,
|
|
jsonbSchemas: usersSchemas,
|
|
jsonbColumn: 'personal_info',
|
|
auditOptions: { mdl_Users, parentAlias: 'User' },
|
|
context: 'list',
|
|
findOptions: {
|
|
include: [{
|
|
model: mdl_UserGroupMembers,
|
|
where: { group_id: req.params.gid },
|
|
attributes: [],
|
|
required: true,
|
|
}],
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Group retrieved.', { group, members });
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET GROUP]', err);
|
|
return R.error(res, 'Could not retrieve group.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── CREATE ───────────────────────────────────────────────────────────────────
|
|
exports.createGroup = async (req, res) => {
|
|
try {
|
|
const { name, description, group_code } = req.body;
|
|
if (!name) return R.error(res, 'Group name is required.', 400);
|
|
|
|
// Use provided group_code (uppercased via model hook), otherwise auto-generate
|
|
const code = group_code
|
|
? group_code.toUpperCase().trim()
|
|
: await generateGroupCode(name);
|
|
|
|
// Check uniqueness explicitly so we return a clear error message
|
|
const duplicate = await mdl_UserGroups.findOne({ where: { group_code: code }, paranoid: false });
|
|
if (duplicate) return R.error(res, `Group code "${code}" is already in use.`, 409);
|
|
|
|
const group = await mdl_UserGroups.create({
|
|
name,
|
|
description,
|
|
group_code: code,
|
|
createdBy: req.user.user_id,
|
|
});
|
|
|
|
logActivity(req.user.user_id, 'create_group', { entityType: 'group', entityId: group.group_id, details: { name: group.name, group_code: group.group_code } });
|
|
return R.success(res, 'Group created.', group, 201);
|
|
} catch (err) {
|
|
console.error('[ADMIN][CREATE GROUP]', err);
|
|
return R.error(res, 'Could not create group.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
|
exports.updateGroup = async (req, res) => {
|
|
try {
|
|
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
|
if (!group) return R.error(res, 'Group not found.', 404);
|
|
|
|
const { name, description, group_code } = req.body;
|
|
|
|
if (name !== undefined) group.name = name;
|
|
if (description !== undefined) group.description = description;
|
|
|
|
if (group_code !== undefined) {
|
|
const code = group_code.toUpperCase().trim();
|
|
const duplicate = await mdl_UserGroups.findOne({
|
|
where: { group_code: code, group_id: { [Op.ne]: group.group_id } },
|
|
paranoid: false,
|
|
});
|
|
if (duplicate) return R.error(res, `Group code "${code}" is already in use.`, 409);
|
|
group.group_code = code;
|
|
}
|
|
|
|
group.updatedBy = req.user.user_id;
|
|
await group.save();
|
|
|
|
logActivity(req.user.user_id, 'update_group', { entityType: 'group', entityId: group.group_id });
|
|
return R.success(res, 'Group updated.', group);
|
|
} catch (err) {
|
|
console.error('[ADMIN][UPDATE GROUP]', err);
|
|
return R.error(res, 'Could not update group.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── DEACTIVATE ───────────────────────────────────────────────────────────────
|
|
exports.deactivateGroup = async (req, res) => {
|
|
try {
|
|
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
|
if (!group) return R.error(res, 'Group not found.', 404);
|
|
if (!group.is_active) return R.error(res, 'Group is already deactivated.', 400);
|
|
|
|
await group.update({ is_active: false, updatedBy: req.user.user_id, deletedBy: req.user.user_id });
|
|
await group.destroy();
|
|
|
|
logActivity(req.user.user_id, 'deactivate_group', { entityType: 'group', entityId: group.group_id });
|
|
return R.success(res, 'Group deactivated.');
|
|
} catch (err) {
|
|
console.error('[ADMIN][DEACTIVATE GROUP]', err);
|
|
return R.error(res, 'Could not deactivate group.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── RESTORE ──────────────────────────────────────────────────────────────────
|
|
exports.restoreGroup = async (req, res) => {
|
|
try {
|
|
const group = await mdl_UserGroups.findOne({ where: { group_id: req.params.gid }, paranoid: false });
|
|
if (!group) return R.error(res, 'Group not found.', 404);
|
|
if (group.is_active) return R.error(res, 'Group is already active.', 400);
|
|
|
|
await group.restore();
|
|
await group.update({ is_active: true, updatedBy: req.user.user_id, deletedBy: null });
|
|
|
|
logActivity(req.user.user_id, 'restore_group', { entityType: 'group', entityId: group.group_id });
|
|
return R.success(res, 'Group restored.');
|
|
} catch (err) {
|
|
console.error('[ADMIN][RESTORE GROUP]', err);
|
|
return R.error(res, 'Could not restore group.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK DEACTIVATE ──────────────────────────────────────────────────────────
|
|
exports.bulkDeactivateGroups = async (req, res) => {
|
|
try {
|
|
const { ids } = req.body;
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No group IDs provided.', 400);
|
|
|
|
const groups = await mdl_UserGroups.findAll({ where: { group_id: ids } });
|
|
if (!groups.length) return R.error(res, 'No groups found.', 404);
|
|
|
|
const activeGroups = groups.filter((g) => g.is_active && !g.deletedAt);
|
|
if (!activeGroups.length)
|
|
return R.error(res, 'All selected groups are already deactivated.', 400);
|
|
|
|
const activeIds = activeGroups.map((g) => g.group_id);
|
|
|
|
await mdl_UserGroups.update(
|
|
{ is_active: false, deletedBy: req.user.user_id },
|
|
{ where: { group_id: activeIds } }
|
|
);
|
|
await mdl_UserGroups.destroy({ where: { group_id: activeIds } });
|
|
|
|
logActivity(req.user.user_id, 'bulk_deactivate_groups', { entityType: 'group', details: { ids: activeIds, count: activeIds.length } });
|
|
return R.success(res, `${activeIds.length} group(s) deactivated successfully.`, {
|
|
deactivated_ids: activeIds,
|
|
skipped_ids: ids.filter((id) => !activeIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
console.error('[ADMIN][BULK DEACTIVATE GROUPS]', err);
|
|
return R.error(res, 'Could not deactivate groups.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK RESTORE ─────────────────────────────────────────────────────────────
|
|
exports.bulkRestoreGroups = async (req, res) => {
|
|
try {
|
|
const { ids } = req.body;
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No group IDs provided.', 400);
|
|
|
|
const groups = await mdl_UserGroups.findAll({ where: { group_id: ids }, paranoid: false });
|
|
if (!groups.length) return R.error(res, 'No groups found.', 404);
|
|
|
|
const deletedGroups = groups.filter((g) => g.deletedAt);
|
|
if (!deletedGroups.length)
|
|
return R.error(res, 'All selected groups are already active.', 400);
|
|
|
|
const deletedIds = deletedGroups.map((g) => g.group_id);
|
|
|
|
await mdl_UserGroups.restore({ where: { group_id: deletedIds } });
|
|
await mdl_UserGroups.update(
|
|
{ is_active: true, updatedBy: req.user.user_id, deletedBy: null },
|
|
{ where: { group_id: deletedIds }, paranoid: false }
|
|
);
|
|
|
|
logActivity(req.user.user_id, 'bulk_restore_groups', { entityType: 'group', details: { ids: deletedIds, count: deletedIds.length } });
|
|
return R.success(res, `${deletedIds.length} group(s) restored successfully.`, {
|
|
restored_ids: deletedIds,
|
|
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
console.error('[ADMIN][BULK RESTORE GROUPS]', err);
|
|
return R.error(res, 'Could not restore groups.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── PERMANENT DELETE ─────────────────────────────────────────────────────────
|
|
exports.permanentlyDeleteGroup = async (req, res) => {
|
|
try {
|
|
const group = await mdl_UserGroups.findOne({ where: { group_id: req.params.gid }, paranoid: false });
|
|
if (!group) return R.error(res, 'Group not found.', 404);
|
|
if (!group.deletedAt) return R.error(res, 'Group must be deactivated before it can be permanently deleted.', 400);
|
|
|
|
await group.destroy({ force: true });
|
|
|
|
logActivity(req.user.user_id, 'permanently_delete_group', { entityType: 'group', entityId: group.group_id });
|
|
return R.success(res, 'Group permanently deleted.');
|
|
} catch (err) {
|
|
console.error('[ADMIN][PERMANENT DELETE GROUP]', err);
|
|
return R.error(res, 'Could not permanently delete group.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK PERMANENT DELETE ────────────────────────────────────────────────────
|
|
exports.bulkPermanentlyDeleteGroups = async (req, res) => {
|
|
try {
|
|
const { ids } = req.body;
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No group IDs provided.', 400);
|
|
|
|
const groups = await mdl_UserGroups.findAll({ where: { group_id: ids }, paranoid: false });
|
|
if (!groups.length) return R.error(res, 'No groups found.', 404);
|
|
|
|
const deletedGroups = groups.filter((g) => g.deletedAt);
|
|
if (!deletedGroups.length)
|
|
return R.error(res, 'All selected groups must be deactivated before they can be permanently deleted.', 400);
|
|
|
|
const deletedIds = deletedGroups.map((g) => g.group_id);
|
|
|
|
await mdl_UserGroups.destroy({ where: { group_id: deletedIds }, force: true });
|
|
|
|
logActivity(req.user.user_id, 'bulk_permanently_delete_groups', { entityType: 'group', details: { ids: deletedIds, count: deletedIds.length } });
|
|
return R.success(res, `${deletedIds.length} group(s) permanently deleted.`, {
|
|
deleted_ids: deletedIds,
|
|
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
console.error('[ADMIN][BULK PERMANENT DELETE GROUPS]', err);
|
|
return R.error(res, 'Could not permanently delete groups.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
|
exports.getArchivedGroups = async (req, res) => {
|
|
try {
|
|
const result = await paginate(mdl_UserGroups, req, {
|
|
excludeAttributes: groupExclude,
|
|
jsonbSchemas: groupSchemas,
|
|
computedAttributes: groupComputed,
|
|
context: 'archived',
|
|
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
|
findOptions: {
|
|
paranoid: false,
|
|
where: { deletedAt: { [Op.ne]: null }, is_active: false },
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Archived groups retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET ARCHIVED GROUPS]', err);
|
|
return R.error(res, 'Could not retrieve archived groups.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
|
|
exports.getGroupFieldValues = getFieldValues(mdl_UserGroups, 'GROUP', {
|
|
blockedFields: ['deletedAt'],
|
|
paranoid: false,
|
|
});
|
|
|
|
// ─── MEMBERSHIP ───────────────────────────────────────────────────────────────
|
|
exports.getUsersNotInGroup = async (req, res) => {
|
|
try {
|
|
const { gid: group_id } = req.params;
|
|
|
|
// Exclude users already in THIS group
|
|
const members = await mdl_UserGroupMembers.findAll({ where: { group_id }, attributes: ['user_id'] });
|
|
const memberIds = members.map((m) => m.user_id);
|
|
|
|
const users = await mdl_Users.findAll({
|
|
where: { user_id: { [Op.notIn]: memberIds.length ? memberIds : [0] } },
|
|
attributes: [
|
|
'user_id',
|
|
[Sequelize.literal(`("User"."personal_info"->'name'->>'full_name')`), 'full_name'],
|
|
// All active groups this user belongs to (excluding NOGRP), comma-separated
|
|
[
|
|
Sequelize.literal(`(
|
|
SELECT STRING_AGG(ug.name || ' (' || ug.group_code || ')', ', ' ORDER BY ug.name)
|
|
FROM user_group_members ugm
|
|
JOIN user_groups ug ON ug.group_id = ugm.group_id
|
|
WHERE ugm.user_id = "User".user_id
|
|
AND ugm."deletedAt" IS NULL
|
|
AND ug."deletedAt" IS NULL
|
|
AND ug.group_code != 'NOGRP'
|
|
)`),
|
|
'current_group',
|
|
],
|
|
],
|
|
});
|
|
|
|
return R.success(res, 'Users fetched.', users);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET USERS NOT IN GROUP]', err);
|
|
return R.error(res, 'Could not fetch users.', 500);
|
|
}
|
|
};
|
|
|
|
exports.getUsersInGroup = async (req, res) => {
|
|
try {
|
|
const { gid: group_id } = req.params;
|
|
|
|
const group = await mdl_UserGroups.findByPk(group_id, {
|
|
include: [{
|
|
model: mdl_Users,
|
|
as: 'members',
|
|
through: { attributes: [] },
|
|
attributes: [
|
|
'user_id',
|
|
[Sequelize.literal(`("members"."personal_info"->'name'->>'full_name')`), 'full_name'],
|
|
],
|
|
}],
|
|
});
|
|
|
|
if (!group) return R.error(res, 'Group not found.', 404);
|
|
return R.success(res, 'Group members fetched.', group.members);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET USERS IN GROUP]', err);
|
|
return R.error(res, 'Could not fetch group members.', 500);
|
|
}
|
|
};
|
|
|
|
exports.addUserToGroup = async (req, res) => {
|
|
try {
|
|
const { gid: group_id } = req.params;
|
|
const { user_ids } = req.body;
|
|
|
|
if (!Array.isArray(user_ids) || !user_ids.length)
|
|
return R.error(res, 'No users provided.', 400);
|
|
|
|
const existingUsers = await mdl_Users.findAll({ where: { user_id: user_ids }, attributes: ['user_id'] });
|
|
const existingIds = existingUsers.map((u) => u.user_id);
|
|
const notFound = user_ids.filter((id) => !existingIds.includes(id));
|
|
|
|
if (notFound.length)
|
|
return R.error(res, `Users not found: ${notFound.join(', ')}`, 404);
|
|
|
|
await mdl_UserGroupMembers.restore({ where: { user_id: user_ids, group_id } });
|
|
await mdl_UserGroupMembers.update(
|
|
{ deletedBy: null, updatedBy: req.user.user_id },
|
|
{ where: { user_id: user_ids, group_id }, paranoid: false }
|
|
);
|
|
await mdl_UserGroupMembers.bulkCreate(
|
|
user_ids.map((user_id) => ({ user_id, group_id, createdBy: req.user.user_id })),
|
|
{ ignoreDuplicates: true }
|
|
);
|
|
|
|
logActivity(req.user.user_id, 'add_user_to_group', { entityType: 'group', entityId: Number(group_id), details: { user_ids } });
|
|
return R.success(res, 'Users added to group.');
|
|
} catch (err) {
|
|
console.error('[ADMIN][ADD USERS TO GROUP]', err);
|
|
return R.error(res, 'Could not add users to group.', 500);
|
|
}
|
|
};
|
|
|
|
exports.removeUserFromGroup = async (req, res) => {
|
|
try {
|
|
const { gid: group_id } = req.params;
|
|
const { user_ids } = req.body;
|
|
|
|
if (!Array.isArray(user_ids) || !user_ids.length)
|
|
return R.error(res, 'No users provided.', 400);
|
|
|
|
const existingMembers = await mdl_UserGroupMembers.findAll({
|
|
where: { user_id: user_ids, group_id }, attributes: ['user_id'],
|
|
});
|
|
const existingIds = existingMembers.map((m) => m.user_id);
|
|
const notFound = user_ids.filter((id) => !existingIds.includes(id));
|
|
|
|
if (notFound.length)
|
|
return R.error(res, `Memberships not found for users: ${notFound.join(', ')}`, 404);
|
|
|
|
await mdl_UserGroupMembers.update(
|
|
{ deletedBy: req.user.user_id },
|
|
{ where: { user_id: user_ids, group_id } }
|
|
);
|
|
await mdl_UserGroupMembers.destroy({ where: { user_id: user_ids, group_id } });
|
|
|
|
logActivity(req.user.user_id, 'remove_user_from_group', { entityType: 'group', entityId: Number(group_id), details: { user_ids } });
|
|
return R.success(res, 'Users removed from group.');
|
|
} catch (err) {
|
|
console.error('[ADMIN][REMOVE USERS FROM GROUP]', err);
|
|
return R.error(res, 'Could not remove users from group.', 500);
|
|
}
|
|
}; |