feat: add adminNotes and registered class counts for users
Expose nationalId aliases and enrollment counts on user list responses, and store admin notes on users, classes, and sessions.
This commit is contained in:
@@ -39,6 +39,10 @@ const classSchema = new mongoose.Schema({
|
|||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
adminNotes: {
|
||||||
|
type: [String],
|
||||||
|
default: []
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: true
|
timestamps: true
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const getAll = async (query) => {
|
|||||||
|
|
||||||
const [items, total] = await Promise.all([
|
const [items, total] = await Promise.all([
|
||||||
Class.find(filter)
|
Class.find(filter)
|
||||||
|
.select('name course professor students capacity tuitionFee startDate endDate isActive adminNotes createdAt updatedAt')
|
||||||
.populate({ path: 'course', select: 'title type price' })
|
.populate({ path: 'course', select: 'title type price' })
|
||||||
.populate({ path: 'professor', select: 'name surname' })
|
.populate({ path: 'professor', select: 'name surname' })
|
||||||
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
|
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ const sessionSchema = new mongoose.Schema({
|
|||||||
reminderSentAt: {
|
reminderSentAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
adminNotes: {
|
||||||
|
type: [String],
|
||||||
|
default: []
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: true
|
timestamps: true
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ const userSchema = new mongoose.Schema({
|
|||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
adminNotes: {
|
||||||
|
type: [String],
|
||||||
|
default: []
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: true
|
timestamps: true
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
const User = require('./userModel');
|
const User = require('./userModel');
|
||||||
const Role = require('../roles/roleModel');
|
const Role = require('../roles/roleModel');
|
||||||
|
const Class = require('../classes/classModel');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const AppError = require('../../utils/AppError');
|
const AppError = require('../../utils/AppError');
|
||||||
const { calculateMeta } = require('../../utils/pagination');
|
const { calculateMeta } = require('../../utils/pagination');
|
||||||
@@ -11,6 +12,29 @@ const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
|
|||||||
const logger = require('../../utils/logger');
|
const logger = require('../../utils/logger');
|
||||||
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
|
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
|
||||||
|
|
||||||
|
const normalizeAdminNotes = (value) => {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
if (!Array.isArray(value)) return undefined;
|
||||||
|
return value.map((n) => String(n).trim()).filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const withUserListExtras = async (users) => {
|
||||||
|
if (!users.length) return users;
|
||||||
|
const userIds = users.map((u) => u._id);
|
||||||
|
const counts = await Class.aggregate([
|
||||||
|
{ $match: { students: { $in: userIds } } },
|
||||||
|
{ $unwind: '$students' },
|
||||||
|
{ $match: { students: { $in: userIds } } },
|
||||||
|
{ $group: { _id: '$students', count: { $sum: 1 } } }
|
||||||
|
]);
|
||||||
|
const countMap = Object.fromEntries(counts.map((c) => [String(c._id), c.count]));
|
||||||
|
return users.map((u) => ({
|
||||||
|
...u,
|
||||||
|
nationalId: u.nationalIdCode,
|
||||||
|
registeredClassesCount: countMap[String(u._id)] || 0
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const POPULATE_ROLE = { path: 'role', select: 'name permissions' };
|
const POPULATE_ROLE = { path: 'role', select: 'name permissions' };
|
||||||
const SAFE_FIELDS = '-passwordHash -refreshTokens';
|
const SAFE_FIELDS = '-passwordHash -refreshTokens';
|
||||||
const ALLOWED_MESSENGERS = ['Bale', 'WhatsApp', 'Telegram', 'SMS', 'Email'];
|
const ALLOWED_MESSENGERS = ['Bale', 'WhatsApp', 'Telegram', 'SMS', 'Email'];
|
||||||
@@ -54,7 +78,8 @@ const pickProfileFields = (body = {}) => {
|
|||||||
birthDate: body.birthDate || undefined,
|
birthDate: body.birthDate || undefined,
|
||||||
education: body.education,
|
education: body.education,
|
||||||
parentPhoneNumber: body.parentPhoneNumber,
|
parentPhoneNumber: body.parentPhoneNumber,
|
||||||
preferredMessenger: normalizePreferredMessengers(body.preferredMessenger)
|
preferredMessenger: normalizePreferredMessengers(body.preferredMessenger),
|
||||||
|
adminNotes: normalizeAdminNotes(body.adminNotes)
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(fields).forEach((key) => {
|
Object.keys(fields).forEach((key) => {
|
||||||
@@ -87,7 +112,8 @@ const signUp = async (body) => {
|
|||||||
const getUserById = async (id) => {
|
const getUserById = async (id) => {
|
||||||
const user = await User.findById(id).select(SAFE_FIELDS).populate(POPULATE_ROLE).lean();
|
const user = await User.findById(id).select(SAFE_FIELDS).populate(POPULATE_ROLE).lean();
|
||||||
if (!user) throw new AppError('USER_NOT_FOUND');
|
if (!user) throw new AppError('USER_NOT_FOUND');
|
||||||
return user;
|
const [enriched] = await withUserListExtras([user]);
|
||||||
|
return enriched;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllUsers = async (query) => {
|
const getAllUsers = async (query) => {
|
||||||
@@ -111,7 +137,7 @@ const getAllUsers = async (query) => {
|
|||||||
User.countDocuments(filter)
|
User.countDocuments(filter)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { data: items, meta: calculateMeta(total, page, limit) };
|
return { data: await withUserListExtras(items), meta: calculateMeta(total, page, limit) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchUsers = async (query) => getAllUsers(query);
|
const searchUsers = async (query) => getAllUsers(query);
|
||||||
@@ -204,6 +230,9 @@ const updateUser = async (id, body) => {
|
|||||||
if (preferredMessenger !== undefined) {
|
if (preferredMessenger !== undefined) {
|
||||||
update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || [];
|
update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || [];
|
||||||
}
|
}
|
||||||
|
if (body.adminNotes !== undefined) {
|
||||||
|
update.adminNotes = normalizeAdminNotes(body.adminNotes) || [];
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof username === 'string' && username.trim()) {
|
if (typeof username === 'string' && username.trim()) {
|
||||||
update.username = username.trim();
|
update.username = username.trim();
|
||||||
@@ -213,7 +242,7 @@ const updateUser = async (id, body) => {
|
|||||||
delete update.surname;
|
delete update.surname;
|
||||||
|
|
||||||
Object.keys(update).forEach((key) => {
|
Object.keys(update).forEach((key) => {
|
||||||
if (key === 'preferredMessenger') return;
|
if (key === 'preferredMessenger' || key === 'adminNotes') return;
|
||||||
if (update[key] === '' || update[key] === null || update[key] === undefined) {
|
if (update[key] === '' || update[key] === null || update[key] === undefined) {
|
||||||
delete update[key];
|
delete update[key];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user