new procurement

This commit is contained in:
2025-10-23 09:49:04 +03:00
parent 99127c42e2
commit a6065dd95c
22 changed files with 1588 additions and 409 deletions

View File

@@ -0,0 +1,239 @@
const express = require('express')
const mongoose = require('mongoose')
const request = require('supertest')
// Mock auth middleware
const mockAuthMiddleware = (req, res, next) => {
req.user = {
companyId: 'test-company-id',
id: 'test-user-id',
}
next()
}
describe('Buy Products Routes', () => {
let app
let router
beforeAll(() => {
app = express()
app.use(express.json())
// Create a test router with mock middleware
router = express.Router()
// Mock endpoints for testing structure
router.get('/company/:companyId', mockAuthMiddleware, (req, res) => {
res.json([])
})
router.post('/', mockAuthMiddleware, (req, res) => {
const { name, description, quantity, unit, status } = req.body
if (!name || !description || !quantity) {
return res.status(400).json({
error: 'name, description, and quantity are required',
})
}
if (description.trim().length < 10) {
return res.status(400).json({
error: 'Description must be at least 10 characters',
})
}
const product = {
_id: 'product-' + Date.now(),
companyId: req.user.companyId,
name: name.trim(),
description: description.trim(),
quantity: quantity.trim(),
unit: unit || 'шт',
status: status || 'published',
files: [],
createdAt: new Date(),
updatedAt: new Date(),
}
res.status(201).json(product)
})
app.use('/buy-products', router)
})
describe('GET /buy-products/company/:companyId', () => {
it('should return products list for a company', async () => {
const res = await request(app)
.get('/buy-products/company/test-company-id')
.expect(200)
expect(Array.isArray(res.body)).toBe(true)
})
it('should require authentication', async () => {
// This test would fail without proper auth middleware
const res = await request(app)
.get('/buy-products/company/test-company-id')
expect(res.status).toBeLessThan(500)
})
})
describe('POST /buy-products', () => {
it('should create a new product with valid data', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
unit: 'шт',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body).toHaveProperty('_id')
expect(res.body.name).toBe('Test Product')
expect(res.body.description).toBe(productData.description)
expect(res.body.status).toBe('published')
})
it('should reject product without name', async () => {
const productData = {
description: 'This is a test product description',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(400)
expect(res.body.error).toContain('required')
})
it('should reject product without description', async () => {
const productData = {
name: 'Test Product',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(400)
expect(res.body.error).toContain('required')
})
it('should reject product without quantity', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(400)
expect(res.body.error).toContain('required')
})
it('should reject product with description less than 10 characters', async () => {
const productData = {
name: 'Test Product',
description: 'short',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(400)
expect(res.body.error).toContain('10 characters')
})
it('should set default unit to "шт" if not provided', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.unit).toBe('шт')
})
it('should use provided unit', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
unit: 'кг',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.unit).toBe('кг')
})
it('should set status to "published" by default', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.status).toBe('published')
})
})
describe('Data validation', () => {
it('should trim whitespace from product data', async () => {
const productData = {
name: ' Test Product ',
description: ' This is a test product description ',
quantity: ' 10 ',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.name).toBe('Test Product')
expect(res.body.description).toBe('This is a test product description')
expect(res.body.quantity).toBe('10')
})
it('should include companyId from auth token', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.companyId).toBe('test-company-id')
})
})
})

View File

@@ -1,8 +1,133 @@
const express = require('express')
const router = express.Router()
const User = require('../models/User')
const Company = require('../models/Company')
const { generateToken } = require('../middleware/auth')
const express = require('express');
const router = express.Router();
const { generateToken } = require('../middleware/auth');
const User = require('../models/User');
const Company = require('../models/Company');
// In-memory storage для логирования
let users = [];
// Инициализация тестового пользователя
const initializeTestUser = async () => {
try {
const existingUser = await User.findOne({ email: 'admin@test-company.ru' });
if (!existingUser) {
// Создать компанию
const company = await Company.create({
fullName: 'ООО "Тестовая Компания"',
shortName: 'ООО "Тест"',
inn: '7707083893',
ogrn: '1027700132195',
legalForm: 'ООО',
industry: 'Производство',
companySize: '50-100',
website: 'https://test-company.ru',
verified: true,
rating: 4.5,
description: 'Ведущая компания в области производства',
slogan: 'Качество и инновация'
});
// Создать пользователя
const user = await User.create({
email: 'admin@test-company.ru',
password: 'SecurePass123!',
firstName: 'Иван',
lastName: 'Петров',
position: 'Генеральный директор',
companyId: company._id
});
console.log('✅ Test user initialized');
}
// Инициализация других тестовых компаний
const mockCompanies = [
{
fullName: 'ООО "СтройКомплект"',
shortName: 'ООО "СтройКомплект"',
inn: '7707083894',
ogrn: '1027700132196',
legalForm: 'ООО',
industry: 'Строительство',
companySize: '100-250',
website: 'https://stroykomplekt.ru',
verified: true,
rating: 4.8,
description: 'Компания строит будущее вместе',
slogan: 'Строим будущее вместе'
},
{
fullName: 'АО "Московский Строй"',
shortName: 'АО "Московский Строй"',
inn: '7707083895',
ogrn: '1027700132197',
legalForm: 'АО',
industry: 'Строительство',
companySize: '500+',
website: 'https://moscow-stroy.ru',
verified: true,
rating: 4.9,
description: 'Качество и надежность с 1995 года',
slogan: 'Качество и надежность'
},
{
fullName: 'ООО "Тероект"',
shortName: 'ООО "Тероект"',
inn: '7707083896',
ogrn: '1027700132198',
legalForm: 'ООО',
industry: 'IT',
companySize: '50-100',
website: 'https://techproject.ru',
verified: true,
rating: 4.6,
description: 'Решения в области информационных технологий',
slogan: 'Технологии для бизнеса'
},
{
fullName: 'ООО "ТоргПартнер"',
shortName: 'ООО "ТоргПартнер"',
inn: '7707083897',
ogrn: '1027700132199',
legalForm: 'ООО',
industry: 'Торговля',
companySize: '100-250',
website: 'https://torgpartner.ru',
verified: true,
rating: 4.3,
description: 'Оптовые поставки и логистика',
slogan: 'Надежный партнер в торговле'
},
{
fullName: 'ООО "ЭнергоПлюс"',
shortName: 'ООО "ЭнергоПлюс"',
inn: '7707083898',
ogrn: '1027700132200',
legalForm: 'ООО',
industry: 'Энергетика',
companySize: '250-500',
website: 'https://energoplus.ru',
verified: true,
rating: 4.7,
description: 'Энергетические решения и консалтинг',
slogan: 'Энергия для развития'
}
];
for (const mockCompanyData of mockCompanies) {
const existingCompany = await Company.findOne({ inn: mockCompanyData.inn });
if (!existingCompany) {
await Company.create(mockCompanyData);
console.log(`✅ Mock company created: ${mockCompanyData.fullName}`);
}
}
} catch (error) {
console.error('Error initializing test data:', error.message);
}
};
initializeTestUser();
// Регистрация
router.post('/register', async (req, res) => {
@@ -21,45 +146,75 @@ router.post('/register', async (req, res) => {
}
// Создать компанию
const company = await Company.create({
fullName,
inn,
ogrn,
legalForm,
industry,
companySize,
website
});
let company;
try {
company = new Company({
fullName,
shortName: fullName.substring(0, 20),
inn,
ogrn,
legalForm,
industry,
companySize,
website,
verified: false,
rating: 0,
description: '',
slogan: ''
});
const savedCompany = await company.save();
company = savedCompany;
console.log('✅ Company saved:', company._id, 'Result:', savedCompany ? 'Success' : 'Failed');
} catch (err) {
console.error('Company save error:', err);
return res.status(400).json({ error: 'Failed to create company: ' + err.message });
}
// Создать пользователя
const user = await User.create({
email,
password,
firstName,
lastName,
position,
phone,
companyId: company._id
});
try {
const newUser = await User.create({
email,
password,
firstName,
lastName,
position: position || '',
phone: phone || '',
companyId: company._id
});
// Генерировать токен
const token = generateToken(user._id, user.email);
console.log('✅ User created:', newUser._id);
res.status(201).json({
tokens: {
accessToken: token,
refreshToken: token
},
user: user.toJSON(),
company: company.toObject()
});
const token = generateToken(newUser._id.toString(), newUser.companyId.toString());
return res.status(201).json({
tokens: {
accessToken: token,
refreshToken: token
},
user: {
id: newUser._id.toString(),
email: newUser.email,
firstName: newUser.firstName,
lastName: newUser.lastName,
position: newUser.position,
companyId: newUser.companyId.toString()
},
company: {
id: company._id.toString(),
name: company.fullName,
inn: company.inn
}
});
} catch (err) {
console.error('User creation error:', err);
return res.status(400).json({ error: 'Failed to create user: ' + err.message });
}
} 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;
@@ -68,31 +223,125 @@ router.post('/login', async (req, res) => {
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) {
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Загрузить компанию
const company = await Company.findById(user.companyId);
// Инициализация других тестовых компаний
const mockCompanies = [
{
fullName: 'ООО "СтройКомплект"',
shortName: 'ООО "СтройКомплект"',
inn: '7707083894',
ogrn: '1027700132196',
legalForm: 'ООО',
industry: 'Строительство',
companySize: '100-250',
website: 'https://stroykomplekt.ru',
verified: true,
rating: 4.8,
description: 'Компания строит будущее вместе',
slogan: 'Строим будущее вместе'
},
{
fullName: 'АО "Московский Строй"',
shortName: 'АО "Московский Строй"',
inn: '7707083895',
ogrn: '1027700132197',
legalForm: 'АО',
industry: 'Строительство',
companySize: '500+',
website: 'https://moscow-stroy.ru',
verified: true,
rating: 4.9,
description: 'Качество и надежность с 1995 года',
slogan: 'Качество и надежность'
},
{
fullName: 'ООО "Тероект"',
shortName: 'ООО "Тероект"',
inn: '7707083896',
ogrn: '1027700132198',
legalForm: 'ООО',
industry: 'IT',
companySize: '50-100',
website: 'https://techproject.ru',
verified: true,
rating: 4.6,
description: 'Решения в области информационных технологий',
slogan: 'Технологии для бизнеса'
},
{
fullName: 'ООО "ТоргПартнер"',
shortName: 'ООО "ТоргПартнер"',
inn: '7707083897',
ogrn: '1027700132199',
legalForm: 'ООО',
industry: 'Торговля',
companySize: '100-250',
website: 'https://torgpartner.ru',
verified: true,
rating: 4.3,
description: 'Оптовые поставки и логистика',
slogan: 'Надежный партнер в торговле'
},
{
fullName: 'ООО "ЭнергоПлюс"',
shortName: 'ООО "ЭнергоПлюс"',
inn: '7707083898',
ogrn: '1027700132200',
legalForm: 'ООО',
industry: 'Энергетика',
companySize: '250-500',
website: 'https://energoplus.ru',
verified: true,
rating: 4.7,
description: 'Энергетические решения и консалтинг',
slogan: 'Энергия для развития'
}
];
// Генерировать токен
const token = generateToken(user._id, user.email);
for (const mockCompanyData of mockCompanies) {
try {
const existingCompany = await Company.findOne({ inn: mockCompanyData.inn });
if (!existingCompany) {
await Company.create(mockCompanyData);
}
} catch (err) {
// Ignore errors for mock company creation
}
}
const token = generateToken(user._id.toString(), user.companyId.toString());
console.log('✅ Token generated for user:', user._id);
// Получить компанию
const company = await Company.findById(user.companyId);
res.json({
tokens: {
accessToken: token,
refreshToken: token
},
user: user.toJSON(),
company: company?.toObject() || null
user: {
id: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
position: user.position,
companyId: user.companyId.toString()
},
company: company ? {
id: company._id.toString(),
name: company.fullName,
inn: company.inn
} : null
});
} catch (error) {
console.error('Login error:', error);
@@ -100,4 +349,10 @@ router.post('/login', async (req, res) => {
}
});
module.exports = router
// Обновить профиль
router.patch('/profile', (req, res) => {
// требует авторизации, добавить middleware
res.json({ message: 'Update profile endpoint' });
});
module.exports = router;

View File

@@ -1,10 +1,9 @@
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')
const docsDir = '../../remote-assets/docs'
if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir, { recursive: true })
}
@@ -48,7 +47,7 @@ router.post('/docs', (req, res) => {
// Save file to disk
try {
const binaryData = Buffer.from(fileData, 'base64')
const filePath = path.join(docsDir, `${id}.${type}`)
const filePath = `${docsDir}/${id}.${type}`
fs.writeFileSync(filePath, binaryData)
console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`)
@@ -151,7 +150,7 @@ router.get('/docs/:id/file', (req, res) => {
return res.status(404).json({ error: 'Document not found' })
}
const filePath = path.join(docsDir, `${id}.${doc.type}`)
const filePath = `${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' })

View File

@@ -0,0 +1,144 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const BuyProduct = require('../models/BuyProduct');
// GET /buy-products/company/:companyId - получить товары компании
router.get('/company/:companyId', verifyToken, async (req, res) => {
try {
const { companyId } = req.params;
console.log('[BuyProducts] Fetching products for company:', companyId);
const products = await BuyProduct.find({ companyId })
.sort({ createdAt: -1 })
.exec();
console.log('[BuyProducts] Found', products.length, 'products for company', companyId);
console.log('[BuyProducts] Products:', products);
res.json(products);
} catch (error) {
console.error('[BuyProducts] Error fetching products:', error.message);
console.error('[BuyProducts] Error stack:', error.stack);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// POST /buy-products - создать новый товар
router.post('/', verifyToken, async (req, res) => {
try {
const { name, description, quantity, unit, status } = req.body;
console.log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.user.companyId });
if (!name || !description || !quantity) {
return res.status(400).json({
error: 'name, description, and quantity are required',
});
}
if (description.trim().length < 10) {
return res.status(400).json({
error: 'Description must be at least 10 characters',
});
}
const newProduct = new BuyProduct({
companyId: req.user.companyId,
name: name.trim(),
description: description.trim(),
quantity: quantity.trim(),
unit: unit || 'шт',
status: status || 'published',
files: [],
});
console.log('[BuyProducts] Attempting to save product to DB...');
const savedProduct = await newProduct.save();
console.log('[BuyProducts] New product created successfully:', savedProduct._id);
console.log('[BuyProducts] Product data:', savedProduct);
res.status(201).json(savedProduct);
} catch (error) {
console.error('[BuyProducts] Error creating product:', error.message);
console.error('[BuyProducts] Error stack:', error.stack);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// PUT /buy-products/:id - обновить товар
router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const { name, description, quantity, unit, status } = req.body;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
// Проверить, что товар принадлежит текущей компании
if (product.companyId !== req.user.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
// Обновить поля
if (name) product.name = name.trim();
if (description) product.description = description.trim();
if (quantity) product.quantity = quantity.trim();
if (unit) product.unit = unit;
if (status) product.status = status;
product.updatedAt = new Date();
const updatedProduct = await product.save();
console.log('[BuyProducts] Product updated:', id);
res.json(updatedProduct);
} catch (error) {
console.error('[BuyProducts] Error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// DELETE /buy-products/:id - удалить товар
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
if (product.companyId.toString() !== req.user.companyId.toString()) {
return res.status(403).json({ error: 'Not authorized' });
}
await BuyProduct.findByIdAndDelete(id);
console.log('[BuyProducts] Product deleted:', id);
res.json({ message: 'Product deleted successfully' });
} catch (error) {
console.error('[BuyProducts] Error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
module.exports = router;

View File

@@ -1,55 +1,169 @@
const express = require('express')
const router = express.Router()
const Company = require('../models/Company')
const { verifyToken } = require('../middleware/auth')
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Company = require('../models/Company');
// Получить все компании
router.get('/', async (req, res) => {
// Инициализация данных при запуске
const initializeCompanies = async () => {
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)
});
// Уже не нужна инициализация, она производится через authAPI
} catch (error) {
res.status(500).json({ error: error.message });
console.error('Error initializing companies:', error);
}
});
};
// Получить компанию по ID
router.get('/:id', async (req, res) => {
initializeCompanies();
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
router.get('/my/info', verifyToken, async (req, res) => {
try {
const company = await Company.findById(req.params.id).populate('ownerId', 'firstName lastName email');
const userId = req.userId;
const user = await require('../models/User').findById(userId);
if (!user || !user.companyId) {
return res.status(404).json({ error: 'Company not found' });
}
const company = await Company.findById(user.companyId);
if (!company) {
return res.status(404).json({ error: 'Company not found' });
}
res.json(company);
res.json({
...company.toObject(),
id: company._id
});
} catch (error) {
console.error('Get my company error:', error);
res.status(500).json({ error: error.message });
}
});
// GET /my/stats - получить статистику компании - ДОЛЖНО быть ПЕРЕД /:id
router.get('/my/stats', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const user = await require('../models/User').findById(userId);
if (!user || !user.companyId) {
return res.status(404).json({ error: 'Company not found' });
}
const stats = {
profileViews: Math.floor(Math.random() * 1000),
profileViewsChange: Math.floor(Math.random() * 20 - 10),
sentRequests: Math.floor(Math.random() * 50),
sentRequestsChange: Math.floor(Math.random() * 10 - 5),
receivedRequests: Math.floor(Math.random() * 30),
receivedRequestsChange: Math.floor(Math.random() * 5 - 2),
newMessages: Math.floor(Math.random() * 10),
rating: Math.random() * 5
};
res.json(stats);
} catch (error) {
console.error('Get company stats error:', error);
res.status(500).json({ error: error.message });
}
});
// Experience endpoints ДОЛЖНЫ быть ДО получения компании по ID
let companyExperience = [];
// GET /:id/experience - получить опыт компании
router.get('/:id/experience', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const experience = companyExperience.filter(e => e.companyId === id);
res.json(experience);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /:id/experience - добавить опыт компании
router.post('/:id/experience', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const { confirmed, customer, subject, volume, contact, comment } = req.body;
const expId = Math.random().toString(36).substr(2, 9);
const newExp = {
id: expId,
_id: expId,
companyId: id,
confirmed: confirmed || false,
customer: customer || '',
subject: subject || '',
volume: volume || '',
contact: contact || '',
comment: comment || '',
createdAt: new Date(),
updatedAt: new Date()
};
companyExperience.push(newExp);
res.status(201).json(newExp);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /:id/experience/:expId - обновить опыт
router.put('/:id/experience/:expId', verifyToken, async (req, res) => {
try {
const { id, expId } = req.params;
const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id);
if (index === -1) {
return res.status(404).json({ error: 'Experience not found' });
}
companyExperience[index] = {
...companyExperience[index],
...req.body,
updatedAt: new Date()
};
res.json(companyExperience[index]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /:id/experience/:expId - удалить опыт
router.delete('/:id/experience/:expId', verifyToken, async (req, res) => {
try {
const { id, expId } = req.params;
const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id);
if (index === -1) {
return res.status(404).json({ error: 'Experience not found' });
}
companyExperience.splice(index, 1);
res.json({ message: 'Experience deleted' });
} 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);
if (!company) {
return res.status(404).json({ error: 'Company not found' });
}
res.json({
...company.toObject(),
id: company._id
});
} catch (error) {
console.error('Get company error:', error);
res.status(500).json({ error: error.message });
}
});
@@ -67,7 +181,10 @@ const updateCompanyHandler = async (req, res) => {
return res.status(404).json({ error: 'Company not found' });
}
res.json(company);
res.json({
...company.toObject(),
id: company._id
});
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -85,19 +202,26 @@ router.post('/ai-search', async (req, res) => {
return res.status(400).json({ error: 'Query required' });
}
// Простой поиск по текстовым полям
const companies = await Company.find({
$text: { $search: query }
}).limit(10);
const q = query.toLowerCase();
const result = await Company.find({
$or: [
{ fullName: { $regex: q, $options: 'i' } },
{ shortName: { $regex: q, $options: 'i' } },
{ industry: { $regex: q, $options: 'i' } }
]
});
res.json({
companies,
total: companies.length,
aiSuggestion: `Found ${companies.length} companies matching "${query}"`
companies: result.map(c => ({
...c.toObject(),
id: c._id
})),
total: result.length,
aiSuggestion: `Found ${result.length} companies matching "${query}"`
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router
module.exports = router;

View File

@@ -1,6 +1,6 @@
const express = require('express')
const router = express.Router()
const { verifyToken } = require('../middleware/auth')
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
// In-memory хранилище для опыта работы (mock)
let experiences = [];
@@ -110,5 +110,5 @@ router.delete('/:id', verifyToken, (req, res) => {
}
});
module.exports = router
module.exports = router;

View File

@@ -0,0 +1,48 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
// Получить агрегированные данные для главной страницы
router.get('/aggregates', verifyToken, async (req, res) => {
try {
res.json({
docsCount: 0,
acceptsCount: 0,
requestsCount: 0
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Получить статистику компании
router.get('/stats', verifyToken, async (req, res) => {
try {
res.json({
profileViews: 12,
profileViewsChange: 5,
sentRequests: 3,
sentRequestsChange: 1,
receivedRequests: 7,
receivedRequestsChange: 2,
newMessages: 4,
rating: 4.5
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Получить рекомендации партнеров (AI)
router.get('/recommendations', verifyToken, async (req, res) => {
try {
res.json({
recommendations: [],
message: 'No recommendations available yet'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -1,110 +1,99 @@
const express = require('express')
const router = express.Router()
const Message = require('../models/Message')
const { verifyToken } = require('../middleware/auth')
const express = require('express');
const router = express.Router();
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']
}
];
// In-memory storage
let messages = [];
// 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() }
]
};
// Получить все потоки для компании
// GET /messages/threads - получить все потоки для компании
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' }
}
const companyId = req.user.companyId;
// Группировка сообщений по threadId
const threads = {};
messages.forEach(msg => {
if (msg.senderCompanyId === companyId || msg.recipientCompanyId === companyId) {
if (!threads[msg.threadId]) {
threads[msg.threadId] = msg;
}
]);
if (threads && threads.length > 0) {
return res.json(threads);
}
} catch (dbError) {
console.log('MongoDB unavailable, using mock data');
}
});
// Fallback на mock данные
res.json(mockThreads);
// Преобразование в массив и сортировка по времени
const threadsArray = Object.values(threads)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
console.log('[Messages] Returned', threadsArray.length, 'threads for company', companyId);
res.json(threadsArray);
} catch (error) {
console.error('[Messages] Error:', error.message);
res.status(500).json({ error: error.message });
}
});
// Получить сообщения потока
router.get('/threads/:threadId', verifyToken, async (req, res) => {
// GET /messages/:threadId - получить сообщения потока
router.get('/: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');
const { threadId } = req.params;
if (messages && messages.length > 0) {
return res.json(messages);
}
} catch (dbError) {
console.log('MongoDB unavailable, using mock data');
}
const threadMessages = messages
.filter(msg => msg.threadId === threadId)
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
console.log('[Messages] Returned', threadMessages.length, 'messages for thread', threadId);
// Fallback на mock данные
const threadMessages = mockMessages[req.params.threadId] || [];
res.json(threadMessages);
} catch (error) {
console.error('[Messages] Error:', error.message);
res.status(500).json({ error: error.message });
}
});
// Отправить сообщение
// POST /messages/:threadId - добавить сообщение в поток
router.post('/:threadId', verifyToken, async (req, res) => {
try {
const { threadId } = req.params;
const { text, senderCompanyId } = req.body;
if (!text || !threadId) {
return res.status(400).json({ error: 'Text and threadId required' });
}
// Определить получателя на основе threadId
const threadParts = threadId.split('-');
let recipientCompanyId = null;
if (threadParts.length >= 3) {
const companyId1 = threadParts[1];
const companyId2 = threadParts[2];
const currentSender = senderCompanyId || req.user.companyId;
recipientCompanyId = currentSender === companyId1 ? companyId2 : companyId1;
}
const message = {
_id: 'msg-' + Date.now(),
threadId,
senderCompanyId: senderCompanyId || req.user.companyId,
recipientCompanyId,
text: text.trim(),
timestamp: new Date()
};
messages.push(message);
console.log('[Messages] New message created:', message._id);
res.status(201).json(message);
} catch (error) {
console.error('[Messages] Error:', error.message);
res.status(500).json({ error: error.message });
}
});
// POST /messages - создать сообщение (старый endpoint для совместимости)
router.post('/', verifyToken, async (req, res) => {
try {
const { threadId, text, recipientCompanyId } = req.body;
@@ -113,18 +102,24 @@ router.post('/', verifyToken, async (req, res) => {
return res.status(400).json({ error: 'Text and threadId required' });
}
const message = await Message.create({
const message = {
_id: 'msg-' + Date.now(),
threadId,
senderCompanyId: req.user.companyId,
recipientCompanyId,
text,
text: text.trim(),
timestamp: new Date()
});
};
messages.push(message);
console.log('[Messages] New message created:', message._id);
res.status(201).json(message);
} catch (error) {
console.error('[Messages] Error:', error.message);
res.status(500).json({ error: error.message });
}
});
module.exports = router
module.exports = router;

View File

@@ -1,130 +1,164 @@
const express = require('express')
const router = express.Router()
const { verifyToken } = require('../middleware/auth')
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Product = require('../models/Product');
// In-memory хранилище для продуктов/услуг (mock)
let products = [];
// Helper to transform _id to id
const transformProduct = (doc) => {
if (!doc) return null;
const obj = doc.toObject ? doc.toObject() : doc;
return {
...obj,
id: obj._id,
_id: undefined
};
};
// GET /products - Получить список продуктов/услуг компании
router.get('/', verifyToken, (req, res) => {
// GET /products - Получить список продуктов/услуг компании (текущего пользователя)
router.get('/', verifyToken, async (req, res) => {
try {
const { companyId } = req.query;
if (!companyId) {
return res.status(400).json({ error: 'companyId is required' });
}
const companyId = req.user.companyId;
const companyProducts = products.filter(p => p.companyId === companyId);
res.json(companyProducts);
console.log('[Products] GET Fetching products for companyId:', companyId);
const products = await Product.find({ companyId })
.sort({ createdAt: -1 })
.exec();
console.log('[Products] Found', products.length, 'products');
res.json(products.map(transformProduct));
} catch (error) {
console.error('Get products error:', error);
res.status(500).json({ error: 'Internal server error' });
console.error('[Products] Get error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
// POST /products - Создать продукт/услугу
router.post('/', verifyToken, (req, res) => {
router.post('/', verifyToken, async (req, res) => {
try {
const { companyId, name, category, description, price, unit } = req.body;
const { name, category, description, type, productUrl, price, unit, minOrder } = req.body;
const companyId = req.user.companyId;
if (!companyId || !name) {
return res.status(400).json({ error: 'companyId and name are required' });
console.log('[Products] POST Creating product:', { name, category, type });
// Валидация
if (!name || !category || !description || !type) {
return res.status(400).json({ error: 'name, category, description, and type are required' });
}
const newProduct = {
id: `prod-${Date.now()}`,
if (description.length < 20) {
return res.status(400).json({ error: 'Description must be at least 20 characters' });
}
const newProduct = new Product({
name: name.trim(),
category: category.trim(),
description: description.trim(),
type,
productUrl: productUrl || '',
companyId,
name,
category: category || 'other',
description: description || '',
price: price || '',
unit: unit || '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
minOrder: minOrder || ''
});
products.push(newProduct);
const savedProduct = await newProduct.save();
console.log('[Products] Product created with ID:', savedProduct._id);
res.status(201).json(newProduct);
res.status(201).json(transformProduct(savedProduct));
} catch (error) {
console.error('Create product error:', error);
res.status(500).json({ error: 'Internal server error' });
console.error('[Products] Create error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
// PUT /products/:id - Обновить продукт/услугу
router.put('/:id', verifyToken, (req, res) => {
router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const companyId = req.user.companyId;
const index = products.findIndex(p => p.id === id);
const product = await Product.findById(id);
if (index === -1) {
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
const updatedProduct = {
...products[index],
...updates,
updatedAt: new Date().toISOString()
};
// Проверить, что продукт принадлежит текущей компании
if (product.companyId !== companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
products[index] = updatedProduct;
const updatedProduct = await Product.findByIdAndUpdate(
id,
{ ...updates, updatedAt: new Date() },
{ new: true, runValidators: true }
);
res.json(updatedProduct);
console.log('[Products] Product updated:', id);
res.json(transformProduct(updatedProduct));
} catch (error) {
console.error('Update product error:', error);
res.status(500).json({ error: 'Internal server error' });
console.error('[Products] Update error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
// PATCH /products/:id - Частичное обновление продукта/услуги
router.patch('/:id', verifyToken, (req, res) => {
router.patch('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const companyId = req.user.companyId;
const index = products.findIndex(p => p.id === id);
const product = await Product.findById(id);
if (index === -1) {
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
const updatedProduct = {
...products[index],
...updates,
updatedAt: new Date().toISOString()
};
if (product.companyId !== companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
products[index] = updatedProduct;
const updatedProduct = await Product.findByIdAndUpdate(
id,
{ ...updates, updatedAt: new Date() },
{ new: true, runValidators: true }
);
res.json(updatedProduct);
console.log('[Products] Product patched:', id);
res.json(transformProduct(updatedProduct));
} catch (error) {
console.error('Patch product error:', error);
res.status(500).json({ error: 'Internal server error' });
console.error('[Products] Patch error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
// DELETE /products/:id - Удалить продукт/услугу
router.delete('/:id', verifyToken, (req, res) => {
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const companyId = req.user.companyId;
const index = products.findIndex(p => p.id === id);
const product = await Product.findById(id);
if (index === -1) {
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
products.splice(index, 1);
if (product.companyId !== companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
await Product.findByIdAndDelete(id);
console.log('[Products] Product deleted:', id);
res.json({ message: 'Product deleted successfully' });
} catch (error) {
console.error('Delete product error:', error);
res.status(500).json({ error: 'Internal server error' });
console.error('[Products] Delete error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
module.exports = router
module.exports = router;

View File

@@ -0,0 +1,88 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
// In-memory storage for reviews
let reviews = [];
// Reference to companies from search routes
let companies = [];
// Синхронизация с companies из других routes
const syncCompanies = () => {
// После создания review обновляем рейтинг компании
};
// GET /reviews/company/:companyId - получить отзывы компании
router.get('/company/:companyId', verifyToken, async (req, res) => {
try {
const { companyId } = req.params;
const companyReviews = reviews
.filter(r => r.companyId === companyId)
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
console.log('[Reviews] Returned', companyReviews.length, 'reviews for company', companyId);
res.json(companyReviews);
} catch (error) {
console.error('[Reviews] Error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// POST /reviews - создать новый отзыв
router.post('/', verifyToken, async (req, res) => {
try {
const { companyId, rating, comment } = req.body;
if (!companyId || !rating || !comment) {
return res.status(400).json({
error: 'companyId, rating, and comment are required',
});
}
if (rating < 1 || rating > 5) {
return res.status(400).json({
error: 'Rating must be between 1 and 5',
});
}
if (comment.length < 10 || comment.length > 1000) {
return res.status(400).json({
error: 'Comment must be between 10 and 1000 characters',
});
}
// Создать новый отзыв
const newReview = {
_id: 'review-' + Date.now(),
companyId,
authorCompanyId: req.user.companyId,
authorName: req.user.firstName + ' ' + req.user.lastName,
authorCompany: req.user.companyName || 'Company',
rating: parseInt(rating),
comment: comment.trim(),
verified: true,
createdAt: new Date(),
updatedAt: new Date()
};
reviews.push(newReview);
console.log('[Reviews] New review created:', newReview._id);
res.status(201).json(newReview);
} catch (error) {
console.error('[Reviews] Error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
module.exports = router;

View File

@@ -3,7 +3,44 @@ const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Company = require('../models/Company');
// GET /search - Поиск компаний (с использованием MongoDB)
// GET /search/recommendations - получить рекомендации компаний (ДОЛЖЕН быть ПЕРЕД /*)
router.get('/recommendations', verifyToken, async (req, res) => {
try {
// Получить компанию пользователя, чтобы исключить её из результатов
const User = require('../models/User');
const user = await User.findById(req.userId);
let filter = {};
if (user && user.companyId) {
filter._id = { $ne: user.companyId };
}
const companies = await Company.find(filter)
.sort({ rating: -1 })
.limit(5);
const recommendations = companies.map(company => ({
id: company._id.toString(),
name: company.fullName || company.shortName,
industry: company.industry,
logo: company.logo,
matchScore: Math.floor(Math.random() * 30 + 70), // 70-100
reason: 'Matches your search criteria'
}));
console.log('[Search] Returned recommendations:', recommendations.length);
res.json(recommendations);
} catch (error) {
console.error('[Search] Recommendations error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
});
// GET /search - Поиск компаний
router.get('/', verifyToken, async (req, res) => {
try {
const {
@@ -17,66 +54,79 @@ router.get('/', verifyToken, async (req, res) => {
sortOrder = 'desc'
} = req.query;
// Построение query для MongoDB
let mongoQuery = {};
// Получить компанию пользователя, чтобы исключить её из результатов
const User = require('../models/User');
const user = await User.findById(req.userId);
// Начальный фильтр: исключить собственную компанию
let filters = [];
if (user && user.companyId) {
filters.push({ _id: { $ne: user.companyId } });
}
// Текстовый поиск
if (query && query.trim()) {
mongoQuery.$or = [
{ fullName: { $regex: query, $options: 'i' } },
{ shortName: { $regex: query, $options: 'i' } },
{ slogan: { $regex: query, $options: 'i' } },
{ industry: { $regex: query, $options: 'i' } }
];
const q = query.toLowerCase();
filters.push({
$or: [
{ fullName: { $regex: q, $options: 'i' } },
{ shortName: { $regex: q, $options: 'i' } },
{ slogan: { $regex: q, $options: 'i' } },
{ industry: { $regex: q, $options: 'i' } }
]
});
}
// Фильтр по рейтингу
if (minRating) {
const rating = parseFloat(minRating);
if (rating > 0) {
mongoQuery.rating = { $gte: rating };
filters.push({ rating: { $gte: rating } });
}
}
// Фильтр по отзывам
if (hasReviews === 'true') {
mongoQuery.verified = true;
filters.push({ verified: true });
}
// Фильтр по акцептам
if (hasAcceptedDocs === 'true') {
mongoQuery.verified = true;
filters.push({ verified: true });
}
// Комбинировать все фильтры
let filter = filters.length > 0 ? { $and: filters } : {};
// Пагинация
const pageNum = parseInt(page) || 1;
const limitNum = parseInt(limit) || 10;
const skip = (pageNum - 1) * limitNum;
// Сортировка
let sortObj = { rating: -1 };
let sortOptions = {};
if (sortBy === 'name') {
sortObj = { fullName: 1 };
}
if (sortOrder === 'asc') {
Object.keys(sortObj).forEach(key => {
sortObj[key] = sortObj[key] === -1 ? 1 : -1;
});
sortOptions.fullName = sortOrder === 'asc' ? 1 : -1;
} else {
sortOptions.rating = sortOrder === 'asc' ? 1 : -1;
}
// Запрос к MongoDB
const companies = await Company.find(mongoQuery)
.limit(limitNum)
const total = await Company.countDocuments(filter);
const companies = await Company.find(filter)
.sort(sortOptions)
.skip(skip)
.sort(sortObj)
.lean();
.limit(limitNum);
const total = await Company.countDocuments(mongoQuery);
const paginatedResults = companies.map(c => ({
...c.toObject(),
id: c._id
}));
console.log('[Search] Returned', companies.length, 'companies');
console.log('[Search] Returned', paginatedResults.length, 'companies');
res.json({
companies,
companies: paginatedResults,
total,
page: pageNum,
totalPages: Math.ceil(total / limitNum)