diff --git a/package-lock.json b/package-lock.json index 540a4cd..a43c05c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", + "bcryptjs": "^3.0.2", "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", @@ -3721,6 +3722,15 @@ "node": ">= 10.0.0" } }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", diff --git a/package.json b/package.json index 948f3ea..a691838 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", + "bcryptjs": "^3.0.2", "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index f1ba3bc..5765845 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -1,565 +1,64 @@ -const router = require('express').Router(); +const express = require('express') +const dotenv = require('dotenv') -const timer = (time = 300) => (req, res, next) => setTimeout(next, time); +// Загрузить переменные окружения +dotenv.config() -// Настройка кодировки UTF-8 -router.use((req, res, next) => { - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - next(); -}); +// Подключение к MongoDB через mongoose +require('../../utils/mongoose') -router.use(timer()); +// Импортировать маршруты +const authRoutes = require('./routes/auth') +const companiesRoutes = require('./routes/companies') +const messagesRoutes = require('./routes/messages') +const searchRoutes = require('./routes/search') +const buyRoutes = require('./routes/buy') +const experienceRoutes = require('./routes/experience') +const productsRoutes = require('./routes/products') -// Загружаем моки через прямые импорты -const userMocks = require('./mocks/user.json'); -const companyMocks = require('./mocks/companies.json'); -const productMocks = require('./mocks/products.json'); -const searchMocks = require('./mocks/search.json'); -const authMocks = require('./mocks/auth.json'); +const mongoose = require('mongoose') +const app = express() -// Вспомогательные функции для генерации динамических данных -const generateTimestamp = () => Date.now(); -const generateDate = (daysAgo) => new Date(Date.now() - 86400000 * daysAgo).toISOString(); +// Задержка для имитации сети (опционально) +const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms) +app.use(delay()) -// Функция для замены плейсхолдеров в данных -const processMockData = (data) => { - if (data === undefined || data === null) { - return data; - } +// Health check endpoint +app.get('/health', (req, res) => { + const mongodbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected' + res.json({ + status: 'ok', + api: 'running', + database: mongodbStatus, + mongoUri: process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db', + timestamp: new Date().toISOString() + }) +}) - const timestamp = generateTimestamp(); - const jsonString = JSON.stringify(data); - - if (jsonString === undefined || jsonString === null) { - return data; - } +// Маршруты +app.use('/auth', authRoutes) +app.use('/companies', companiesRoutes) +app.use('/messages', messagesRoutes) +app.use('/search', searchRoutes) +app.use('/buy', buyRoutes) +app.use('/experience', experienceRoutes) +app.use('/products', productsRoutes) - const processedData = jsonString - .replace(/{{timestamp}}/g, timestamp) - .replace(/{{date-(\d+)-days?}}/g, (match, days) => generateDate(parseInt(days))) - .replace(/{{date-1-day}}/g, generateDate(1)) - .replace(/{{date-2-days}}/g, generateDate(2)) - .replace(/{{date-3-days}}/g, generateDate(3)) - .replace(/{{date-4-days}}/g, generateDate(4)) - .replace(/{{date-5-days}}/g, generateDate(5)) - .replace(/{{date-6-days}}/g, generateDate(6)) - .replace(/{{date-7-days}}/g, generateDate(7)) - .replace(/{{date-8-days}}/g, generateDate(8)) - .replace(/{{date-10-days}}/g, generateDate(10)) - .replace(/{{date-12-days}}/g, generateDate(12)) - .replace(/{{date-15-days}}/g, generateDate(15)) - .replace(/{{date-18-days}}/g, generateDate(18)) - .replace(/{{date-20-days}}/g, generateDate(20)) - .replace(/{{date-21-days}}/g, generateDate(21)) - .replace(/{{date-25-days}}/g, generateDate(25)) - .replace(/{{date-28-days}}/g, generateDate(28)) - .replace(/{{date-30-days}}/g, generateDate(30)) - .replace(/{{date-35-days}}/g, generateDate(35)); - - try { - return JSON.parse(processedData); - } catch (error) { - return data; - } -}; +// Обработка ошибок +app.use((err, req, res, next) => { + console.error('API Error:', err) + res.status(err.status || 500).json({ + error: err.message || 'Internal server error' + }) +}) -// Auth endpoints -router.post('/auth/login', (req, res) => { - const { email, password } = req.body; - - if (!email || !password) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks?.errorMessages?.validationFailed || 'Email и пароль обязательны' - }); - } - - // Имитация неверных учетных данных - if (password === 'wrong') { - return res.status(401).json({ - error: 'Unauthorized', - message: authMocks?.errorMessages?.invalidCredentials || 'Неверный email или пароль' - }); - } - - if (!authMocks?.mockAuthResponse) { - return res.status(500).json({ - error: 'Internal Server Error', - message: 'Ошибка загрузки данных аутентификации', - details: { - authMocksExists: !!authMocks, - authMocksType: typeof authMocks, - authMocksKeys: authMocks ? Object.keys(authMocks) : null - } - }); - } - - const authResponse = processMockData(authMocks.mockAuthResponse); - res.status(200).json(authResponse); -}); +// 404 handler +app.use((req, res) => { + res.status(404).json({ + error: 'Not found' + }) +}) -router.post('/auth/register', (req, res) => { - const { email, password, inn, agreeToTerms } = req.body; - - if (!email || !password || !inn) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.validationFailed || 'Заполните все обязательные поля' - }); - } - - if (!agreeToTerms) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.termsRequired || 'Необходимо принять условия использования' - }); - } - - // Создаем нового пользователя с данными из регистрации - const newUser = { - id: 'user-' + generateTimestamp(), - email: email, - firstName: req.body.firstName || 'Иван', - lastName: req.body.lastName || 'Петров', - position: req.body.position || 'Директор' - }; - - const newCompany = { - id: 'company-' + generateTimestamp(), - name: req.body.fullName || companyMocks.mockCompany?.name, - inn: req.body.inn, - ogrn: req.body.ogrn || companyMocks.mockCompany?.ogrn, - fullName: req.body.fullName || companyMocks.mockCompany?.fullName, - shortName: req.body.shortName, - legalForm: req.body.legalForm || 'ООО', - industry: req.body.industry || 'Другое', - companySize: req.body.companySize || '1-10', - website: req.body.website || '', - verified: false, - rating: 0 - }; - - res.status(201).json({ - user: newUser, - company: newCompany, - tokens: { - accessToken: 'mock-access-token-' + generateTimestamp(), - refreshToken: 'mock-refresh-token-' + generateTimestamp() - } - }); -}); - -router.post('/auth/logout', (req, res) => { - res.status(200).json({ - message: authMocks.successMessages?.logoutSuccess || 'Успешный выход' - }); -}); - -router.post('/auth/refresh', (req, res) => { - const { refreshToken } = req.body; - - if (!refreshToken) { - return res.status(401).json({ - error: 'Unauthorized', - message: authMocks.errorMessages?.refreshTokenRequired || 'Refresh token обязателен' - }); - } - - res.status(200).json({ - accessToken: 'mock-access-token-refreshed-' + generateTimestamp(), - refreshToken: 'mock-refresh-token-refreshed-' + generateTimestamp() - }); -}); - -router.get('/auth/verify-email/:token', (req, res) => { - res.status(200).json({ - message: authMocks.successMessages?.emailVerified || 'Email успешно подтвержден' - }); -}); - -router.post('/auth/request-password-reset', (req, res) => { - const { email } = req.body; - - if (!email) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.emailRequired || 'Email обязателен' - }); - } - - res.status(200).json({ - message: authMocks.successMessages?.passwordResetSent || 'Письмо для восстановления пароля отправлено' - }); -}); - -router.post('/auth/reset-password', (req, res) => { - const { token, newPassword } = req.body; - - if (!token || !newPassword) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.validationFailed || 'Token и новый пароль обязательны' - }); - } - - res.status(200).json({ - message: authMocks.successMessages?.passwordResetSuccess || 'Пароль успешно изменен' - }); -}); - -// Companies endpoints -router.get('/companies/my/stats', (req, res) => { - res.status(200).json({ - profileViews: 142, - profileViewsChange: 12, - sentRequests: 8, - sentRequestsChange: 2, - receivedRequests: 15, - receivedRequestsChange: 5, - newMessages: 3, - rating: 4.5 - }); -}); - -router.get('/companies/:id', (req, res) => { - const company = processMockData(companyMocks.mockCompany); - res.status(200).json(company); -}); - -router.patch('/companies/:id', (req, res) => { - const updatedCompany = { - ...processMockData(companyMocks.mockCompany), - ...req.body, - id: req.params.id - }; - - res.status(200).json(updatedCompany); -}); - -router.get('/companies/:id/stats', (req, res) => { - res.status(200).json({ - profileViews: 142, - profileViewsChange: 12, - sentRequests: 8, - sentRequestsChange: 2, - receivedRequests: 15, - receivedRequestsChange: 5, - newMessages: 3, - rating: 4.5 - }); -}); - -router.post('/companies/:id/logo', (req, res) => { - res.status(200).json({ - logoUrl: 'https://via.placeholder.com/200x200/4299E1/FFFFFF?text=Logo' - }); -}); - -router.get('/companies/check-inn/:inn', (req, res) => { - const inn = req.params.inn; - - // Имитация проверки ИНН - if (inn.length !== 10 && inn.length !== 12) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.innValidation || 'ИНН должен содержать 10 или 12 цифр' - }); - } - - const mockINNData = companyMocks.mockINNData || {}; - const companyData = mockINNData[inn] || { - name: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ТЕСТОВАЯ КОМПАНИЯ ' + inn + '"', - ogrn: '10277' + inn, - legal_form: 'ООО' - }; - - res.status(200).json({ data: companyData }); -}); - -// Products endpoints -router.get('/products/my', (req, res) => { - const products = processMockData(productMocks.mockProducts); - res.status(200).json(products); -}); - -router.get('/products', (req, res) => { - const products = processMockData(productMocks.mockProducts); - res.status(200).json({ - items: products, - total: products.length, - page: 1, - pageSize: 20 - }); -}); - -router.post('/products', (req, res) => { - const newProduct = { - id: 'prod-' + generateTimestamp(), - ...req.body, - companyId: 'company-123', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; - - res.status(201).json(newProduct); -}); - -router.get('/products/:id', (req, res) => { - const products = processMockData(productMocks.mockProducts); - const product = products.find(p => p.id === req.params.id); - - if (product) { - res.status(200).json(product); - } else { - res.status(200).json({ - id: req.params.id, - name: 'Продукт ' + req.params.id, - description: 'Описание продукта', - category: 'Категория', - type: 'sell', - companyId: 'company-123', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }); - } -}); - -router.patch('/products/:id', (req, res) => { - const products = processMockData(productMocks.mockProducts); - const product = products.find(p => p.id === req.params.id); - - const updatedProduct = { - ...(product || {}), - ...req.body, - id: req.params.id, - updatedAt: new Date().toISOString() - }; - - res.status(200).json(updatedProduct); -}); - -router.delete('/products/:id', (req, res) => { - res.status(204).send(); -}); - -// Тестовый endpoint для проверки данных -router.get('/test-data', (req, res) => { - res.status(200).json({ - companiesCount: companyMocks.mockCompanies?.length || 0, - suggestionsCount: searchMocks.suggestions?.length || 0, - firstCompany: companyMocks.mockCompanies?.[0] || null, - firstSuggestion: searchMocks.suggestions?.[0] || null, - allSuggestions: searchMocks.suggestions || [] - }); -}); -router.get('/search', (req, res) => { - const { - query, - industries, - companySize, - geography, - minRating, - type, - sortBy = 'relevance', - sortOrder = 'desc', - page = 1, - limit = 20 - } = req.query; - - const companies = processMockData(companyMocks.mockCompanies); - let filtered = [...companies]; - - if (query) { - const q = query.toLowerCase().trim(); - - filtered = filtered.filter(c => { - const fullName = (c.fullName || '').toLowerCase(); - const shortName = (c.shortName || '').toLowerCase(); - const industry = (c.industry || '').toLowerCase(); - const slogan = (c.slogan || '').toLowerCase(); - const legalAddress = (c.legalAddress || '').toLowerCase(); - - return fullName.includes(q) || - shortName.includes(q) || - industry.includes(q) || - slogan.includes(q) || - legalAddress.includes(q); - }); - } - - // Фильтр по отраслям - if (industries && industries.length > 0) { - const industriesArray = Array.isArray(industries) ? industries : [industries]; - filtered = filtered.filter(c => industriesArray.includes(c.industry)); - } - - // Фильтр по размеру компании - if (companySize && companySize.length > 0) { - const sizeArray = Array.isArray(companySize) ? companySize : [companySize]; - filtered = filtered.filter(c => sizeArray.includes(c.companySize)); - } - - // Фильтр по рейтингу - if (minRating) { - filtered = filtered.filter(c => c.rating >= parseFloat(minRating)); - } - - // Сортировка - filtered.sort((a, b) => { - let comparison = 0; - - switch (sortBy) { - case 'rating': - comparison = a.rating - b.rating; - break; - case 'name': - comparison = (a.shortName || a.fullName).localeCompare(b.shortName || b.fullName); - break; - case 'relevance': - default: - // Для релевантности используем рейтинг как основной критерий - comparison = a.rating - b.rating; - break; - } - - return sortOrder === 'asc' ? comparison : -comparison; - }); - - const total = filtered.length; - const totalPages = Math.ceil(total / limit); - const startIndex = (page - 1) * limit; - const endIndex = startIndex + parseInt(limit); - const paginatedResults = filtered.slice(startIndex, endIndex); - - res.status(200).json({ - companies: paginatedResults, - total, - page: parseInt(page), - totalPages - }); -}); - -router.post('/search/ai', (req, res) => { - const { query } = req.body; - - // Простая логика AI поиска на основе ключевых слов - const companies = processMockData(companyMocks.mockCompanies); - let aiResults = [...companies]; - const q = query.toLowerCase(); - - // Определяем приоритетные отрасли на основе запроса - if (q.includes('строитель') || q.includes('строй') || q.includes('дом') || q.includes('здание')) { - aiResults = aiResults.filter(c => c.industry === 'Строительство'); - } else if (q.includes('металл') || q.includes('сталь') || q.includes('производств') || q.includes('завод')) { - aiResults = aiResults.filter(c => c.industry === 'Производство'); - } else if (q.includes('логистик') || q.includes('доставк') || q.includes('транспорт')) { - aiResults = aiResults.filter(c => c.industry === 'Логистика'); - } else if (q.includes('торговл') || q.includes('продаж') || q.includes('снабжени')) { - aiResults = aiResults.filter(c => c.industry === 'Торговля'); - } else if (q.includes('it') || q.includes('программ') || q.includes('технолог') || q.includes('софт')) { - aiResults = aiResults.filter(c => c.industry === 'IT'); - } else if (q.includes('услуг') || q.includes('консалт') || q.includes('помощь')) { - aiResults = aiResults.filter(c => c.industry === 'Услуги'); - } - - // Сортируем по рейтингу и берем топ-5 - aiResults.sort((a, b) => b.rating - a.rating); - const topResults = aiResults.slice(0, 5); - - // Генерируем AI предложение - let aiSuggestion = `На основе вашего запроса "${query}" мы нашли ${topResults.length} подходящих партнеров. `; - - if (topResults.length > 0) { - const industries = [...new Set(topResults.map(c => c.industry))]; - aiSuggestion += `Рекомендуем обратить внимание на компании в сфере ${industries.join(', ')}. `; - aiSuggestion += `Все предложенные партнеры имеют высокий рейтинг (от ${Math.min(...topResults.map(c => c.rating)).toFixed(1)} до ${Math.max(...topResults.map(c => c.rating)).toFixed(1)}) и подтвержденный статус.`; - } else { - aiSuggestion += 'Попробуйте изменить формулировку запроса или использовать фильтры для более точного поиска.'; - } - - res.status(200).json({ - companies: topResults, - total: topResults.length, - page: 1, - totalPages: 1, - aiSuggestion - }); -}); - -router.get('/search/suggestions', (req, res) => { - const { q } = req.query; - - const suggestions = searchMocks.suggestions || []; - - const filtered = q - ? suggestions.filter(s => s.toLowerCase().includes(q.toLowerCase())) - : suggestions.slice(0, 10); - - res.status(200).json(filtered); -}); - -router.get('/search/recommendations', (req, res) => { - // Динамически генерируем рекомендации на основе топовых компаний - const companies = processMockData(companyMocks.mockCompanies); - const topCompanies = companies - .filter(c => c.verified && c.rating >= 4.5) - .sort((a, b) => b.rating - a.rating) - .slice(0, 6); - - const recommendations = topCompanies.map(company => ({ - id: company.id, - name: company.shortName || company.fullName, - industry: company.industry, - logo: company.logo, - matchScore: Math.floor(company.rating * 20), // Конвертируем рейтинг в проценты - reason: getRecommendationReason(company, searchMocks.recommendationReasons) - })); - - res.status(200).json(recommendations); -}); - -// Вспомогательная функция для генерации причин рекомендаций -function getRecommendationReason(company, reasons) { - return reasons?.[company.industry] || 'Проверенный партнер с высоким рейтингом'; -} - -router.get('/search/history', (req, res) => { - const history = processMockData(searchMocks.searchHistory); - res.status(200).json(history); -}); - -router.get('/search/saved', (req, res) => { - const savedSearches = processMockData(searchMocks.savedSearches); - res.status(200).json(savedSearches); -}); - -router.post('/search/saved', (req, res) => { - const { name, params } = req.body; - - res.status(201).json({ - id: 'saved-' + generateTimestamp(), - name, - params, - createdAt: new Date().toISOString() - }); -}); - -router.delete('/search/saved/:id', (req, res) => { - res.status(204).send(); -}); - -router.post('/search/favorites/:companyId', (req, res) => { - res.status(200).json({ - message: authMocks.successMessages?.addedToFavorites || 'Добавлено в избранное' - }); -}); - -router.delete('/search/favorites/:companyId', (req, res) => { - res.status(204).send(); -}); - -module.exports = router; \ No newline at end of file +// Экспортировать для использования в brojs +module.exports = app \ No newline at end of file diff --git a/server/routers/procurement/middleware/auth.js b/server/routers/procurement/middleware/auth.js new file mode 100644 index 0000000..b126134 --- /dev/null +++ b/server/routers/procurement/middleware/auth.js @@ -0,0 +1,27 @@ +const jwt = require('jsonwebtoken') + +const verifyToken = (req, res, next) => { + const token = req.headers.authorization?.replace('Bearer ', '') + + if (!token) { + return res.status(401).json({ error: 'No token provided' }) + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key') + req.user = decoded + next() + } catch (error) { + return res.status(401).json({ error: 'Invalid token' }) + } +} + +const generateToken = (userId, email) => { + return jwt.sign( + { userId, email }, + process.env.JWT_SECRET || 'your-secret-key', + { expiresIn: '7d' } + ) +} + +module.exports = { verifyToken, generateToken } diff --git a/server/routers/procurement/models/Company.js b/server/routers/procurement/models/Company.js new file mode 100644 index 0000000..ca1a87d --- /dev/null +++ b/server/routers/procurement/models/Company.js @@ -0,0 +1,64 @@ +const mongoose = require('mongoose') + +const companySchema = new mongoose.Schema({ + fullName: { + type: String, + required: true + }, + shortName: String, + inn: { + type: String, + unique: true, + sparse: true + }, + ogrn: String, + legalForm: String, + industry: String, + companySize: String, + website: String, + phone: String, + email: String, + slogan: String, + description: String, + foundedYear: Number, + employeeCount: String, + revenue: String, + legalAddress: String, + actualAddress: String, + bankDetails: String, + logo: String, + rating: { + type: Number, + default: 0, + min: 0, + max: 5 + }, + reviews: { + type: Number, + default: 0 + }, + ownerId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + platformGoals: [String], + productsOffered: String, + productsNeeded: String, + partnerIndustries: [String], + partnerGeography: [String], + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// Индексы для поиска +companySchema.index({ fullName: 'text', shortName: 'text', description: 'text' }) +companySchema.index({ industry: 1 }) +companySchema.index({ rating: -1 }) + +module.exports = mongoose.model('Company', companySchema) diff --git a/server/routers/procurement/models/Message.js b/server/routers/procurement/models/Message.js new file mode 100644 index 0000000..3e29204 --- /dev/null +++ b/server/routers/procurement/models/Message.js @@ -0,0 +1,37 @@ +const mongoose = require('mongoose') + +const messageSchema = new mongoose.Schema({ + threadId: { + type: String, + required: true, + index: true + }, + senderCompanyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true + }, + recipientCompanyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true + }, + text: { + type: String, + required: true + }, + read: { + type: Boolean, + default: false + }, + timestamp: { + type: Date, + default: Date.now, + index: true + } +}) + +// Индекс для быстрого поиска сообщений потока +messageSchema.index({ threadId: 1, timestamp: -1 }) + +module.exports = mongoose.model('Message', messageSchema) diff --git a/server/routers/procurement/models/Product.js b/server/routers/procurement/models/Product.js new file mode 100644 index 0000000..4926923 --- /dev/null +++ b/server/routers/procurement/models/Product.js @@ -0,0 +1,46 @@ +const mongoose = require('mongoose') + +const productSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + category: { + type: String, + required: true + }, + description: { + type: String, + required: true, + minlength: 20, + maxlength: 500 + }, + type: { + type: String, + enum: ['sell', 'buy'], + required: true + }, + productUrl: String, + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true + }, + price: String, + unit: String, + minOrder: String, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// Индекс для поиска +productSchema.index({ companyId: 1, type: 1 }) +productSchema.index({ name: 'text', description: 'text' }) + +module.exports = mongoose.model('Product', productSchema) diff --git a/server/routers/procurement/models/User.js b/server/routers/procurement/models/User.js new file mode 100644 index 0000000..7a604d9 --- /dev/null +++ b/server/routers/procurement/models/User.js @@ -0,0 +1,67 @@ +const mongoose = require('mongoose') +const bcrypt = require('bcryptjs') + +const userSchema = new mongoose.Schema({ + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + }, + password: { + type: String, + required: true, + minlength: 8 + }, + firstName: { + type: String, + required: true + }, + lastName: { + type: String, + required: true + }, + position: String, + phone: String, + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company' + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// Хешировать пароль перед сохранением +userSchema.pre('save', async function(next) { + if (!this.isModified('password')) return next() + + try { + const salt = await bcrypt.genSalt(10) + this.password = await bcrypt.hash(this.password, salt) + next() + } catch (error) { + next(error) + } +}) + +// Метод для сравнения паролей +userSchema.methods.comparePassword = async function(candidatePassword) { + return await bcrypt.compare(candidatePassword, this.password) +} + +// Скрыть пароль при преобразовании в JSON +userSchema.methods.toJSON = function() { + const obj = this.toObject() + delete obj.password + return obj +} + +module.exports = mongoose.model('User', userSchema) diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js new file mode 100644 index 0000000..cd7bdc7 --- /dev/null +++ b/server/routers/procurement/routes/auth.js @@ -0,0 +1,103 @@ +const express = require('express') +const router = express.Router() +const User = require('../models/User') +const Company = require('../models/Company') +const { generateToken } = require('../middleware/auth') + +// Регистрация +router.post('/register', async (req, res) => { + try { + const { email, password, firstName, lastName, position, phone, fullName, inn, ogrn, legalForm, industry, companySize, website } = req.body; + + // Проверка обязательных полей + if (!email || !password || !firstName || !lastName || !fullName) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + // Проверка существования пользователя + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(409).json({ error: 'User already exists' }); + } + + // Создать компанию + const company = await Company.create({ + fullName, + inn, + ogrn, + legalForm, + industry, + companySize, + website + }); + + // Создать пользователя + const user = await User.create({ + email, + password, + firstName, + lastName, + position, + phone, + companyId: company._id + }); + + // Генерировать токен + const token = generateToken(user._id, user.email); + + res.status(201).json({ + tokens: { + accessToken: token, + refreshToken: token + }, + user: user.toJSON(), + company: company.toObject() + }); + } catch (error) { + console.error('Registration error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Логин +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: 'Email and password required' }); + } + + // Найти пользователя + const user = await User.findOne({ email }); + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Проверить пароль + const isValid = await user.comparePassword(password); + if (!isValid) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Загрузить компанию + const company = await Company.findById(user.companyId); + + // Генерировать токен + const token = generateToken(user._id, user.email); + + res.json({ + tokens: { + accessToken: token, + refreshToken: token + }, + user: user.toJSON(), + company: company?.toObject() || null + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js new file mode 100644 index 0000000..8155341 --- /dev/null +++ b/server/routers/procurement/routes/buy.js @@ -0,0 +1,186 @@ +const express = require('express') +const fs = require('fs') +const path = require('path') +const router = express.Router() + +// Create remote-assets/docs directory if it doesn't exist +const docsDir = path.resolve('server/remote-assets/docs') +if (!fs.existsSync(docsDir)) { + fs.mkdirSync(docsDir, { recursive: true }) +} + +// In-memory store for documents metadata +const buyDocs = [] + +function generateId() { + return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}` +} + +// GET /buy/docs?ownerCompanyId=... +router.get('/docs', (req, res) => { + const { ownerCompanyId } = req.query + console.log('[BUY API] GET /docs', { ownerCompanyId, totalDocs: buyDocs.length }) + let result = buyDocs + if (ownerCompanyId) { + result = result.filter((d) => d.ownerCompanyId === ownerCompanyId) + } + result = result.map(doc => ({ + ...doc, + url: `/api/buy/docs/${doc.id}/file` + })) + res.json(result) +}) + +// POST /buy/docs +router.post('/docs', (req, res) => { + const { ownerCompanyId, name, type, fileData } = req.body || {} + console.log('[BUY API] POST /docs', { ownerCompanyId, name, type }) + if (!ownerCompanyId || !name || !type) { + return res.status(400).json({ error: 'ownerCompanyId, name and type are required' }) + } + + if (!fileData) { + return res.status(400).json({ error: 'fileData is required' }) + } + + const id = generateId() + + // Save file to disk + try { + const binaryData = Buffer.from(fileData, 'base64') + const filePath = path.join(docsDir, `${id}.${type}`) + fs.writeFileSync(filePath, binaryData) + console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`) + + const size = binaryData.length + const url = `/api/buy/docs/${id}/file` + const doc = { + id, + ownerCompanyId, + name, + type, + size, + url, + filePath, + acceptedBy: [], + createdAt: new Date().toISOString(), + } + buyDocs.unshift(doc) + console.log('[BUY API] Document created:', id) + res.status(201).json(doc) + } catch (e) { + console.error(`[BUY API] Error saving file: ${e.message}`) + res.status(500).json({ error: 'Failed to save file' }) + } +}) + +router.post('/docs/:id/accept', (req, res) => { + const { id } = req.params + const { companyId } = req.body || {} + console.log('[BUY API] POST /docs/:id/accept', { id, companyId }) + const doc = buyDocs.find((d) => d.id === id) + if (!doc) { + console.log('[BUY API] Document not found:', id) + return res.status(404).json({ error: 'Document not found' }) + } + if (!companyId) { + return res.status(400).json({ error: 'companyId is required' }) + } + if (!doc.acceptedBy.includes(companyId)) { + doc.acceptedBy.push(companyId) + } + res.json({ id: doc.id, acceptedBy: doc.acceptedBy }) +}) + +router.get('/docs/:id/delete', (req, res) => { + const { id } = req.params + console.log('[BUY API] GET /docs/:id/delete', { id, totalDocs: buyDocs.length }) + const index = buyDocs.findIndex((d) => d.id === id) + if (index === -1) { + console.log('[BUY API] Document not found for deletion:', id) + return res.status(404).json({ error: 'Document not found' }) + } + const deletedDoc = buyDocs.splice(index, 1)[0] + + // Delete file from disk + if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) { + try { + fs.unlinkSync(deletedDoc.filePath) + console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`) + } catch (e) { + console.error(`[BUY API] Error deleting file: ${e.message}`) + } + } + + console.log('[BUY API] Document deleted via GET:', id, { remainingDocs: buyDocs.length }) + res.json({ id: deletedDoc.id, success: true }) +}) + +router.delete('/docs/:id', (req, res) => { + const { id } = req.params + console.log('[BUY API] DELETE /docs/:id', { id, totalDocs: buyDocs.length }) + const index = buyDocs.findIndex((d) => d.id === id) + if (index === -1) { + console.log('[BUY API] Document not found for deletion:', id) + return res.status(404).json({ error: 'Document not found' }) + } + const deletedDoc = buyDocs.splice(index, 1)[0] + + // Delete file from disk + if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) { + try { + fs.unlinkSync(deletedDoc.filePath) + console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`) + } catch (e) { + console.error(`[BUY API] Error deleting file: ${e.message}`) + } + } + + console.log('[BUY API] Document deleted:', id, { remainingDocs: buyDocs.length }) + res.json({ id: deletedDoc.id, success: true }) +}) + +// GET /buy/docs/:id/file - Serve the file +router.get('/docs/:id/file', (req, res) => { + const { id } = req.params + console.log('[BUY API] GET /docs/:id/file', { id }) + + const doc = buyDocs.find(d => d.id === id) + if (!doc) { + console.log('[BUY API] Document not found:', id) + return res.status(404).json({ error: 'Document not found' }) + } + + const filePath = path.join(docsDir, `${id}.${doc.type}`) + if (!fs.existsSync(filePath)) { + console.log('[BUY API] File not found on disk:', filePath) + return res.status(404).json({ error: 'File not found on disk' }) + } + + try { + const fileBuffer = fs.readFileSync(filePath) + + const mimeTypes = { + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'pdf': 'application/pdf' + } + + const mimeType = mimeTypes[doc.type] || 'application/octet-stream' + const sanitizedName = doc.name.replace(/[^\w\s\-\.]/g, '_') + + res.setHeader('Content-Type', mimeType) + // RFC 5987 encoding: filename for ASCII fallback, filename* for UTF-8 with percent-encoding + const encodedFilename = encodeURIComponent(`${doc.name}.${doc.type}`) + res.setHeader('Content-Disposition', `attachment; filename="${sanitizedName}.${doc.type}"; filename*=UTF-8''${encodedFilename}`) + res.setHeader('Content-Length', fileBuffer.length) + + console.log(`[BUY API] Serving file ${id} from ${filePath} (${fileBuffer.length} bytes)`) + res.send(fileBuffer) + } catch (e) { + console.error(`[BUY API] Error serving file: ${e.message}`) + res.status(500).json({ error: 'Error serving file' }) + } +}) + +module.exports = router \ No newline at end of file diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js new file mode 100644 index 0000000..380fecf --- /dev/null +++ b/server/routers/procurement/routes/companies.js @@ -0,0 +1,103 @@ +const express = require('express') +const router = express.Router() +const Company = require('../models/Company') +const { verifyToken } = require('../middleware/auth') + +// Получить все компании +router.get('/', async (req, res) => { + try { + const { page = 1, limit = 10, search = '', industry = '' } = req.query; + + let query = {}; + + if (search) { + query.$text = { $search: search }; + } + + if (industry) { + query.industry = industry; + } + + const skip = (page - 1) * limit; + + const companies = await Company.find(query) + .limit(Number(limit)) + .skip(Number(skip)) + .sort({ rating: -1 }); + + const total = await Company.countDocuments(query); + + res.json({ + companies, + total, + page: Number(page), + limit: Number(limit), + pages: Math.ceil(total / limit) + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Получить компанию по ID +router.get('/:id', async (req, res) => { + try { + const company = await Company.findById(req.params.id).populate('ownerId', 'firstName lastName email'); + + if (!company) { + return res.status(404).json({ error: 'Company not found' }); + } + + res.json(company); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Обновить компанию (требует авторизации) +const updateCompanyHandler = async (req, res) => { + try { + const company = await Company.findByIdAndUpdate( + req.params.id, + { ...req.body, updatedAt: new Date() }, + { new: true } + ); + + if (!company) { + return res.status(404).json({ error: 'Company not found' }); + } + + res.json(company); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +router.put('/:id', verifyToken, updateCompanyHandler); +router.patch('/:id', verifyToken, updateCompanyHandler); + +// Поиск с AI анализом +router.post('/ai-search', async (req, res) => { + try { + const { query } = req.body; + + if (!query) { + return res.status(400).json({ error: 'Query required' }); + } + + // Простой поиск по текстовым полям + const companies = await Company.find({ + $text: { $search: query } + }).limit(10); + + res.json({ + companies, + total: companies.length, + aiSuggestion: `Found ${companies.length} companies matching "${query}"` + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router diff --git a/server/routers/procurement/routes/experience.js b/server/routers/procurement/routes/experience.js new file mode 100644 index 0000000..5ca6d47 --- /dev/null +++ b/server/routers/procurement/routes/experience.js @@ -0,0 +1,114 @@ +const express = require('express') +const router = express.Router() +const { verifyToken } = require('../middleware/auth') + +// In-memory хранилище для опыта работы (mock) +let experiences = []; + +// GET /experience - Получить список опыта работы компании +router.get('/', verifyToken, (req, res) => { + try { + const { companyId } = req.query; + + if (!companyId) { + return res.status(400).json({ error: 'companyId is required' }); + } + + const companyExperiences = experiences.filter(exp => exp.companyId === companyId); + res.json(companyExperiences); + } catch (error) { + console.error('Get experience error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// POST /experience - Создать запись опыта работы +router.post('/', verifyToken, (req, res) => { + try { + const { companyId, data } = req.body; + + if (!companyId || !data) { + return res.status(400).json({ error: 'companyId and data are required' }); + } + + const { confirmed, customer, subject, volume, contact, comment } = data; + + if (!customer || !subject) { + return res.status(400).json({ error: 'customer and subject are required' }); + } + + const newExperience = { + id: `exp-${Date.now()}`, + companyId, + confirmed: confirmed || false, + customer, + subject, + volume: volume || '', + contact: contact || '', + comment: comment || '', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + experiences.push(newExperience); + + res.status(201).json(newExperience); + } catch (error) { + console.error('Create experience error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// PUT /experience/:id - Обновить запись опыта работы +router.put('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + const { data } = req.body; + + if (!data) { + return res.status(400).json({ error: 'data is required' }); + } + + const index = experiences.findIndex(exp => exp.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Experience not found' }); + } + + const updatedExperience = { + ...experiences[index], + ...data, + updatedAt: new Date().toISOString() + }; + + experiences[index] = updatedExperience; + + res.json(updatedExperience); + } catch (error) { + console.error('Update experience error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// DELETE /experience/:id - Удалить запись опыта работы +router.delete('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + + const index = experiences.findIndex(exp => exp.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Experience not found' }); + } + + experiences.splice(index, 1); + + res.json({ message: 'Experience deleted successfully' }); + } catch (error) { + console.error('Delete experience error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router + diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js new file mode 100644 index 0000000..6b52eca --- /dev/null +++ b/server/routers/procurement/routes/messages.js @@ -0,0 +1,130 @@ +const express = require('express') +const router = express.Router() +const Message = require('../models/Message') +const { verifyToken } = require('../middleware/auth') + +// Mock данные для тредов +const mockThreads = [ + { + id: 'thread-1', + lastMessage: 'Добрый день! Интересует поставка металлопроката.', + lastMessageAt: new Date(Date.now() - 3600000).toISOString(), + participants: ['company-123', 'company-1'] + }, + { + id: 'thread-2', + lastMessage: 'Можем предложить скидку 15% на оптовую партию.', + lastMessageAt: new Date(Date.now() - 7200000).toISOString(), + participants: ['company-123', 'company-2'] + }, + { + id: 'thread-3', + lastMessage: 'Спасибо за предложение, рассмотрим.', + lastMessageAt: new Date(Date.now() - 86400000).toISOString(), + participants: ['company-123', 'company-4'] + } +]; + +// Mock данные для сообщений +const mockMessages = { + 'thread-1': [ + { id: 'msg-1', senderCompanyId: 'company-1', text: 'Добрый день! Интересует поставка металлопроката.', timestamp: new Date(Date.now() - 3600000).toISOString() }, + { id: 'msg-2', senderCompanyId: 'company-123', text: 'Здравствуйте! Какой объем вас интересует?', timestamp: new Date(Date.now() - 3500000).toISOString() } + ], + 'thread-2': [ + { id: 'msg-3', senderCompanyId: 'company-2', text: 'Можем предложить скидку 15% на оптовую партию.', timestamp: new Date(Date.now() - 7200000).toISOString() } + ], + 'thread-3': [ + { id: 'msg-4', senderCompanyId: 'company-4', text: 'Спасибо за предложение, рассмотрим.', timestamp: new Date(Date.now() - 86400000).toISOString() } + ] +}; + +// Получить все потоки для компании +router.get('/threads', verifyToken, async (req, res) => { + try { + // Попытка получить из MongoDB + try { + const threads = await Message.aggregate([ + { + $match: { + $or: [ + { senderCompanyId: req.user.companyId }, + { recipientCompanyId: req.user.companyId } + ] + } + }, + { + $sort: { timestamp: -1 } + }, + { + $group: { + _id: '$threadId', + lastMessage: { $first: '$text' }, + lastMessageAt: { $first: '$timestamp' } + } + } + ]); + + if (threads && threads.length > 0) { + return res.json(threads); + } + } catch (dbError) { + console.log('MongoDB unavailable, using mock data'); + } + + // Fallback на mock данные + res.json(mockThreads); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Получить сообщения потока +router.get('/threads/:threadId', verifyToken, async (req, res) => { + try { + // Попытка получить из MongoDB + try { + const messages = await Message.find({ threadId: req.params.threadId }) + .sort({ timestamp: 1 }) + .populate('senderCompanyId', 'shortName fullName') + .populate('recipientCompanyId', 'shortName fullName'); + + if (messages && messages.length > 0) { + return res.json(messages); + } + } catch (dbError) { + console.log('MongoDB unavailable, using mock data'); + } + + // Fallback на mock данные + const threadMessages = mockMessages[req.params.threadId] || []; + res.json(threadMessages); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Отправить сообщение +router.post('/', verifyToken, async (req, res) => { + try { + const { threadId, text, recipientCompanyId } = req.body; + + if (!text || !threadId) { + return res.status(400).json({ error: 'Text and threadId required' }); + } + + const message = await Message.create({ + threadId, + senderCompanyId: req.user.companyId, + recipientCompanyId, + text, + timestamp: new Date() + }); + + res.status(201).json(message); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router diff --git a/server/routers/procurement/routes/products.js b/server/routers/procurement/routes/products.js new file mode 100644 index 0000000..03f7a29 --- /dev/null +++ b/server/routers/procurement/routes/products.js @@ -0,0 +1,130 @@ +const express = require('express') +const router = express.Router() +const { verifyToken } = require('../middleware/auth') + +// In-memory хранилище для продуктов/услуг (mock) +let products = []; + +// GET /products - Получить список продуктов/услуг компании +router.get('/', verifyToken, (req, res) => { + try { + const { companyId } = req.query; + + if (!companyId) { + return res.status(400).json({ error: 'companyId is required' }); + } + + const companyProducts = products.filter(p => p.companyId === companyId); + res.json(companyProducts); + } catch (error) { + console.error('Get products error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// POST /products - Создать продукт/услугу +router.post('/', verifyToken, (req, res) => { + try { + const { companyId, name, category, description, price, unit } = req.body; + + if (!companyId || !name) { + return res.status(400).json({ error: 'companyId and name are required' }); + } + + const newProduct = { + id: `prod-${Date.now()}`, + companyId, + name, + category: category || 'other', + description: description || '', + price: price || '', + unit: unit || '', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + products.push(newProduct); + + res.status(201).json(newProduct); + } catch (error) { + console.error('Create product error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// PUT /products/:id - Обновить продукт/услугу +router.put('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + + const index = products.findIndex(p => p.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Product not found' }); + } + + const updatedProduct = { + ...products[index], + ...updates, + updatedAt: new Date().toISOString() + }; + + products[index] = updatedProduct; + + res.json(updatedProduct); + } catch (error) { + console.error('Update product error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// PATCH /products/:id - Частичное обновление продукта/услуги +router.patch('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + + const index = products.findIndex(p => p.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Product not found' }); + } + + const updatedProduct = { + ...products[index], + ...updates, + updatedAt: new Date().toISOString() + }; + + products[index] = updatedProduct; + + res.json(updatedProduct); + } catch (error) { + console.error('Patch product error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// DELETE /products/:id - Удалить продукт/услугу +router.delete('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + + const index = products.findIndex(p => p.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Product not found' }); + } + + products.splice(index, 1); + + res.json({ message: 'Product deleted successfully' }); + } catch (error) { + console.error('Delete product error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router + diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js new file mode 100644 index 0000000..c852fdd --- /dev/null +++ b/server/routers/procurement/routes/search.js @@ -0,0 +1,99 @@ +const express = require('express') +const router = express.Router() +const { verifyToken } = require('../middleware/auth') +const mockCompaniesData = require('../mocks/companies.json') +const mockCompanies = mockCompaniesData.mockCompanies + +// Маппинг отраслей для фильтрации +const industryMapping = { + 'it': 'IT', + 'finance': 'Финансы', + 'manufacturing': 'Производство', + 'construction': 'Строительство', + 'retail': 'Торговля', + 'wholesale': 'Торговля', + 'logistics': 'Логистика', + 'healthcare': 'Медицина' +}; + +// GET /search/companies - Поиск компаний +router.get('/companies', verifyToken, (req, res) => { + try { + const { + query = '', + industries = '', + companySizes = '', + geographies = '', + minRating = 0, + hasReviews, + hasAccepts + } = req.query; + + let result = [...mockCompanies]; + + // Фильтр по текстовому запросу + if (query.trim()) { + const q = query.toLowerCase(); + result = result.filter(c => + c.fullName.toLowerCase().includes(q) || + c.shortName.toLowerCase().includes(q) || + c.slogan.toLowerCase().includes(q) || + c.industry.toLowerCase().includes(q) + ); + } + + // Фильтр по отраслям + if (industries) { + const selectedIndustries = industries.split(','); + result = result.filter(c => { + const mappedIndustries = selectedIndustries.map(i => industryMapping[i] || i); + return mappedIndustries.some(ind => + c.industry.toLowerCase().includes(ind.toLowerCase()) + ); + }); + } + + // Фильтр по размеру компании + if (companySizes) { + const sizes = companySizes.split(','); + result = result.filter(c => { + if (!c.companySize) return false; + return sizes.some(size => { + if (size === '1-10') return c.companySize === '1-10'; + if (size === '11-50') return c.companySize === '10-50'; + if (size === '51-250') return c.companySize.includes('50') || c.companySize.includes('100') || c.companySize.includes('250'); + if (size === '251-500') return c.companySize.includes('250') || c.companySize.includes('500'); + if (size === '500+') return c.companySize === '500+'; + return false; + }); + }); + } + + // Фильтр по рейтингу + const rating = parseFloat(minRating); + if (rating > 0) { + result = result.filter(c => c.rating >= rating); + } + + // Фильтр по наличию отзывов + if (hasReviews === 'true') { + result = result.filter(c => c.verified === true); + } + + // Фильтр по акцептам документов + if (hasAccepts === 'true') { + result = result.filter(c => c.verified === true); + } + + res.json({ + companies: result, + total: result.length + }); + } catch (error) { + console.error('Search error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router + diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js new file mode 100644 index 0000000..9e4ea96 --- /dev/null +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -0,0 +1,90 @@ +const mongoose = require('mongoose') +require('dotenv').config() + +// Импорт моделей +const User = require('../models/User') +const Company = require('../models/Company') + +const recreateTestUser = async () => { + try { + const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db' + + console.log('\n🔄 Подключение к MongoDB...') + await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }) + console.log('✅ Подключено к MongoDB\n') + + // Удалить старого тестового пользователя + console.log('🗑️ Удаление старого тестового пользователя...') + const oldUser = await User.findOne({ email: 'admin@test-company.ru' }) + if (oldUser) { + // Удалить связанную компанию + if (oldUser.companyId) { + await Company.findByIdAndDelete(oldUser.companyId) + console.log(' ✓ Старая компания удалена') + } + await User.findByIdAndDelete(oldUser._id) + console.log(' ✓ Старый пользователь удален') + } else { + console.log(' ℹ️ Старый пользователь не найден') + } + + // Создать новую компанию с правильной кодировкой UTF-8 + console.log('\n🏢 Создание тестовой компании...') + const company = await Company.create({ + fullName: 'ООО "Тестовая Компания"', + inn: '1234567890', + ogrn: '1234567890123', + legalForm: 'ООО', + industry: 'IT', + companySize: '50-100', + website: 'https://test-company.ru', + description: 'Тестовая компания для разработки', + address: 'г. Москва, ул. Тестовая, д. 1', + rating: 4.5, + reviewsCount: 10, + dealsCount: 25, + }) + console.log(' ✓ Компания создана:', company.fullName) + + // Создать нового пользователя с правильной кодировкой UTF-8 + console.log('\n👤 Создание тестового пользователя...') + const user = await User.create({ + email: 'admin@test-company.ru', + password: 'SecurePass123!', + firstName: 'Иван', + lastName: 'Иванов', + position: 'Директор', + phone: '+7 (999) 123-45-67', + companyId: company._id, + }) + console.log(' ✓ Пользователь создан:', user.firstName, user.lastName) + + // Проверка что данные сохранены правильно + console.log('\n✅ Проверка данных:') + console.log(' Email:', user.email) + console.log(' Имя:', user.firstName) + console.log(' Фамилия:', user.lastName) + console.log(' Компания:', company.fullName) + console.log(' Должность:', user.position) + + console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8') + console.log('\n📋 Данные для входа:') + console.log(' Email: admin@test-company.ru') + console.log(' Пароль: SecurePass123!') + console.log('') + + await mongoose.connection.close() + process.exit(0) + } catch (error) { + console.error('\n❌ Ошибка:', error.message) + console.error(error) + process.exit(1) + } +} + +// Запуск +recreateTestUser() +