обновил бэк закупок
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"ai": "^4.1.13",
|
"ai": "^4.1.13",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -3721,6 +3722,15 @@
|
|||||||
"node": ">= 10.0.0"
|
"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": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"ai": "^4.1.13",
|
"ai": "^4.1.13",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -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
|
// Подключение к MongoDB через mongoose
|
||||||
router.use((req, res, next) => {
|
require('../../utils/mongoose')
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
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 mongoose = require('mongoose')
|
||||||
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 app = express()
|
||||||
|
|
||||||
// Вспомогательные функции для генерации динамических данных
|
// Задержка для имитации сети (опционально)
|
||||||
const generateTimestamp = () => Date.now();
|
const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms)
|
||||||
const generateDate = (daysAgo) => new Date(Date.now() - 86400000 * daysAgo).toISOString();
|
app.use(delay())
|
||||||
|
|
||||||
// Функция для замены плейсхолдеров в данных
|
// Health check endpoint
|
||||||
const processMockData = (data) => {
|
app.get('/health', (req, res) => {
|
||||||
if (data === undefined || data === null) {
|
const mongodbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected'
|
||||||
return data;
|
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);
|
app.use('/auth', authRoutes)
|
||||||
|
app.use('/companies', companiesRoutes)
|
||||||
if (jsonString === undefined || jsonString === null) {
|
app.use('/messages', messagesRoutes)
|
||||||
return data;
|
app.use('/search', searchRoutes)
|
||||||
}
|
app.use('/buy', buyRoutes)
|
||||||
|
app.use('/experience', experienceRoutes)
|
||||||
|
app.use('/products', productsRoutes)
|
||||||
|
|
||||||
const processedData = jsonString
|
// Обработка ошибок
|
||||||
.replace(/{{timestamp}}/g, timestamp)
|
app.use((err, req, res, next) => {
|
||||||
.replace(/{{date-(\d+)-days?}}/g, (match, days) => generateDate(parseInt(days)))
|
console.error('API Error:', err)
|
||||||
.replace(/{{date-1-day}}/g, generateDate(1))
|
res.status(err.status || 500).json({
|
||||||
.replace(/{{date-2-days}}/g, generateDate(2))
|
error: err.message || 'Internal server error'
|
||||||
.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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auth endpoints
|
// 404 handler
|
||||||
router.post('/auth/login', (req, res) => {
|
app.use((req, res) => {
|
||||||
const { email, password } = req.body;
|
res.status(404).json({
|
||||||
|
error: 'Not found'
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/auth/register', (req, res) => {
|
// Экспортировать для использования в brojs
|
||||||
const { email, password, inn, agreeToTerms } = req.body;
|
module.exports = app
|
||||||
|
|
||||||
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;
|
|
||||||
27
server/routers/procurement/middleware/auth.js
Normal file
27
server/routers/procurement/middleware/auth.js
Normal file
@@ -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 }
|
||||||
64
server/routers/procurement/models/Company.js
Normal file
64
server/routers/procurement/models/Company.js
Normal file
@@ -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)
|
||||||
37
server/routers/procurement/models/Message.js
Normal file
37
server/routers/procurement/models/Message.js
Normal file
@@ -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)
|
||||||
46
server/routers/procurement/models/Product.js
Normal file
46
server/routers/procurement/models/Product.js
Normal file
@@ -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)
|
||||||
67
server/routers/procurement/models/User.js
Normal file
67
server/routers/procurement/models/User.js
Normal file
@@ -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)
|
||||||
103
server/routers/procurement/routes/auth.js
Normal file
103
server/routers/procurement/routes/auth.js
Normal file
@@ -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
|
||||||
186
server/routers/procurement/routes/buy.js
Normal file
186
server/routers/procurement/routes/buy.js
Normal file
@@ -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
|
||||||
103
server/routers/procurement/routes/companies.js
Normal file
103
server/routers/procurement/routes/companies.js
Normal file
@@ -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
|
||||||
114
server/routers/procurement/routes/experience.js
Normal file
114
server/routers/procurement/routes/experience.js
Normal file
@@ -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
|
||||||
|
|
||||||
130
server/routers/procurement/routes/messages.js
Normal file
130
server/routers/procurement/routes/messages.js
Normal file
@@ -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
|
||||||
130
server/routers/procurement/routes/products.js
Normal file
130
server/routers/procurement/routes/products.js
Normal file
@@ -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
|
||||||
|
|
||||||
99
server/routers/procurement/routes/search.js
Normal file
99
server/routers/procurement/routes/search.js
Normal file
@@ -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
|
||||||
|
|
||||||
90
server/routers/procurement/scripts/recreate-test-user.js
Normal file
90
server/routers/procurement/scripts/recreate-test-user.js
Normal file
@@ -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()
|
||||||
|
|
||||||
Reference in New Issue
Block a user