/*********************************************************************************************************************************************************************** * File Name: groups.ctrl.js (staff) * Type of Program: Controller * Description: Staff-scoped group endpoints. * Returns only the groups the logged-in staff member belongs to, * along with their members and associated task lists. ***********************************************************************************************************************************************************************/ const { Op } = require('sequelize'); const mdl_Users = require('../../models/users/users.mdl'); const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl'); const { TaskList, Task, TaskRequirement, TaskListGroup } = require('../../models/task/task.mdl'); const { getFieldValues } = require("../../utils/fieldValues.util"); const { paginate } = require('../../utils/paginate.util'); const R = require('../../utils/response.util'); const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, computedAttributes: userComputed } = require('../../models/users/users.attributes'); /** * GET /api/staff/groups * Returns all groups the staff member belongs to, * with member count and associated task lists. */ const getMyGroups = async (req, res) => { try { const staffUserId = req.user.user_id; const groups = await mdl_UserGroups.findAll({ include: [ { model: mdl_UserGroupMembers, required: true, // INNER JOIN — only groups the staff member is in where: { user_id: staffUserId }, attributes: [], }, { model: mdl_Users, as: 'members', attributes: ['user_id', 'email', 'personal_info', 'is_active'], through: { attributes: ['joined_at'] }, required: false, }, { model: TaskList, as: 'taskLists', attributes: ['task_list_id', 'name', 'description', 'createdAt'], through: { attributes: ['assignedAt'] }, required: false, include: [ { model: Task, as: 'tasks', attributes: ['task_id', 'name', 'deadline', 'status'], required: false, }, ], }, ], where: { is_active: true }, order: [['name', 'ASC']], }); // Add member_count computed field const data = groups.map(g => ({ ...g.toJSON(), member_count: g.members?.length ?? 0, })); return res.status(200).json({ success: true, data }); } catch (err) { console.error('[staff/groups] getMyGroups error:', err); return res.status(500).json({ success: false, message: 'Internal server error.' }); } }; /** * GET /api/staff/groups/:group_id * Returns a single group's full details — members + task lists. * Staff must be a member of this group. */ const getGroupById = async (req, res) => { try { const staffUserId = req.user.user_id; const groupId = parseInt(req.params.group_id); // Verify staff membership const membership = await mdl_UserGroupMembers.findOne({ where: { user_id: staffUserId, group_id: groupId }, }); if (!membership) { return res.status(403).json({ success: false, message: 'You are not a member of this group.' }); } const group = await mdl_UserGroups.findOne({ where: { group_id: groupId, is_active: true }, include: [ { model: mdl_Users, as: 'members', attributes: ['user_id', 'email', 'personal_info', 'is_active', 'acc_type'], through: { attributes: ['joined_at'] }, }, { model: TaskList, as: 'taskLists', attributes: ['task_list_id', 'name', 'description', 'createdAt'], through: { attributes: ['assignedAt'] }, include: [ { model: Task, as: 'tasks', attributes: ['task_id', 'name', 'description', 'deadline', 'status'], include: [ { model: TaskRequirement, as: 'requirements', attributes: ['requirement_id', 'type', 'link_url', 'link_label', 'reference_id', 'reference_label', 'order'], }, ], }, ], }, ], }); if (!group) return res.status(404).json({ success: false, message: 'Group not found.' }); return res.status(200).json({ success: true, data: { ...group.toJSON(), member_count: group.members?.length ?? 0 }, }); } catch (err) { console.error('[staff/groups] getGroupById error:', err); return res.status(500).json({ success: false, message: 'Internal server error.' }); } }; // GET /staff/groups/:group_id/members // Paginated, searchable, filterable — mirrors getUsers pattern const getGroupMembers = async (req, res) => { try { const staffUserId = req.user.user_id; const groupId = parseInt(req.params.group_id); // Verify staff is a member of this group const membership = await mdl_UserGroupMembers.findOne({ where: { user_id: staffUserId, group_id: groupId }, }); if (!membership) { return res.status(403).json({ success: false, message: 'You are not a member of this group.' }); } // Confirm group exists const groupExists = await mdl_UserGroups.findOne({ where: { group_id: groupId, is_active: true }, attributes: ['group_id'], }); if (!groupExists) { return res.status(404).json({ success: false, message: 'Group not found.' }); } const result = await paginate(mdl_Users, req, { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, jsonbColumn: 'personal_info', computedAttributes: userComputed, context: "list", findOptions: { include: [ { model: mdl_UserGroups, as: 'groups', through: { attributes: [] }, attributes: ['group_id', 'name', 'group_code'], required: true, // INNER JOIN — only users in this group where: { group_id: groupId }, }, ], }, }); return R.success(res, 'Group members retrieved.', result); } catch (err) { console.error('[staff/groups] getGroupMembers error:', err); return R.error(res, 'Could not retrieve group members.', 500); } }; /** * GET /api/staff/groups/by-code/:group_code * Convenience endpoint — look up a group by its invite code. * Staff must still be a member. */ const getGroupByCode = async (req, res) => { try { const staffUserId = req.user.user_id; const groupCode = req.params.group_code.toUpperCase().trim(); const group = await mdl_UserGroups.findOne({ where: { group_code: groupCode, is_active: true }, }); if (!group) return res.status(404).json({ success: false, message: 'Group not found.' }); // Verify staff membership const membership = await mdl_UserGroupMembers.findOne({ where: { user_id: staffUserId, group_id: group.group_id }, }); if (!membership) { return res.status(403).json({ success: false, message: 'You are not a member of this group.' }); } // Reuse getGroupById logic by patching req.params req.params.group_id = group.group_id; return getGroupById(req, res); } catch (err) { console.error('[staff/groups] getGroupByCode error:', err); return res.status(500).json({ success: false, message: 'Internal server error.' }); } }; // ─── GET /staff/groups/:group_id/members/field-values ──────────────────────── const getGroupMemberFieldValues = async (req, res) => { try { const staffUserId = req.user.user_id; const groupId = parseInt(req.params.group_id); const membership = await mdl_UserGroupMembers.findOne({ where: { user_id: staffUserId, group_id: groupId }, }); if (!membership) { return res.status(403).json({ success: false, message: "You are not a member of this group." }); } const groupExists = await mdl_UserGroups.findOne({ where: { group_id: groupId, is_active: true }, attributes: ["group_id"], }); if (!groupExists) { return res.status(404).json({ success: false, message: "Group not found." }); } return getFieldValues(mdl_Users, "USER", { blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"], extraDateFields: ["modifiedAt"], allowJsonb: true, selfJoin: true, // Scope distinct values to this group only baseWhere: { "$groups.group_id$": groupId, }, associations: { groups: { model: mdl_UserGroups, labelField: "name", valueField: "name", // INNER JOIN so values are scoped to group members only required: true, where: { group_id: groupId }, }, }, })(req, res); } catch (err) { console.error("[staff/groups] getGroupMemberFieldValues error:", err); return R.error(res, "Could not retrieve field values.", 500); } }; module.exports = { getMyGroups, getGroupById, getGroupByCode, getGroupMembers, getGroupMemberFieldValues };