/*********************************************************************************************************************************************************************** * 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 ***********************************************************************************************************************************************************************/ 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 { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes'); const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.attributes'); const auditByFields = ['createdBy', 'updatedBy', 'deletedBy']; // ─── GET ALL ────────────────────────────────────────────────────────────────── exports.getGroups = async (req, res) => { try { const result = await paginate(mdl_UserGroups, req, { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed, 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' }, 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 } = req.body; if (!name) return R.error(res, 'Group name is required.', 400); const group = await mdl_UserGroups.create({ name, description, createdBy: req.user.user_id, }); 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 } = req.body; if (name !== undefined) group.name = name; if (description !== undefined) group.description = description; group.updatedBy = req.user.user_id; await group.save(); 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(); 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 }); 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 } }); 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 } ); 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); } }; // ─── ARCHIVED ───────────────────────────────────────────────────────────────── exports.getArchivedGroups = async (req, res) => { try { const result = await paginate(mdl_UserGroups, req, { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed, 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 = async (req, res) => { try { const { field } = req.query; if (!field) return R.error(res, 'Field is required.', 400); const allowedFields = Object.keys(mdl_UserGroups.rawAttributes).filter( (f) => !['deletedAt'].includes(f) ); const dateFields = ['createdAt', 'updatedAt', 'deletedAt']; if (!field.includes('.')) { if (!allowedFields.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 user_groups g JOIN users u ON u.user_id = g."${field}" WHERE g."${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 mdl_UserGroups.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']], paranoid: false, raw: true, }); return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean)); } const results = await mdl_UserGroups.findAll({ attributes: [[Sequelize.fn('DISTINCT', Sequelize.col(field)), 'value']], where: { [field]: { [Op.ne]: null } }, paranoid: false, raw: true, }); return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean).sort()); } return R.error(res, 'JSONB fields are not supported for groups.', 400); } catch (err) { console.error('[ADMIN][GET GROUP FIELD VALUES]', err); return R.error(res, 'Could not retrieve field values.', 500); } }; // ─── MEMBERSHIP ─────────────────────────────────────────────────────────────── exports.getUsersNotInGroup = async (req, res) => { try { const { gid: group_id } = req.params; 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'], ], }); 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 } ); 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 } }); 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); } };