chore: relocate backend into apps/api ahead of monorepo merge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:20:40 +08:00
co-authored by Claude Sonnet 5
parent af44598df6
commit eaa6d2276a
388 changed files with 0 additions and 13998 deletions
+267
View File
@@ -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 };