Files
multy-stub/server/routers/procurement/routes/search.js

338 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Company = require('../models/Company');
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
// GET /search/recommendations - получить рекомендации компаний (ДОЛЖЕН быть ПЕРЕД /*)
router.get('/recommendations', verifyToken, async (req, res) => {
try {
// Получить компанию пользователя, чтобы исключить её из результатов
const User = require('../models/User');
const user = await User.findById(req.userId);
let filter = {};
if (user && user.companyId) {
filter._id = { $ne: user.companyId };
}
const companies = await Company.find(filter)
.sort({ rating: -1 })
.limit(5);
const recommendations = companies.map(company => ({
id: company._id.toString(),
name: company.fullName || company.shortName,
industry: company.industry,
logo: company.logo,
matchScore: Math.floor(Math.random() * 30 + 70), // 70-100
reason: 'Matches your search criteria'
}));
log('[Search] Returned recommendations:', recommendations.length);
res.json(recommendations);
} catch (error) {
console.error('[Search] Recommendations error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
});
// GET /search - Поиск компаний
router.get('/', verifyToken, async (req, res) => {
try {
console.log('[Search] === NEW VERSION WITH FIXED SIZE FILTER ===');
const {
query = '',
page = 1,
limit = 10,
offset, // Добавляем поддержку offset для точной пагинации
industries,
companySize,
geography,
minRating = 0,
hasReviews,
hasAcceptedDocs,
sortBy = 'relevance',
sortOrder = 'desc',
minEmployees, // Кастомный фильтр: минимум сотрудников
maxEmployees // Кастомный фильтр: максимум сотрудников
} = req.query;
console.log('[Search] Filters:', { minEmployees, maxEmployees, companySize });
// Получить компанию пользователя, чтобы исключить её из результатов
const User = require('../models/User');
const user = await User.findById(req.userId);
log('[Search] Request params:', { query, industries, companySize, geography, minRating, hasReviews, hasAcceptedDocs, sortBy, sortOrder });
// Маппинг кодов фильтров на значения в БД
const industryMap = {
'it': 'IT',
'finance': 'Финансы',
'manufacturing': 'Производство',
'construction': 'Строительство',
'retail': 'Розничная торговля',
'wholesale': 'Оптовая торговля',
'logistics': 'Логистика',
'healthcare': 'Здравоохранение',
'education': 'Образование',
'consulting': 'Консалтинг',
'marketing': 'Маркетинг',
'realestate': 'Недвижимость',
'food': 'Пищевая промышленность',
'agriculture': 'Сельское хозяйство',
'energy': 'Энергетика',
'telecom': 'Телекоммуникации',
'media': 'Медиа'
};
// Начальный фильтр: исключить собственную компанию
let filters = [];
if (user && user.companyId) {
filters.push({ _id: { $ne: user.companyId } });
}
// Текстовый поиск
if (query && query.trim()) {
const q = query.toLowerCase();
filters.push({
$or: [
{ fullName: { $regex: q, $options: 'i' } },
{ shortName: { $regex: q, $options: 'i' } },
{ slogan: { $regex: q, $options: 'i' } },
{ industry: { $regex: q, $options: 'i' } }
]
});
}
// Фильтр по отраслям - преобразуем коды в значения БД
if (industries) {
const industryList = Array.isArray(industries) ? industries : [industries];
if (industryList.length > 0) {
const dbIndustries = industryList
.map(code => industryMap[code])
.filter(val => val !== undefined);
log('[Search] Raw industries param:', industries);
log('[Search] Industry codes:', industryList, 'Mapped to:', dbIndustries);
if (dbIndustries.length > 0) {
filters.push({ industry: { $in: dbIndustries } });
log('[Search] Added industry filter:', { industry: { $in: dbIndustries } });
} else {
log('[Search] No industries mapped! Codes were:', industryList);
}
}
}
// Функция для парсинга диапазона из строки вида "51-250" или "500+"
const parseEmployeeRange = (sizeStr) => {
if (sizeStr.includes('+')) {
const min = parseInt(sizeStr.replace('+', ''));
return { min, max: Infinity };
}
const parts = sizeStr.split('-');
return {
min: parseInt(parts[0]),
max: parts[1] ? parseInt(parts[1]) : parseInt(parts[0])
};
};
// Функция для проверки пересечения двух диапазонов
const rangesOverlap = (range1, range2) => {
return range1.min <= range2.max && range1.max >= range2.min;
};
// Фильтр по размеру компании (чекбоксы) или кастомный диапазон
// Важно: этот фильтр должен получить все компании для корректной работы пересечения диапазонов
let sizeFilteredIds = null;
if ((companySize && companySize.length > 0) || minEmployees || maxEmployees) {
// Получаем все компании (без других фильтров, так как размер компании - это property-based фильтр)
const allCompanies = await Company.find({});
log('[Search] Employee size filter - checking companies:', allCompanies.length);
let matchingIds = [];
// Если есть кастомный диапазон - используем его
if (minEmployees || maxEmployees) {
const customRange = {
min: minEmployees ? parseInt(minEmployees, 10) : 0,
max: maxEmployees ? parseInt(maxEmployees, 10) : Infinity
};
log('[Search] Custom employee range filter:', customRange);
matchingIds = allCompanies
.filter(company => {
if (!company.companySize) {
log('[Search] Company has no size:', company.fullName);
return false;
}
const companyRange = parseEmployeeRange(company.companySize);
const overlaps = rangesOverlap(companyRange, customRange);
log('[Search] Checking overlap:', {
company: company.fullName,
companyRange,
customRange,
overlaps
});
return overlaps;
})
.map(c => c._id);
log('[Search] Matching companies by custom range:', matchingIds.length);
}
// Иначе используем чекбоксы
else if (companySize && companySize.length > 0) {
const sizeList = Array.isArray(companySize) ? companySize : [companySize];
log('[Search] Company size checkboxes filter:', sizeList);
matchingIds = allCompanies
.filter(company => {
if (!company.companySize) {
return false;
}
const companyRange = parseEmployeeRange(company.companySize);
// Проверяем пересечение с любым из выбранных диапазонов
const matches = sizeList.some(selectedSize => {
const filterRange = parseEmployeeRange(selectedSize);
const overlaps = rangesOverlap(companyRange, filterRange);
log('[Search] Check:', company.fullName, companyRange, 'vs', filterRange, '=', overlaps);
return overlaps;
});
return matches;
})
.map(c => c._id);
log('[Search] Matching companies by size checkboxes:', matchingIds.length);
}
// Сохраняем ID для дальнейшей фильтрации
sizeFilteredIds = matchingIds;
log('[Search] Size filtered IDs count:', sizeFilteredIds.length);
}
// Фильтр по географии
if (geography) {
const geoList = Array.isArray(geography) ? geography : [geography];
if (geoList.length > 0) {
filters.push({ partnerGeography: { $in: geoList } });
log('[Search] Geography filter:', { partnerGeography: { $in: geoList } });
}
}
// Фильтр по рейтингу
if (minRating) {
const rating = parseFloat(minRating);
if (rating > 0) {
filters.push({ rating: { $gte: rating } });
}
}
// Фильтр по отзывам
if (hasReviews === 'true') {
filters.push({ verified: true });
}
// Фильтр по акцептам
if (hasAcceptedDocs === 'true') {
filters.push({ verified: true });
}
// Применяем фильтр по размеру компании (если был задан)
if (sizeFilteredIds !== null) {
if (sizeFilteredIds.length > 0) {
filters.push({ _id: { $in: sizeFilteredIds } });
log('[Search] Applied size filter, IDs:', sizeFilteredIds.length);
} else {
// Если нет подходящих компаний по размеру, возвращаем пустой результат
filters.push({ _id: null });
log('[Search] No companies match size criteria');
}
}
// Комбинировать все фильтры
let filter = filters.length > 0 ? { $and: filters } : {};
// Пагинация - используем offset если передан, иначе вычисляем из page
const limitNum = parseInt(limit) || 10;
const skip = offset !== undefined ? parseInt(offset) : ((parseInt(page) || 1) - 1) * limitNum;
const pageNum = offset !== undefined ? Math.floor(skip / limitNum) + 1 : parseInt(page) || 1;
// Сортировка
let sortOptions = {};
if (sortBy === 'name') {
sortOptions.fullName = sortOrder === 'asc' ? 1 : -1;
} else {
sortOptions.rating = sortOrder === 'asc' ? 1 : -1;
}
log('[Search] Final MongoDB filter:', JSON.stringify(filter, null, 2));
let filterDebug = filters.length > 0 ? { $and: filters } : {};
const allCompanies = await Company.find({});
log('[Search] All companies in DB:', allCompanies.map(c => ({ name: c.fullName, geography: c.partnerGeography, industry: c.industry })));
const total = await Company.countDocuments(filter);
const companies = await Company.find(filter)
.sort(sortOptions)
.skip(skip)
.limit(limitNum);
const paginatedResults = companies.map(c => ({
...c.toObject(),
id: c._id
}));
log('[Search] Query:', query, 'Industries:', industries, 'Size:', companySize, 'Geo:', geography);
log('[Search] Total found:', total, 'Returning:', paginatedResults.length, 'companies');
log('[Search] Company details:', paginatedResults.map(c => ({ name: c.fullName, industry: c.industry })));
res.json({
companies: paginatedResults,
total,
page: pageNum,
totalPages: Math.ceil(total / limitNum),
_debug: {
filter: JSON.stringify(filter),
industriesReceived: industries
}
});
} catch (error) {
console.error('[Search] Error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
});
module.exports = router;