338 lines
12 KiB
JavaScript
338 lines
12 KiB
JavaScript
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;
|
||
|
||
|