diff --git a/components/category/categoryController.js b/components/category/categoryController.js index 6aef053..e7cd88a 100644 --- a/components/category/categoryController.js +++ b/components/category/categoryController.js @@ -11,7 +11,7 @@ exports.getCategoryTree = catchAsync(async (req, res, next) => { }) exports.getAllCategories = catchAsync(async (req, res, next) => { - const categories = await categoryService.getAllCategories() + const categories = await categoryService.getAllCategories(req.query) res.status(200).json({ status: 'success', @@ -20,6 +20,7 @@ exports.getAllCategories = catchAsync(async (req, res, next) => { }) }) + exports.createCategory = catchAsync(async (req, res, next) => { const category = await categoryService.createCategory(req.body) diff --git a/components/category/categoryService.js b/components/category/categoryService.js index c09cd57..36236f7 100644 --- a/components/category/categoryService.js +++ b/components/category/categoryService.js @@ -1,10 +1,19 @@ const Category = require('./categoryModel') const AppError = require('../../globalErrorHandler/AppError') -module.exports.getAllCategories = async () => { - return Category.find().populate('features') +module.exports.getAllCategories = async (query = {}) => { + const mongoQuery = {} + const search = (query.search || query.query || '').trim() + if (search) { + mongoQuery.$or = [ + { nativeName: { $regex: search, $options: 'i' } }, + { englishName: { $regex: search, $options: 'i' } }, + ] + } + return Category.find(mongoQuery).populate('features') } + module.exports.createCategory = async (data) => { return Category.create(data, null) } diff --git a/components/permission/permissionList.js b/components/permission/permissionList.js index 0eac07a..8a02f01 100644 --- a/components/permission/permissionList.js +++ b/components/permission/permissionList.js @@ -511,6 +511,19 @@ const permissionsList = [ key: 'admindashboard', category: 'سیستم و داشبورد', }, + { + englishName: 'View System Settings', + nativeName: 'مشاهده تنظیمات سیستم', + key: 'getsettings', + category: 'تنظیمات سیستم', + }, + { + englishName: 'Update System Settings', + nativeName: 'ویرایش تنظیمات سیستم', + key: 'updatesettings', + category: 'تنظیمات سیستم', + }, ] module.exports = permissionsList + diff --git a/components/property/propertyController.js b/components/property/propertyController.js index 875b065..5cec38a 100644 --- a/components/property/propertyController.js +++ b/components/property/propertyController.js @@ -3,7 +3,7 @@ const propertyService = require('./propertyService') const { eventEmitter } = require('../../events/eventEmitter') exports.searchProperties = catchAsync(async (req, res, next) => { - const { facets, belongings, totalCount, pagesCount } = await propertyService.searchProperties(req.query) + const { facets, belongings, totalCount, pagesCount, searchEngine } = await propertyService.searchProperties(req.query) res.status(200).json({ status: 'success', @@ -11,9 +11,24 @@ exports.searchProperties = catchAsync(async (req, res, next) => { properties: belongings, totalCount, pagesCount, + searchEngine, }) }) +exports.searchPropertiesMongo = catchAsync(async (req, res, next) => { + const { facets, belongings, totalCount, pagesCount, searchEngine } = await propertyService.searchPropertiesMongo(req.query) + + res.status(200).json({ + status: 'success', + facets, + properties: belongings, + totalCount, + pagesCount, + searchEngine: searchEngine || 'mongo', + }) +}) + + exports.indexAllProperties = catchAsync(async (req, res, next) => { const { indexedCount } = await propertyService.indexAllProperties() diff --git a/components/property/propertyRoutes.js b/components/property/propertyRoutes.js index 7cfbd53..36900b6 100644 --- a/components/property/propertyRoutes.js +++ b/components/property/propertyRoutes.js @@ -16,6 +16,8 @@ router.post('/admin/index-all', perm.requires('indexproperties', 'indexbelonging router.post('/admin/index-one/:propertyId', perm.requires('indexproperties', 'indexbelongings'), propertyController.indexOneProperty) router.get('/admin/search', perm.requires('searchproperties', 'searchbelongings'), propertyController.searchProperties) +router.get('/admin/search-mongo', perm.requires('searchproperties', 'searchbelongings'), propertyController.searchPropertiesMongo) + router.get('/admin/get-one/:propertyId', perm.requires('getaproperty', 'getabelonging'), propertyController.getProperty) diff --git a/components/property/propertyService.js b/components/property/propertyService.js index a713faa..7be711c 100644 --- a/components/property/propertyService.js +++ b/components/property/propertyService.js @@ -90,15 +90,136 @@ const ensureCollectionExists = async (forceRecreate = false) => { } } +const settingService = require('../setting/settingService') +const mongoose = require('mongoose') + +const searchPropertiesMongo = async (inputQuery = {}) => { + const page = parseInt(inputQuery.page) || 1 + const per_page = parseInt(inputQuery.per_page) || parseInt(inputQuery.limit) || 20 + const mongoQuery = { isActive: { $ne: false } } + + const q = (inputQuery.query || inputQuery.search || inputQuery.q || '').trim() + if (q && q !== '*') { + const searchRegex = { $regex: q, $options: 'i' } + const orConditions = [ + { name: searchRegex }, + { nationalGeoCode: searchRegex }, + { uniqueId: searchRegex }, + { serialNumber: searchRegex }, + { ownerName: searchRegex }, + { registrationPlate: searchRegex }, + { description: searchRegex }, + { address: searchRegex }, + { tags: searchRegex }, + { relatedProjects: searchRegex }, + ] + + if (!isNaN(q) && q !== '') { + orConditions.push({ propertyCode: Number(q) }) + } + + if (mongoose.Types.ObjectId.isValid(q)) { + orConditions.push({ _id: q }) + } + + mongoQuery.$or = orConditions + } + + if (inputQuery.category && mongoose.Types.ObjectId.isValid(inputQuery.category)) { + mongoQuery.category = inputQuery.category + } + + if (inputQuery.status && mongoose.Types.ObjectId.isValid(inputQuery.status)) { + mongoQuery.status = inputQuery.status + } + + let sortObj = { createdAt: -1 } + if (inputQuery.sort) { + if (typeof inputQuery.sort === 'string') { + const parts = inputQuery.sort.split(':') + const field = parts[0] + const order = parts[1] === 'asc' ? 1 : -1 + sortObj = { [field]: order } + } else if (typeof inputQuery.sort === 'object') { + sortObj = inputQuery.sort + } + } + + const totalCount = await Property.countDocuments(mongoQuery) + const properties = await Property.find(mongoQuery) + .populate('category') + .populate('status') + .sort(sortObj) + .skip((page - 1) * per_page) + .limit(per_page) + + const items = properties.map((prop) => { + const combinedTags = Array.isArray(prop.tags) ? prop.tags.join(' ') : (prop.tags || '') + return { + _id: prop._id.toString(), + id: prop.propertyCode ? prop.propertyCode.toString() : prop._id.toString(), + propertyCode: prop.propertyCode, + name: prop.name || '', + category: prop.category?.englishName || (typeof prop.category === 'string' ? prop.category : ''), + nativeCategory: prop.category?.nativeName || '', + categoryObj: prop.category, + status: prop.status, + imageUrl: prop.mainImageUrls?.[0] || 'https://cloud.mbmanager.ir/images/item-no-image.png', + mainImageUrls: prop.mainImageUrls || [], + documents: prop.documentUrls || [], + documentUrls: prop.documentUrls || [], + description: prop.description || '', + nationalGeoCode: prop.nationalGeoCode || '', + tags: combinedTags, + dong: prop.dong, + ownerName: prop.ownerName || '', + registrationPlate: prop.registrationPlate || '', + uniqueId: prop.uniqueId || '', + serialNumber: prop.serialNumber || '', + coordinates: prop.location?.coordinates || [0, 0], + location: prop.location, + createdAt: Math.floor(new Date(prop.createdAt || Date.now()).getTime() / 1000), + updatedAt: prop.updatedAt, + isActive: prop.isActive !== false, + features: prop.features || [], + } + }) + + const pagesCount = Math.ceil(totalCount / per_page) || 1 + + return { + facets: [], + belongings: items, + properties: items, + totalCount, + pagesCount, + searchEngine: 'mongo', + } +} + const searchProperties = async (inputQuery) => { - await ensureCollectionExists(false) - inputQuery = updateInputQuery(inputQuery, {}) + try { + const setting = await settingService.getSearchSettingByPage('properties') + if (setting && setting.searchMethod === 'mongo') { + return searchPropertiesMongo(inputQuery) + } + } catch (e) { + console.warn('Failed to fetch search setting for properties, attempting typesense:', e.message) + } - const tsFeatures = new TsApiFeatures(client, 'properties', inputQuery).filter().facet().sort().limitFields().paginate().search() + try { + await ensureCollectionExists(false) + const updatedQuery = updateInputQuery({ ...inputQuery }, {}) - let { facets, items, totalCount, pagesCount } = await tsFeatures.execute() + const tsFeatures = new TsApiFeatures(client, 'properties', updatedQuery).filter().facet().sort().limitFields().paginate().search() - return { facets, belongings: items, totalCount, pagesCount } + let { facets, items, totalCount, pagesCount } = await tsFeatures.execute() + + return { facets, belongings: items, properties: items, totalCount, pagesCount, searchEngine: 'typesense' } + } catch (err) { + console.warn('Typesense search failed for properties, falling back to MongoDB:', err.message) + return searchPropertiesMongo(inputQuery) + } } const updateInputQuery = (inputQuery, extraQueries) => { @@ -111,6 +232,7 @@ const updateInputQuery = (inputQuery, extraQueries) => { return inputQuery } + const indexAllProperties = async () => { const batchSize = 20 const count = await Property.countDocuments({ isActive: true }, null) @@ -213,5 +335,7 @@ module.exports = { createProperty, deleteProperty, searchProperties, + searchPropertiesMongo, indexOneProperty, } + diff --git a/components/role/roleController.js b/components/role/roleController.js index 42e165e..4a08d65 100644 --- a/components/role/roleController.js +++ b/components/role/roleController.js @@ -4,7 +4,7 @@ const Role = require('./roleModel') const roleService = require('./roleService') exports.getAllRoles = catchAsync(async (req, res, next) => { - const roles = await roleService.getAllRoles() + const roles = await roleService.getAllRoles(req.query) res.status(200).json({ status: 'success', @@ -13,6 +13,7 @@ exports.getAllRoles = catchAsync(async (req, res, next) => { }) }) + exports.createRole = catchAsync(async (req, res, next) => { const role = await roleService.createRole(req.body) diff --git a/components/role/roleService.js b/components/role/roleService.js index f1a94d1..ec57af8 100644 --- a/components/role/roleService.js +++ b/components/role/roleService.js @@ -1,10 +1,19 @@ const Role = require('./roleModel') const AppError = require('../../globalErrorHandler/AppError') -module.exports.getAllRoles = async () => { - return Role.find({}).populate('permissions').sort({ createdAt: -1 }) +module.exports.getAllRoles = async (query = {}) => { + const mongoQuery = {} + const search = (query.search || query.query || '').trim() + if (search) { + mongoQuery.$or = [ + { nativeName: { $regex: search, $options: 'i' } }, + { englishName: { $regex: search, $options: 'i' } }, + ] + } + return Role.find(mongoQuery).populate('permissions').sort({ createdAt: -1 }) } + module.exports.createRole = async (data) => { const role = await Role.create(data) return Role.findById(role._id).populate('permissions') diff --git a/components/setting/settingController.js b/components/setting/settingController.js new file mode 100644 index 0000000..1820038 --- /dev/null +++ b/components/setting/settingController.js @@ -0,0 +1,42 @@ +const catchAsync = require('../../globalErrorHandler/catchAsync') +const settingService = require('./settingService') + +exports.getSearchSettings = catchAsync(async (req, res, next) => { + const settings = await settingService.getSearchSettings() + + res.status(200).json({ + status: 'success', + results: settings.length, + data: settings, + }) +}) + +exports.getSearchSettingByPage = catchAsync(async (req, res, next) => { + const setting = await settingService.getSearchSettingByPage(req.params.pageKey) + + res.status(200).json({ + status: 'success', + data: setting, + }) +}) + +exports.updateSearchSetting = catchAsync(async (req, res, next) => { + const { searchMethod, extraConfig } = req.body + const setting = await settingService.setSearchMethod(req.params.pageKey, searchMethod, extraConfig) + + res.status(200).json({ + status: 'success', + data: setting, + }) +}) + +exports.bulkUpdateSearchSettings = catchAsync(async (req, res, next) => { + const { settings } = req.body + const results = await settingService.bulkUpdateSearchSettings(settings) + + res.status(200).json({ + status: 'success', + results: results.length, + data: results, + }) +}) diff --git a/components/setting/settingModel.js b/components/setting/settingModel.js new file mode 100644 index 0000000..9748519 --- /dev/null +++ b/components/setting/settingModel.js @@ -0,0 +1,46 @@ +const mongoose = require('mongoose') + +const settingSchema = new mongoose.Schema( + { + key: { + type: String, + required: true, + unique: true, + trim: true, + }, + category: { + type: String, + default: 'search', + }, + pageKey: { + type: String, + required: true, + trim: true, + }, + title: { + type: String, + required: true, + }, + description: { + type: String, + }, + searchMethod: { + type: String, + enum: ['typesense', 'mongo'], + default: 'mongo', + }, + extraConfig: { + type: mongoose.Schema.Types.Mixed, + default: {}, + }, + }, + { + timestamps: true, + } +) + +settingSchema.index({ key: 1 }, { unique: true }) +settingSchema.index({ pageKey: 1 }) + +const Setting = mongoose.model('Setting', settingSchema) +module.exports = Setting diff --git a/components/setting/settingRoutes.js b/components/setting/settingRoutes.js new file mode 100644 index 0000000..dec6062 --- /dev/null +++ b/components/setting/settingRoutes.js @@ -0,0 +1,34 @@ +const express = require('express') +const settingController = require('./settingController') +const perm = require('../../middlewares/permissionMiddleware') +const loginCheck = require('../../middlewares/loginCheck') + +const router = new express.Router() + +router.use(loginCheck.loginProtect) + +router.get( + '/search-settings', + perm.requires('admindashboard', 'viewdashboard', 'getallpermissions'), + settingController.getSearchSettings +) + +router.get( + '/search-settings/:pageKey', + perm.requires('admindashboard', 'viewdashboard', 'getallpermissions'), + settingController.getSearchSettingByPage +) + +router.put( + '/search-settings/:pageKey', + perm.requires('admindashboard', 'viewdashboard', 'getallpermissions'), + settingController.updateSearchSetting +) + +router.put( + '/search-settings', + perm.requires('admindashboard', 'viewdashboard', 'getallpermissions'), + settingController.bulkUpdateSearchSettings +) + +module.exports = router diff --git a/components/setting/settingService.js b/components/setting/settingService.js new file mode 100644 index 0000000..d5fb9e5 --- /dev/null +++ b/components/setting/settingService.js @@ -0,0 +1,194 @@ +const Setting = require('./settingModel') +const AppError = require('../../globalErrorHandler/AppError') + +const DEFAULT_SEARCH_PAGES = [ + { + pageKey: 'properties', + title: 'املاک (Properties)', + description: 'جستجوی املاک، کد نوسازی، شناسه ملی جغرافیایی، مشخصات سند، مالک و پلاک ثبتی', + defaultMethod: 'typesense', + supportedMethods: ['typesense', 'mongo'], + endpointTypesense: '/api/properties/admin/search', + endpointMongo: '/api/properties/admin/search-mongo', + routePath: '/property/list-properties', + icon: 'pi pi-file', + }, + { + pageKey: 'transactions', + title: 'تراکنش‌های املاک (Transactions)', + description: 'جستجوی تراکنش‌های واریز و برداشت، توضیحات، نام کاربر و ملک مرتبط', + defaultMethod: 'typesense', + supportedMethods: ['typesense', 'mongo'], + endpointTypesense: '/api/transactions/admin/search', + endpointMongo: '/api/transactions/admin/search-mongo', + routePath: '/transactions/list-transactions', + icon: 'pi pi-history', + }, + { + pageKey: 'warehouseItems', + title: 'اقلام انبار (Warehouse Items)', + description: 'جستجوی اقلام کالا، نام فارسی، نام انگلیسی، کد کالا و دسته‌بندی', + defaultMethod: 'mongo', + supportedMethods: ['mongo', 'typesense'], + endpointTypesense: '/api/warehouse-items/admin/items', + endpointMongo: '/api/warehouse-items/admin/items', + routePath: '/warehouse-items/list', + icon: 'pi pi-box', + }, + { + pageKey: 'warehouses', + title: 'مکان‌های انبار (Warehouses)', + description: 'جستجوی نام انبار، کد و آدرس انبارها', + defaultMethod: 'mongo', + supportedMethods: ['mongo', 'typesense'], + endpointTypesense: '/api/warehouses/admin/get-all', + endpointMongo: '/api/warehouses/admin/get-all', + routePath: '/warehouse-items/warehouses', + icon: 'pi pi-map', + }, + { + pageKey: 'categories', + title: 'دسته‌بندی‌ها (Categories)', + description: 'جستجوی دسته‌بندی‌های املاک بر اساس نام فارسی و انگلیسی', + defaultMethod: 'mongo', + supportedMethods: ['mongo', 'typesense'], + endpointTypesense: '/api/categories/admin/get-all', + endpointMongo: '/api/categories/admin/get-all', + routePath: '/categories/list-categories', + icon: 'pi pi-th-large', + }, + { + pageKey: 'users', + title: 'کاربران (Users)', + description: 'جستجوی کاربران سیستم بر اساس نام، ایمیل و نقش کاربری', + defaultMethod: 'mongo', + supportedMethods: ['mongo', 'typesense'], + endpointTypesense: '/api/users/admin/get-all', + endpointMongo: '/api/users/admin/get-all', + routePath: '/users/list-users', + icon: 'pi pi-users', + }, + { + pageKey: 'roles', + title: 'نقش‌ها و دسترسی‌ها (Roles)', + description: 'جستجوی نقش‌ها بر اساس عنوان فارسی و انگلیسی', + defaultMethod: 'mongo', + supportedMethods: ['mongo', 'typesense'], + endpointTypesense: '/api/roles/admin/get-all', + endpointMongo: '/api/roles/admin/get-all', + routePath: '/roles/list-roles', + icon: 'pi pi-shield', + }, +] + +const getSearchSettings = async () => { + const dbSettings = await Setting.find({ category: 'search' }).lean() + const dbSettingsMap = new Map() + + dbSettings.forEach((s) => { + dbSettingsMap.set(s.pageKey, s) + }) + + return DEFAULT_SEARCH_PAGES.map((pageDef) => { + const dbRecord = dbSettingsMap.get(pageDef.pageKey) + if (dbRecord) { + return { + ...pageDef, + _id: dbRecord._id, + searchMethod: dbRecord.searchMethod || pageDef.defaultMethod, + isPersisted: true, + updatedAt: dbRecord.updatedAt, + createdAt: dbRecord.createdAt, + } + } + return { + ...pageDef, + searchMethod: pageDef.defaultMethod, + isPersisted: false, + } + }) +} + +const getSearchSettingByPage = async (pageKey) => { + const pageDef = DEFAULT_SEARCH_PAGES.find((p) => p.pageKey === pageKey) + if (!pageDef) { + throw new AppError('PageNotFound', 404, { message: `Page key ${pageKey} not found in search configurations` }) + } + + const dbRecord = await Setting.findOne({ pageKey, category: 'search' }).lean() + if (dbRecord) { + return { + ...pageDef, + _id: dbRecord._id, + searchMethod: dbRecord.searchMethod || pageDef.defaultMethod, + isPersisted: true, + updatedAt: dbRecord.updatedAt, + createdAt: dbRecord.createdAt, + } + } + + return { + ...pageDef, + searchMethod: pageDef.defaultMethod, + isPersisted: false, + } +} + +const setSearchMethod = async (pageKey, searchMethod, extraConfig = {}) => { + if (!['typesense', 'mongo'].includes(searchMethod)) { + throw new AppError('InvalidSearchMethod', 400, { message: 'Search method must be either "typesense" or "mongo"' }) + } + + const pageDef = DEFAULT_SEARCH_PAGES.find((p) => p.pageKey === pageKey) || { + pageKey, + title: pageKey, + description: `Settings for ${pageKey}`, + defaultMethod: 'mongo', + } + + const setting = await Setting.findOneAndUpdate( + { pageKey, category: 'search' }, + { + key: `search_${pageKey}`, + category: 'search', + pageKey, + title: pageDef.title, + description: pageDef.description, + searchMethod, + extraConfig, + }, + { upsert: true, new: true, setDefaultsOnInsert: true } + ) + + return { + ...pageDef, + _id: setting._id, + searchMethod: setting.searchMethod, + isPersisted: true, + updatedAt: setting.updatedAt, + createdAt: setting.createdAt, + } +} + +const bulkUpdateSearchSettings = async (settingsArray) => { + if (!Array.isArray(settingsArray)) { + throw new AppError('InvalidDataFormat', 400, { message: 'Expected an array of settings' }) + } + + const results = [] + for (const item of settingsArray) { + if (item.pageKey && item.searchMethod) { + const updated = await setSearchMethod(item.pageKey, item.searchMethod, item.extraConfig || {}) + results.push(updated) + } + } + return results +} + +module.exports = { + DEFAULT_SEARCH_PAGES, + getSearchSettings, + getSearchSettingByPage, + setSearchMethod, + bulkUpdateSearchSettings, +} diff --git a/components/transaction/transactionController.js b/components/transaction/transactionController.js index 43d7d8c..6837bba 100644 --- a/components/transaction/transactionController.js +++ b/components/transaction/transactionController.js @@ -2,7 +2,7 @@ const catchAsync = require('../../globalErrorHandler/catchAsync') const transactionService = require('./transactionService') exports.searchTransactions = catchAsync(async (req, res, next) => { - const { facets, transactions, totalCount, pagesCount } = await transactionService.searchTransactions(req.query) + const { facets, transactions, totalCount, pagesCount, searchEngine } = await transactionService.searchTransactions(req.query) res.status(200).json({ status: 'success', @@ -10,9 +10,24 @@ exports.searchTransactions = catchAsync(async (req, res, next) => { transactions, totalCount, pagesCount, + searchEngine, }) }) +exports.searchTransactionsMongo = catchAsync(async (req, res, next) => { + const { facets, transactions, totalCount, pagesCount, searchEngine } = await transactionService.searchTransactionsMongo(req.query) + + res.status(200).json({ + status: 'success', + facets, + transactions, + totalCount, + pagesCount, + searchEngine: searchEngine || 'mongo', + }) +}) + + exports.indexAllTransactions = catchAsync(async (req, res, next) => { const { indexedCount } = await transactionService.indexAllTransactions() diff --git a/components/transaction/transactionRoutes.js b/components/transaction/transactionRoutes.js index 8709668..41d9b03 100644 --- a/components/transaction/transactionRoutes.js +++ b/components/transaction/transactionRoutes.js @@ -31,6 +31,13 @@ router.get( transactionController.searchTransactions ) +router.get( + '/admin/search-mongo', + // perm.requires('getallbelongings'), + transactionController.searchTransactionsMongo +) + + router.get( '/admin/get-stock-by-belonging/:belongingId', // perm.requires('getallbelongings'), diff --git a/components/transaction/transactionService.js b/components/transaction/transactionService.js index 8ec625a..a13ba9e 100644 --- a/components/transaction/transactionService.js +++ b/components/transaction/transactionService.js @@ -165,15 +165,116 @@ const ensureCollectionExists = async (forceRecreate = false) => { } } +const settingService = require('../setting/settingService') + +const searchTransactionsMongo = async (inputQuery = {}) => { + const page = parseInt(inputQuery.page) || 1 + const per_page = parseInt(inputQuery.per_page) || parseInt(inputQuery.limit) || 20 + const mongoQuery = {} + + const q = (inputQuery.query || inputQuery.search || inputQuery.q || '').trim() + if (q && q !== '*') { + const searchRegex = { $regex: q, $options: 'i' } + const orConditions = [ + { description: searchRegex }, + { userName: searchRegex }, + { type: searchRegex }, + { status: searchRegex }, + { source: searchRegex }, + ] + + if (mongoose.Types.ObjectId.isValid(q)) { + orConditions.push({ _id: q }) + orConditions.push({ propertyId: q }) + orConditions.push({ userId: q }) + } + + mongoQuery.$or = orConditions + } + + if (inputQuery.type) mongoQuery.type = inputQuery.type + if (inputQuery.status) mongoQuery.status = inputQuery.status + if (inputQuery.source) mongoQuery.source = inputQuery.source + if (inputQuery.propertyId && mongoose.Types.ObjectId.isValid(inputQuery.propertyId)) { + mongoQuery.propertyId = inputQuery.propertyId + } + if (inputQuery.userId && mongoose.Types.ObjectId.isValid(inputQuery.userId)) { + mongoQuery.userId = inputQuery.userId + } + + let sortObj = { createdAt: -1 } + if (inputQuery.sort) { + if (typeof inputQuery.sort === 'string') { + const parts = inputQuery.sort.split(':') + const field = parts[0] + const order = parts[1] === 'asc' ? 1 : -1 + sortObj = { [field]: order } + } else if (typeof inputQuery.sort === 'object') { + sortObj = inputQuery.sort + } + } + + const totalCount = await Transaction.countDocuments(mongoQuery) + const transactions = await Transaction.find(mongoQuery) + .populate('propertyId') + .populate('userId') + .sort(sortObj) + .skip((page - 1) * per_page) + .limit(per_page) + + const items = transactions.map((t) => { + return { + _id: t._id.toString(), + id: t._id.toString(), + type: t.type || '', + status: t.status || '', + source: t.source || '', + value: t.value || 0, + description: t.description || '', + createdAt: Math.floor(new Date(t.createdAt || t.date || Date.now()).getTime() / 1000), + date: t.date, + belongingName: t.propertyId?.name || '', + belongingId: t.propertyId?._id ? t.propertyId._id.toString() : '', + propertyId: t.propertyId, + userName: t.userName || t.userId?.name || '', + userId: t.userId, + } + }) + + const pagesCount = Math.ceil(totalCount / per_page) || 1 + + return { + facets: [], + transactions: items, + totalCount, + pagesCount, + searchEngine: 'mongo', + } +} + const searchTransactions = async (inputQuery) => { - await ensureCollectionExists(false) - inputQuery = updateInputQuery(inputQuery, {}) + try { + const setting = await settingService.getSearchSettingByPage('transactions') + if (setting && setting.searchMethod === 'mongo') { + return searchTransactionsMongo(inputQuery) + } + } catch (e) { + console.warn('Failed to fetch search setting for transactions, attempting typesense:', e.message) + } - const tsFeatures = new TsApiFeatures(client, 'transactions', inputQuery).filter().facet().sort().limitFields().paginate().search() + try { + await ensureCollectionExists(false) + const updatedQuery = updateInputQuery({ ...inputQuery }, {}) - let { facets, items, totalCount, pagesCount } = await tsFeatures.execute() + const tsFeatures = new TsApiFeatures(client, 'transactions', updatedQuery).filter().facet().sort().limitFields().paginate().search() - return { facets, transactions: items, totalCount, pagesCount } + let { facets, items, totalCount, pagesCount } = await tsFeatures.execute() + + return { facets, transactions: items, totalCount, pagesCount, searchEngine: 'typesense' } + } catch (err) { + console.warn('Typesense search failed for transactions, falling back to MongoDB:', err.message) + return searchTransactionsMongo(inputQuery) + } } const updateInputQuery = (inputQuery, extraQueries) => { @@ -257,4 +358,6 @@ module.exports = { getTransactionByBelongingId, indexAllTransactions, searchTransactions, + searchTransactionsMongo, } + diff --git a/components/user/userController.js b/components/user/userController.js index 2afbc77..aaa3e42 100644 --- a/components/user/userController.js +++ b/components/user/userController.js @@ -47,7 +47,7 @@ exports.logoutUser = catchAsync(async (req, res, next) => { }) exports.getAllUsers = catchAsync(async (req, res, next) => { - const users = await userService.getAllUsers() + const users = await userService.getAllUsers(req.query) res.status(200).json({ status: 'success', @@ -56,6 +56,7 @@ exports.getAllUsers = catchAsync(async (req, res, next) => { }) }) + exports.userGetSelf = catchAsync(async (req, res, next) => { const user = req.user if (!user) { diff --git a/components/user/userService.js b/components/user/userService.js index 26171b7..e78af91 100644 --- a/components/user/userService.js +++ b/components/user/userService.js @@ -159,10 +159,19 @@ const loginUser = async (email, password) => { return { token: signToken(user._id), user } } -const getAllUsers = async () => { - return User.find({}).populate('role').sort({ createdAt: -1 }) +const getAllUsers = async (query = {}) => { + const mongoQuery = {} + const search = (query.search || query.query || '').trim() + if (search) { + mongoQuery.$or = [ + { name: { $regex: search, $options: 'i' } }, + { email: { $regex: search, $options: 'i' } }, + ] + } + return User.find(mongoQuery).populate('role').sort({ createdAt: -1 }) } + const getUser = async (userId) => { const user = await User.findById(userId).populate('role') diff --git a/setup/routerEndpoints.js b/setup/routerEndpoints.js index c2d5cf7..78dcd86 100644 --- a/setup/routerEndpoints.js +++ b/setup/routerEndpoints.js @@ -10,6 +10,7 @@ const fileRoutes = require('../components/file/fileRoutes') const featureRoutes = require('../components/feature/featureRoutes') const warehouseItemRoutes = require('../components/warehouseItem/warehouseItemRoutes') const warehouseRoutes = require('../components/warehouse/warehouseRoutes') +const settingRoutes = require('../components/setting/settingRoutes') const routerEndpoints = new express.Router() @@ -27,5 +28,7 @@ routerEndpoints.use('/files', fileRoutes) routerEndpoints.use('/features', featureRoutes) routerEndpoints.use('/warehouse-items', warehouseItemRoutes) routerEndpoints.use('/warehouses', warehouseRoutes) +routerEndpoints.use('/settings', settingRoutes) module.exports = routerEndpoints +