mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user