mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
adjusted
This commit is contained in:
@@ -5,6 +5,11 @@
|
|||||||
*
|
*
|
||||||
* Author: rgrgogu
|
* Author: rgrgogu
|
||||||
* Date Created: Oct. 6, 2025
|
* 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 sequelize = require('../../config/db.config');
|
||||||
const { Op, Sequelize } = require('sequelize');
|
const { Op, Sequelize } = require('sequelize');
|
||||||
@@ -14,22 +19,36 @@ const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/use
|
|||||||
|
|
||||||
const R = require('../../utils/response.util');
|
const R = require('../../utils/response.util');
|
||||||
const { paginate } = require('../../utils/paginate.util');
|
const { paginate } = require('../../utils/paginate.util');
|
||||||
const { getFieldValues } = require("../../utils/fieldValues.util");
|
const { getFieldValues } = require('../../utils/fieldValues.util');
|
||||||
|
|
||||||
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
|
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
|
||||||
const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.attributes');
|
const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.attributes');
|
||||||
|
|
||||||
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
|
// ─── 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 ──────────────────────────────────────────────────────────────────
|
// ─── GET ALL ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.getGroups = async (req, res) => {
|
exports.getGroups = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await paginate(mdl_UserGroups, req, {
|
const result = await paginate(mdl_UserGroups, req, {
|
||||||
excludeAttributes: groupExclude,
|
excludeAttributes: groupExclude,
|
||||||
jsonbSchemas: groupSchemas,
|
jsonbSchemas: groupSchemas,
|
||||||
computedAttributes: groupComputed,
|
computedAttributes: groupComputed,
|
||||||
context: "list",
|
context: 'list',
|
||||||
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,7 +60,6 @@ exports.getGroups = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── GET ONE ──────────────────────────────────────────────────────────────────
|
// ─── GET ONE ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.getGroup = async (req, res) => {
|
exports.getGroup = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
||||||
@@ -52,7 +70,7 @@ exports.getGroup = async (req, res) => {
|
|||||||
jsonbSchemas: usersSchemas,
|
jsonbSchemas: usersSchemas,
|
||||||
jsonbColumn: 'personal_info',
|
jsonbColumn: 'personal_info',
|
||||||
auditOptions: { mdl_Users, parentAlias: 'User' },
|
auditOptions: { mdl_Users, parentAlias: 'User' },
|
||||||
context: "list",
|
context: 'list',
|
||||||
findOptions: {
|
findOptions: {
|
||||||
include: [{
|
include: [{
|
||||||
model: mdl_UserGroupMembers,
|
model: mdl_UserGroupMembers,
|
||||||
@@ -71,14 +89,25 @@ exports.getGroup = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── CREATE ───────────────────────────────────────────────────────────────────
|
// ─── CREATE ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.createGroup = async (req, res) => {
|
exports.createGroup = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, description } = req.body;
|
const { name, description, group_code } = req.body;
|
||||||
if (!name) return R.error(res, 'Group name is required.', 400);
|
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({
|
const group = await mdl_UserGroups.create({
|
||||||
name, description, createdBy: req.user.user_id,
|
name,
|
||||||
|
description,
|
||||||
|
group_code: code,
|
||||||
|
createdBy: req.user.user_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return R.success(res, 'Group created.', group, 201);
|
return R.success(res, 'Group created.', group, 201);
|
||||||
@@ -89,15 +118,26 @@ exports.createGroup = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.updateGroup = async (req, res) => {
|
exports.updateGroup = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
||||||
if (!group) return R.error(res, 'Group not found.', 404);
|
if (!group) return R.error(res, 'Group not found.', 404);
|
||||||
|
|
||||||
const { name, description } = req.body;
|
const { name, description, group_code } = req.body;
|
||||||
|
|
||||||
if (name !== undefined) group.name = name;
|
if (name !== undefined) group.name = name;
|
||||||
if (description !== undefined) group.description = description;
|
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;
|
group.updatedBy = req.user.user_id;
|
||||||
await group.save();
|
await group.save();
|
||||||
|
|
||||||
@@ -109,7 +149,6 @@ exports.updateGroup = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── DEACTIVATE ───────────────────────────────────────────────────────────────
|
// ─── DEACTIVATE ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.deactivateGroup = async (req, res) => {
|
exports.deactivateGroup = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
||||||
@@ -127,12 +166,9 @@ exports.deactivateGroup = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── RESTORE ──────────────────────────────────────────────────────────────────
|
// ─── RESTORE ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.restoreGroup = async (req, res) => {
|
exports.restoreGroup = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const group = await mdl_UserGroups.findOne({
|
const group = await mdl_UserGroups.findOne({ where: { group_id: req.params.gid }, paranoid: false });
|
||||||
where: { group_id: req.params.gid }, paranoid: false,
|
|
||||||
});
|
|
||||||
if (!group) return R.error(res, 'Group not found.', 404);
|
if (!group) return R.error(res, 'Group not found.', 404);
|
||||||
if (group.is_active) return R.error(res, 'Group is already active.', 400);
|
if (group.is_active) return R.error(res, 'Group is already active.', 400);
|
||||||
|
|
||||||
@@ -147,7 +183,6 @@ exports.restoreGroup = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── BULK DEACTIVATE ──────────────────────────────────────────────────────────
|
// ─── BULK DEACTIVATE ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.bulkDeactivateGroups = async (req, res) => {
|
exports.bulkDeactivateGroups = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { ids } = req.body;
|
const { ids } = req.body;
|
||||||
@@ -180,7 +215,6 @@ exports.bulkDeactivateGroups = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── BULK RESTORE ─────────────────────────────────────────────────────────────
|
// ─── BULK RESTORE ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.bulkRestoreGroups = async (req, res) => {
|
exports.bulkRestoreGroups = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { ids } = req.body;
|
const { ids } = req.body;
|
||||||
@@ -213,14 +247,13 @@ exports.bulkRestoreGroups = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.getArchivedGroups = async (req, res) => {
|
exports.getArchivedGroups = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await paginate(mdl_UserGroups, req, {
|
const result = await paginate(mdl_UserGroups, req, {
|
||||||
excludeAttributes: groupExclude,
|
excludeAttributes: groupExclude,
|
||||||
jsonbSchemas: groupSchemas,
|
jsonbSchemas: groupSchemas,
|
||||||
computedAttributes: groupComputed,
|
computedAttributes: groupComputed,
|
||||||
context: "archived",
|
context: 'archived',
|
||||||
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
||||||
findOptions: {
|
findOptions: {
|
||||||
paranoid: false,
|
paranoid: false,
|
||||||
@@ -236,14 +269,12 @@ exports.getArchivedGroups = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
|
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
|
||||||
|
exports.getGroupFieldValues = getFieldValues(mdl_UserGroups, 'GROUP', {
|
||||||
exports.getGroupFieldValues = getFieldValues(mdl_UserGroups, "GROUP", {
|
blockedFields: ['deletedAt'],
|
||||||
blockedFields: ["deletedAt"],
|
|
||||||
paranoid: false,
|
paranoid: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── MEMBERSHIP ───────────────────────────────────────────────────────────────
|
// ─── MEMBERSHIP ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.getUsersNotInGroup = async (req, res) => {
|
exports.getUsersNotInGroup = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { gid: group_id } = req.params;
|
const { gid: group_id } = req.params;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const { paginate } = require('../../utils/paginate.util');
|
|||||||
const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
|
const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
|
||||||
const { getFieldValues } = require("../../utils/fieldValues.util");
|
const { getFieldValues } = require("../../utils/fieldValues.util");
|
||||||
|
|
||||||
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
|
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, computedAttributes: userComputed } = require('../../models/users/users.attributes');
|
||||||
|
|
||||||
const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at'];
|
const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at'];
|
||||||
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
|
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
|
||||||
@@ -34,8 +34,18 @@ exports.getUsers = async (req, res) => {
|
|||||||
excludeAttributes: usersExclude,
|
excludeAttributes: usersExclude,
|
||||||
jsonbSchemas: usersSchemas,
|
jsonbSchemas: usersSchemas,
|
||||||
jsonbColumn: 'personal_info',
|
jsonbColumn: 'personal_info',
|
||||||
|
computedAttributes: userComputed,
|
||||||
context: "list",
|
context: "list",
|
||||||
auditOptions: { mdl_Users, parentAlias: 'User' },
|
auditOptions: { mdl_Users, parentAlias: 'User' },
|
||||||
|
findOptions: {
|
||||||
|
include: [{
|
||||||
|
model: mdl_UserGroups,
|
||||||
|
as: 'groups',
|
||||||
|
through: { attributes: [] }, // hide junction columns
|
||||||
|
attributes: ['group_id', 'name', 'group_code'],
|
||||||
|
required: false, // LEFT JOIN — users with no group still appear
|
||||||
|
}],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return R.success(res, 'Users retrieved.', result);
|
return R.success(res, 'Users retrieved.', result);
|
||||||
@@ -312,17 +322,26 @@ exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
|
|||||||
|
|
||||||
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ─── GET ARCHIVED USERS ───────────────────────────────────────────────────────
|
||||||
exports.getArchivedUsers = async (req, res) => {
|
exports.getArchivedUsers = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await paginate(mdl_Users, req, {
|
const result = await paginate(mdl_Users, req, {
|
||||||
excludeAttributes: usersExclude,
|
excludeAttributes: usersExclude,
|
||||||
jsonbSchemas: usersSchemas,
|
jsonbSchemas: usersSchemas,
|
||||||
jsonbColumn: 'personal_info',
|
jsonbColumn: 'personal_info',
|
||||||
context: "archived",
|
computedAttributes: userComputed,
|
||||||
|
context: 'archived',
|
||||||
auditOptions: { mdl_Users, parentAlias: 'User' },
|
auditOptions: { mdl_Users, parentAlias: 'User' },
|
||||||
findOptions: {
|
findOptions: {
|
||||||
paranoid: false,
|
paranoid: false,
|
||||||
where: { deletedAt: { [Op.ne]: null }, is_active: false },
|
where: { deletedAt: { [Op.ne]: null }, is_active: false },
|
||||||
|
include: [{
|
||||||
|
model: mdl_UserGroups,
|
||||||
|
as: 'groups',
|
||||||
|
through: { attributes: [] },
|
||||||
|
attributes: ['group_id', 'name', 'group_code'],
|
||||||
|
required: false,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const bcrypt = require('bcryptjs');
|
|||||||
const sequelize = require('../config/db.config')
|
const sequelize = require('../config/db.config')
|
||||||
const mdl_Users = require('../models/users/users.mdl');
|
const mdl_Users = require('../models/users/users.mdl');
|
||||||
const mdl_UserSessions = require('../models/users/user_sessions.mdl');
|
const mdl_UserSessions = require('../models/users/user_sessions.mdl');
|
||||||
|
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../models/users/user_groups.mdl')
|
||||||
const { generateTokens, verifyRefreshToken, hashToken, shouldRotateRefreshToken } = require('../utils/token.util');
|
const { generateTokens, verifyRefreshToken, hashToken, shouldRotateRefreshToken } = require('../utils/token.util');
|
||||||
const { generateOTP, getOTPExpiry, isOTPExpired } = require('../utils/otp.util');
|
const { generateOTP, getOTPExpiry, isOTPExpired } = require('../utils/otp.util');
|
||||||
const sendEmail = require('../services/email.service');
|
const sendEmail = require('../services/email.service');
|
||||||
@@ -55,11 +56,22 @@ exports.register = async (req, res) => {
|
|||||||
const transaction = await sequelize.transaction();
|
const transaction = await sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { email, password } = req.body;
|
const { email, password, personal_info, group_code } = req.body;
|
||||||
|
|
||||||
|
// ── Duplicate check ───────────────────────────────────────────────────────
|
||||||
const existing = await mdl_Users.findOne({ where: { email } });
|
const existing = await mdl_Users.findOne({ where: { email } });
|
||||||
if (existing) return R.error(res, 'Email is already registered.', 409);
|
if (existing) return R.error(res, 'Email is already registered.', 409);
|
||||||
|
|
||||||
|
// ── Validate group_code if provided ───────────────────────────────────────
|
||||||
|
let group = null;
|
||||||
|
if (group_code) {
|
||||||
|
group = await mdl_UserGroups.findOne({
|
||||||
|
where: { group_code: group_code.toUpperCase().trim(), is_active: true },
|
||||||
|
});
|
||||||
|
if (!group) return R.error(res, 'Invalid or inactive group code.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Create user ───────────────────────────────────────────────────────────
|
||||||
const hashed = await bcrypt.hash(password, 12);
|
const hashed = await bcrypt.hash(password, 12);
|
||||||
const otp = generateOTP();
|
const otp = generateOTP();
|
||||||
|
|
||||||
@@ -72,10 +84,20 @@ exports.register = async (req, res) => {
|
|||||||
is_verified: false,
|
is_verified: false,
|
||||||
reg_type: 'system',
|
reg_type: 'system',
|
||||||
acc_type: 'user',
|
acc_type: 'user',
|
||||||
|
personal_info: personal_info ?? null,
|
||||||
createdBy: null,
|
createdBy: null,
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
await sendEmail({ to: email, type: "OTP", data: { otp } });
|
// ── Enroll into group ─────────────────────────────────────────────────────
|
||||||
|
if (group) {
|
||||||
|
await mdl_UserGroupMembers.create({
|
||||||
|
group_id: group.group_id,
|
||||||
|
user_id: user.user_id,
|
||||||
|
createdBy: null,
|
||||||
|
}, { transaction });
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendEmail({ to: email, type: 'OTP', data: { otp } });
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
* Author: rgrgogu
|
* Author: rgrgogu
|
||||||
* Date Created: Oct. 6, 2025
|
* 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 — unique invite code for self-registration
|
||||||
|
***********************************************************************************************************************************************************************
|
||||||
* HOW TO USE:
|
* HOW TO USE:
|
||||||
* const { mdl_UserGroups, mdl_UserGroupMembers } = require('./models/user_groups.mdl');
|
* const { mdl_UserGroups, mdl_UserGroupMembers } = require('./models/user_groups.mdl');
|
||||||
***********************************************************************************************************************************************************************/
|
***********************************************************************************************************************************************************************/
|
||||||
@@ -18,8 +23,15 @@ const mdl_Users = require('./users.mdl');
|
|||||||
const mdl_UserGroups = sequelize.define('UserGroup', {
|
const mdl_UserGroups = sequelize.define('UserGroup', {
|
||||||
group_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "Group ID", order: 1, hidden: true },
|
group_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "Group ID", order: 1, hidden: true },
|
||||||
name: { type: DataTypes.STRING(50), allowNull: false, label: "Group Name", order: 2 },
|
name: { type: DataTypes.STRING(50), allowNull: false, label: "Group Name", order: 2 },
|
||||||
description: { type: DataTypes.TEXT, label: "Description", order: 3, hidden: true },
|
/**
|
||||||
is_active: { type: DataTypes.BOOLEAN, defaultValue: true, label: "Active", order: 4 },
|
* group_code — unique, human-readable invite code distributed to users.
|
||||||
|
* Used in the self-registration URL: /register?group_code=<value>
|
||||||
|
* Automatically uppercased via a beforeValidate hook below.
|
||||||
|
* Example: "SALES-2025", "ONBOARD-Q1"
|
||||||
|
*/
|
||||||
|
group_code: { type: DataTypes.STRING(50), allowNull: true, unique: true, label: "Group Code", order: 3 },
|
||||||
|
description:{ type: DataTypes.TEXT, label: "Description", order: 4, hidden: true },
|
||||||
|
is_active: { type: DataTypes.BOOLEAN, defaultValue: true, label: "Active", order: 5 },
|
||||||
|
|
||||||
// ── Audit trails ────────────────────────────────────────────────────────────
|
// ── Audit trails ────────────────────────────────────────────────────────────
|
||||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
||||||
@@ -29,6 +41,12 @@ const mdl_UserGroups = sequelize.define('UserGroup', {
|
|||||||
tableName: 'user_groups',
|
tableName: 'user_groups',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
paranoid: true,
|
paranoid: true,
|
||||||
|
hooks: {
|
||||||
|
// Always store group_code in uppercase to make lookups case-insensitive
|
||||||
|
beforeValidate: (group) => {
|
||||||
|
if (group.group_code) group.group_code = group.group_code.toUpperCase().trim();
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Junction: UserGroupMembers ────────────────────────────────────────────────
|
// ─── Junction: UserGroupMembers ────────────────────────────────────────────────
|
||||||
@@ -49,7 +67,7 @@ const mdl_UserGroupMembers = sequelize.define('UserGroupMember', {
|
|||||||
|
|
||||||
// ─── Associations ──────────────────────────────────────────────────────────────
|
// ─── Associations ──────────────────────────────────────────────────────────────
|
||||||
mdl_Users.belongsToMany(mdl_UserGroups, { through: mdl_UserGroupMembers, foreignKey: 'user_id', otherKey: 'group_id', as: 'groups' });
|
mdl_Users.belongsToMany(mdl_UserGroups, { through: mdl_UserGroupMembers, foreignKey: 'user_id', otherKey: 'group_id', as: 'groups' });
|
||||||
mdl_UserGroups.belongsToMany(mdl_Users, { through: mdl_UserGroupMembers, foreignKey: 'group_id', otherKey: 'user_id', as: 'members', });
|
mdl_UserGroups.belongsToMany(mdl_Users, { through: mdl_UserGroupMembers, foreignKey: 'group_id', otherKey: 'user_id', as: 'members' });
|
||||||
|
|
||||||
mdl_UserGroups.hasMany(mdl_UserGroupMembers, { foreignKey: 'group_id' });
|
mdl_UserGroups.hasMany(mdl_UserGroupMembers, { foreignKey: 'group_id' });
|
||||||
mdl_UserGroupMembers.belongsTo(mdl_UserGroups, { foreignKey: 'group_id' });
|
mdl_UserGroupMembers.belongsTo(mdl_UserGroups, { foreignKey: 'group_id' });
|
||||||
@@ -57,12 +75,10 @@ mdl_UserGroupMembers.belongsTo(mdl_UserGroups, { foreignKey: 'group_id' });
|
|||||||
mdl_Users.hasMany(mdl_UserGroupMembers, { foreignKey: 'user_id' });
|
mdl_Users.hasMany(mdl_UserGroupMembers, { foreignKey: 'user_id' });
|
||||||
mdl_UserGroupMembers.belongsTo(mdl_Users, { foreignKey: 'user_id' });
|
mdl_UserGroupMembers.belongsTo(mdl_Users, { foreignKey: 'user_id' });
|
||||||
|
|
||||||
// models/users/user_groups.mdl.js — add at the bottom
|
|
||||||
mdl_UserGroups.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
|
mdl_UserGroups.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
|
||||||
mdl_UserGroups.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' });
|
mdl_UserGroups.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' });
|
||||||
mdl_UserGroups.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' });
|
mdl_UserGroups.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' });
|
||||||
|
|
||||||
// Self-referencing associations for audit fields
|
|
||||||
mdl_Users.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
|
mdl_Users.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
|
||||||
mdl_Users.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' });
|
mdl_Users.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' });
|
||||||
mdl_Users.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' });
|
mdl_Users.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' });
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ const userExclude = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const computedAttributes = [
|
const computedAttributes = [
|
||||||
// {
|
{
|
||||||
// key: "memberCount",
|
key: "groups",
|
||||||
// label: "Members",
|
label: "Groups",
|
||||||
// type: "number",
|
type: "array", // tells the paginator this is a pre-joined association array
|
||||||
// order: 99, // adjust order as needed
|
order: 5,
|
||||||
// },
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = { excludeAttributes, adminExclude, userExclude, jsonbSchemas, computedAttributes };
|
module.exports = { excludeAttributes, adminExclude, userExclude, jsonbSchemas, computedAttributes };
|
||||||
Reference in New Issue
Block a user