From 7394829d4999b425ae06ed5cecd119576215969f Mon Sep 17 00:00:00 2001 From: Kavehhn174 Date: Sat, 15 Aug 2026 02:57:19 +0330 Subject: [PATCH] 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. --- components/activityLogs/activityLogService.js | 4 +-- components/classes/classService.js | 7 +++- components/payments/paymentService.js | 21 +++++++++++- components/users/userService.js | 15 ++++---- utils/pagination.js | 34 ++++++++++++++++--- 5 files changed, 66 insertions(+), 15 deletions(-) diff --git a/components/activityLogs/activityLogService.js b/components/activityLogs/activityLogService.js index b9f301b..8bd4a2e 100644 --- a/components/activityLogs/activityLogService.js +++ b/components/activityLogs/activityLogService.js @@ -107,8 +107,8 @@ const getAllActivityLogs = async (queryParams = {}) => { filter.actor = queryParams.actor; } - if (queryParams.q) { - const searchRegex = new RegExp(queryParams.q, 'i'); + if (queryParams.q || queryParams.search) { + const searchRegex = new RegExp(String(queryParams.q || queryParams.search).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); filter.$or = [ { description: searchRegex }, { actorUsername: searchRegex }, diff --git a/components/classes/classService.js b/components/classes/classService.js index beb170f..eb194f4 100644 --- a/components/classes/classService.js +++ b/components/classes/classService.js @@ -4,7 +4,7 @@ const Class = require('./classModel'); const User = require('../users/userModel'); const AppError = require('../../utils/AppError'); -const { calculateMeta } = require('../../utils/pagination'); +const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination'); const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages'); const logger = require('../../utils/logger'); @@ -17,6 +17,11 @@ const getAll = async (query) => { if (query.courseId) filter.course = query.courseId; 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([ Class.find(filter) .select('name course professor students capacity tuitionFee startDate endDate isActive adminNotes createdAt updatedAt') diff --git a/components/payments/paymentService.js b/components/payments/paymentService.js index 73d14c5..4af7b5d 100644 --- a/components/payments/paymentService.js +++ b/components/payments/paymentService.js @@ -5,7 +5,8 @@ const Payment = require('./paymentModel'); const AppError = require('../../utils/AppError'); const eventEmitter = require('../../events/eventEmitter'); 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 page = parseInt(query.page) || 1; @@ -16,6 +17,24 @@ const getAllPayments = async (query) => { if (query.userId) filter.user = query.userId; 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([ Payment.find(filter) .populate({ path: 'user', select: 'name' }) diff --git a/components/users/userService.js b/components/users/userService.js index 0e31bb7..65963ba 100644 --- a/components/users/userService.js +++ b/components/users/userService.js @@ -6,7 +6,7 @@ const Role = require('../roles/roleModel'); const Class = require('../classes/classModel'); const bcrypt = require('bcryptjs'); const AppError = require('../../utils/AppError'); -const { calculateMeta } = require('../../utils/pagination'); +const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination'); const { generateUsername, generateSimplePassword } = require('../../utils/credentials'); const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages'); const logger = require('../../utils/logger'); @@ -122,12 +122,15 @@ const getAllUsers = async (query) => { const skip = (page - 1) * limit; const filter = {}; - if (query.search) { + const searchTerm = getSearchTerm(query); + if (searchTerm) { + const searchRegex = new RegExp(escapeRegex(searchTerm), 'i'); filter.$or = [ - { name: new RegExp(query.search, 'i') }, - { username: new RegExp(query.search, 'i') }, - { nationalIdCode: new RegExp(query.search, 'i') }, - { phoneNumber: new RegExp(query.search, 'i') } + { name: searchRegex }, + { username: searchRegex }, + { nationalIdCode: searchRegex }, + { phoneNumber: searchRegex }, + { email: searchRegex } ]; } if (query.isActive !== undefined) filter.isActive = query.isActive === 'true'; diff --git a/utils/pagination.js b/utils/pagination.js index 1b321eb..bd004d7 100644 --- a/utils/pagination.js +++ b/utils/pagination.js @@ -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 = {}; // 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 - if (query.q && searchFields.length > 0) { - const searchRegex = new RegExp(query.q, 'i'); + // Build regex search for ?q= or ?search= across defined text fields + const searchTerm = getSearchTerm(query); + if (searchTerm && searchFields.length > 0) { + const searchRegex = new RegExp(escapeRegex(searchTerm), 'i'); filter.$or = searchFields.map((field) => ({ [field]: searchRegex })); @@ -65,5 +86,8 @@ const calculateMeta = (totalCount, page, limit) => { module.exports = { parsePaginationAndSort, buildFilterQuery, - calculateMeta + calculateMeta, + escapeRegex, + toLatinDigits, + getSearchTerm };