fix: honor table search q param on users and related lists
Dashboard sends q, but users/classes/payments ignored it; accept q/search and normalize Persian digits.
This commit is contained in:
@@ -107,8 +107,8 @@ const getAllActivityLogs = async (queryParams = {}) => {
|
|||||||
filter.actor = queryParams.actor;
|
filter.actor = queryParams.actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryParams.q) {
|
if (queryParams.q || queryParams.search) {
|
||||||
const searchRegex = new RegExp(queryParams.q, 'i');
|
const searchRegex = new RegExp(String(queryParams.q || queryParams.search).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
||||||
filter.$or = [
|
filter.$or = [
|
||||||
{ description: searchRegex },
|
{ description: searchRegex },
|
||||||
{ actorUsername: searchRegex },
|
{ actorUsername: searchRegex },
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
const Class = require('./classModel');
|
const Class = require('./classModel');
|
||||||
const User = require('../users/userModel');
|
const User = require('../users/userModel');
|
||||||
const AppError = require('../../utils/AppError');
|
const AppError = require('../../utils/AppError');
|
||||||
const { calculateMeta } = require('../../utils/pagination');
|
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
|
||||||
const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages');
|
const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages');
|
||||||
const logger = require('../../utils/logger');
|
const logger = require('../../utils/logger');
|
||||||
|
|
||||||
@@ -17,6 +17,11 @@ const getAll = async (query) => {
|
|||||||
if (query.courseId) filter.course = query.courseId;
|
if (query.courseId) filter.course = query.courseId;
|
||||||
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
|
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
|
||||||
|
|
||||||
|
const searchTerm = getSearchTerm(query);
|
||||||
|
if (searchTerm) {
|
||||||
|
filter.name = new RegExp(escapeRegex(searchTerm), 'i');
|
||||||
|
}
|
||||||
|
|
||||||
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')
|
.select('name course professor students capacity tuitionFee startDate endDate isActive adminNotes createdAt updatedAt')
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const Payment = require('./paymentModel');
|
|||||||
const AppError = require('../../utils/AppError');
|
const AppError = require('../../utils/AppError');
|
||||||
const eventEmitter = require('../../events/eventEmitter');
|
const eventEmitter = require('../../events/eventEmitter');
|
||||||
const EVENT_NAMES = require('../../constants/eventNames');
|
const EVENT_NAMES = require('../../constants/eventNames');
|
||||||
const { calculateMeta } = require('../../utils/pagination');
|
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
|
||||||
|
const User = require('../users/userModel');
|
||||||
|
|
||||||
const getAllPayments = async (query) => {
|
const getAllPayments = async (query) => {
|
||||||
const page = parseInt(query.page) || 1;
|
const page = parseInt(query.page) || 1;
|
||||||
@@ -16,6 +17,24 @@ const getAllPayments = async (query) => {
|
|||||||
if (query.userId) filter.user = query.userId;
|
if (query.userId) filter.user = query.userId;
|
||||||
if (query.status) filter.status = query.status;
|
if (query.status) filter.status = query.status;
|
||||||
|
|
||||||
|
const searchTerm = getSearchTerm(query);
|
||||||
|
if (searchTerm) {
|
||||||
|
const searchRegex = new RegExp(escapeRegex(searchTerm), 'i');
|
||||||
|
const matchedUsers = await User.find({
|
||||||
|
$or: [
|
||||||
|
{ name: searchRegex },
|
||||||
|
{ phoneNumber: searchRegex },
|
||||||
|
{ nationalIdCode: searchRegex }
|
||||||
|
]
|
||||||
|
}).select('_id').lean();
|
||||||
|
|
||||||
|
filter.$or = [
|
||||||
|
{ notes: searchRegex },
|
||||||
|
{ status: searchRegex },
|
||||||
|
{ user: { $in: matchedUsers.map((u) => u._id) } }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const [items, total] = await Promise.all([
|
const [items, total] = await Promise.all([
|
||||||
Payment.find(filter)
|
Payment.find(filter)
|
||||||
.populate({ path: 'user', select: 'name' })
|
.populate({ path: 'user', select: 'name' })
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const Role = require('../roles/roleModel');
|
|||||||
const Class = require('../classes/classModel');
|
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, escapeRegex, getSearchTerm } = require('../../utils/pagination');
|
||||||
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
||||||
const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
|
const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
|
||||||
const logger = require('../../utils/logger');
|
const logger = require('../../utils/logger');
|
||||||
@@ -122,12 +122,15 @@ const getAllUsers = async (query) => {
|
|||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const filter = {};
|
const filter = {};
|
||||||
if (query.search) {
|
const searchTerm = getSearchTerm(query);
|
||||||
|
if (searchTerm) {
|
||||||
|
const searchRegex = new RegExp(escapeRegex(searchTerm), 'i');
|
||||||
filter.$or = [
|
filter.$or = [
|
||||||
{ name: new RegExp(query.search, 'i') },
|
{ name: searchRegex },
|
||||||
{ username: new RegExp(query.search, 'i') },
|
{ username: searchRegex },
|
||||||
{ nationalIdCode: new RegExp(query.search, 'i') },
|
{ nationalIdCode: searchRegex },
|
||||||
{ phoneNumber: new RegExp(query.search, 'i') }
|
{ phoneNumber: searchRegex },
|
||||||
|
{ email: searchRegex }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
|
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
|
||||||
|
|||||||
+29
-5
@@ -22,7 +22,27 @@ const parsePaginationAndSort = (query, defaultSortBy = 'createdAt', defaultSortO
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildFilterQuery = (query, searchFields = [], excludedKeys = ['page', 'limit', 'sortBy', 'sortOrder', 'q', 'lang']) => {
|
const escapeRegex = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
const toLatinDigits = (value) => {
|
||||||
|
if (value == null) return '';
|
||||||
|
const persian = '۰۱۲۳۴۵۶۷۸۹';
|
||||||
|
const arabic = '٠١٢٣٤٥٦٧٨٩';
|
||||||
|
return String(value).replace(/[۰-۹٠-٩]/g, (ch) => {
|
||||||
|
const p = persian.indexOf(ch);
|
||||||
|
if (p >= 0) return String(p);
|
||||||
|
const a = arabic.indexOf(ch);
|
||||||
|
return a >= 0 ? String(a) : ch;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSearchTerm = (query) => {
|
||||||
|
const raw = query?.q || query?.search;
|
||||||
|
if (raw == null || String(raw).trim() === '') return '';
|
||||||
|
return toLatinDigits(String(raw).trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFilterQuery = (query, searchFields = [], excludedKeys = ['page', 'limit', 'sortBy', 'sortOrder', 'q', 'search', 'lang']) => {
|
||||||
const filter = {};
|
const filter = {};
|
||||||
|
|
||||||
// Build field-based exact or boolean filters
|
// Build field-based exact or boolean filters
|
||||||
@@ -41,9 +61,10 @@ const buildFilterQuery = (query, searchFields = [], excludedKeys = ['page', 'lim
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build regex search for ?q= across defined text fields
|
// Build regex search for ?q= or ?search= across defined text fields
|
||||||
if (query.q && searchFields.length > 0) {
|
const searchTerm = getSearchTerm(query);
|
||||||
const searchRegex = new RegExp(query.q, 'i');
|
if (searchTerm && searchFields.length > 0) {
|
||||||
|
const searchRegex = new RegExp(escapeRegex(searchTerm), 'i');
|
||||||
filter.$or = searchFields.map((field) => ({
|
filter.$or = searchFields.map((field) => ({
|
||||||
[field]: searchRegex
|
[field]: searchRegex
|
||||||
}));
|
}));
|
||||||
@@ -65,5 +86,8 @@ const calculateMeta = (totalCount, page, limit) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
parsePaginationAndSort,
|
parsePaginationAndSort,
|
||||||
buildFilterQuery,
|
buildFilterQuery,
|
||||||
calculateMeta
|
calculateMeta,
|
||||||
|
escapeRegex,
|
||||||
|
toLatinDigits,
|
||||||
|
getSearchTerm
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user