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:
2026-08-15 02:39:10 +03:30
parent bf5e66fa95
commit 5c2fad4b44
5 changed files with 46 additions and 4 deletions
+4
View File
@@ -39,6 +39,10 @@ const classSchema = new mongoose.Schema({
isActive: {
type: Boolean,
default: true
},
adminNotes: {
type: [String],
default: []
}
}, {
timestamps: true
+1
View File
@@ -19,6 +19,7 @@ const getAll = async (query) => {
const [items, total] = await Promise.all([
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: 'professor', select: 'name surname' })
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
+4
View File
@@ -81,6 +81,10 @@ const sessionSchema = new mongoose.Schema({
reminderSentAt: {
type: Date,
default: null
},
adminNotes: {
type: [String],
default: []
}
}, {
timestamps: true
+4
View File
@@ -106,6 +106,10 @@ const userSchema = new mongoose.Schema({
isActive: {
type: Boolean,
default: true
},
adminNotes: {
type: [String],
default: []
}
}, {
timestamps: true
+33 -4
View File
@@ -3,6 +3,7 @@
const User = require('./userModel');
const Role = require('../roles/roleModel');
const Class = require('../classes/classModel');
const bcrypt = require('bcryptjs');
const AppError = require('../../utils/AppError');
const { calculateMeta } = require('../../utils/pagination');
@@ -11,6 +12,29 @@ const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
const logger = require('../../utils/logger');
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 SAFE_FIELDS = '-passwordHash -refreshTokens';
const ALLOWED_MESSENGERS = ['Bale', 'WhatsApp', 'Telegram', 'SMS', 'Email'];
@@ -54,7 +78,8 @@ const pickProfileFields = (body = {}) => {
birthDate: body.birthDate || undefined,
education: body.education,
parentPhoneNumber: body.parentPhoneNumber,
preferredMessenger: normalizePreferredMessengers(body.preferredMessenger)
preferredMessenger: normalizePreferredMessengers(body.preferredMessenger),
adminNotes: normalizeAdminNotes(body.adminNotes)
};
Object.keys(fields).forEach((key) => {
@@ -87,7 +112,8 @@ const signUp = async (body) => {
const getUserById = async (id) => {
const user = await User.findById(id).select(SAFE_FIELDS).populate(POPULATE_ROLE).lean();
if (!user) throw new AppError('USER_NOT_FOUND');
return user;
const [enriched] = await withUserListExtras([user]);
return enriched;
};
const getAllUsers = async (query) => {
@@ -111,7 +137,7 @@ const getAllUsers = async (query) => {
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);
@@ -204,6 +230,9 @@ const updateUser = async (id, body) => {
if (preferredMessenger !== undefined) {
update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || [];
}
if (body.adminNotes !== undefined) {
update.adminNotes = normalizeAdminNotes(body.adminNotes) || [];
}
if (typeof username === 'string' && username.trim()) {
update.username = username.trim();
@@ -213,7 +242,7 @@ const updateUser = async (id, body) => {
delete update.surname;
Object.keys(update).forEach((key) => {
if (key === 'preferredMessenger') return;
if (key === 'preferredMessenger' || key === 'adminNotes') return;
if (update[key] === '' || update[key] === null || update[key] === undefined) {
delete update[key];
}