diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js new file mode 100644 index 0000000..3d03054 --- /dev/null +++ b/server/routers/procurement/config/db.js @@ -0,0 +1,29 @@ +const mongoose = require('mongoose'); + +const connectDB = async () => { + try { + const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; + + console.log('\n📡 Попытка подключения к MongoDB...'); + console.log(` URI: ${mongoUri}`); + + await mongoose.connect(mongoUri, { + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + + console.log('✅ MongoDB подключена успешно!'); + console.log(` Хост: ${mongoose.connection.host}`); + console.log(` БД: ${mongoose.connection.name}\n`); + + return true; + } catch (error) { + console.error('\n❌ Ошибка подключения к MongoDB:'); + console.error(` ${error.message}\n`); + console.warn('⚠️ Приложение продолжит работу с mock данными\n'); + + return false; + } +}; + +module.exports = connectDB; diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 5765845..289b809 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -1,64 +1,94 @@ -const express = require('express') -const dotenv = require('dotenv') +const express = require('express'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const connectDB = require('./config/db'); // Загрузить переменные окружения -dotenv.config() - -// Подключение к MongoDB через mongoose -require('../../utils/mongoose') +dotenv.config(); // Импортировать маршруты -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 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 reviewsRoutes = require('./routes/reviews'); +const buyProductsRoutes = require('./routes/buyProducts'); +const homeRoutes = require('./routes/home'); -const mongoose = require('mongoose') +const app = express(); -const app = express() +// Подключить MongoDB при инициализации +let dbConnected = false; +connectDB().then(() => { + dbConnected = true; +}); + +// Middleware +app.use(cors()); +app.use(express.json({ charset: 'utf-8' })); +app.use(express.urlencoded({ extended: true, charset: 'utf-8' })); + +// Set UTF-8 encoding for all responses +app.use((req, res, next) => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + next(); +}); + +// CORS headers +app.use((req, res, next) => { + 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'); + if (req.method === 'OPTIONS') { + res.sendStatus(200); + } else { + next(); + } +}); // Задержка для имитации сети (опционально) -const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms) -app.use(delay()) +const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms); +app.use(delay()); // Health check endpoint app.get('/health', (req, res) => { - const mongodbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected' res.json({ status: 'ok', api: 'running', - database: mongodbStatus, - mongoUri: process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db', + database: dbConnected ? 'mongodb' : 'mock', timestamp: new Date().toISOString() - }) -}) + }); +}); // Маршруты -app.use('/auth', authRoutes) -app.use('/companies', companiesRoutes) -app.use('/messages', messagesRoutes) -app.use('/search', searchRoutes) -app.use('/buy', buyRoutes) -app.use('/experience', experienceRoutes) -app.use('/products', productsRoutes) +app.use('/auth', authRoutes); +app.use('/companies', companiesRoutes); +app.use('/messages', messagesRoutes); +app.use('/search', searchRoutes); +app.use('/buy', buyRoutes); +app.use('/buy-products', buyProductsRoutes); +app.use('/experience', experienceRoutes); +app.use('/products', productsRoutes); +app.use('/reviews', reviewsRoutes); +app.use('/home', homeRoutes); // Обработка ошибок app.use((err, req, res, next) => { - console.error('API Error:', err) + console.error('API Error:', err); res.status(err.status || 500).json({ error: err.message || 'Internal server error' - }) -}) + }); +}); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Not found' - }) -}) + }); +}); // Экспортировать для использования в brojs -module.exports = app \ No newline at end of file +module.exports = app; \ No newline at end of file diff --git a/server/routers/procurement/middleware/auth.js b/server/routers/procurement/middleware/auth.js index b126134..ba36cd6 100644 --- a/server/routers/procurement/middleware/auth.js +++ b/server/routers/procurement/middleware/auth.js @@ -1,27 +1,32 @@ -const jwt = require('jsonwebtoken') +const jwt = require('jsonwebtoken'); const verifyToken = (req, res, next) => { - const token = req.headers.authorization?.replace('Bearer ', '') + const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { - return res.status(401).json({ error: 'No token provided' }) + 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() + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key'); + req.userId = decoded.userId; + req.companyId = decoded.companyId; + req.user = decoded; + console.log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId); + next(); } catch (error) { - return res.status(401).json({ error: 'Invalid token' }) + console.error('[Auth] Token verification failed:', error.message); + return res.status(401).json({ error: 'Invalid token' }); } -} +}; -const generateToken = (userId, email) => { +const generateToken = (userId, companyId) => { + console.log('[Auth] Generating token for userId:', userId, 'companyId:', companyId); return jwt.sign( - { userId, email }, + { userId, companyId }, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '7d' } - ) -} + ); +}; -module.exports = { verifyToken, generateToken } +module.exports = { verifyToken, generateToken }; diff --git a/server/routers/procurement/models/BuyProduct.js b/server/routers/procurement/models/BuyProduct.js new file mode 100644 index 0000000..346623d --- /dev/null +++ b/server/routers/procurement/models/BuyProduct.js @@ -0,0 +1,58 @@ +const mongoose = require('mongoose'); + +const buyProductSchema = new mongoose.Schema({ + companyId: { + type: String, + required: true, + index: true + }, + name: { + type: String, + required: true + }, + description: { + type: String, + required: true, + minlength: 10, + maxlength: 1000 + }, + quantity: { + type: String, + required: true + }, + unit: { + type: String, + default: 'шт' + }, + files: [{ + id: String, + name: String, + url: String, + type: String, + size: Number, + uploadedAt: { + type: Date, + default: Date.now + } + }], + status: { + type: String, + enum: ['draft', 'published'], + default: 'published' + }, + createdAt: { + type: Date, + default: Date.now, + index: true + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +// Индексы для оптимизации поиска +buyProductSchema.index({ companyId: 1, createdAt: -1 }); +buyProductSchema.index({ name: 'text', description: 'text' }); + +module.exports = mongoose.model('BuyProduct', buyProductSchema); diff --git a/server/routers/procurement/models/Company.js b/server/routers/procurement/models/Company.js index ca1a87d..dc34466 100644 --- a/server/routers/procurement/models/Company.js +++ b/server/routers/procurement/models/Company.js @@ -1,4 +1,4 @@ -const mongoose = require('mongoose') +const mongoose = require('mongoose'); const companySchema = new mongoose.Schema({ fullName: { @@ -8,7 +8,6 @@ const companySchema = new mongoose.Schema({ shortName: String, inn: { type: String, - unique: true, sparse: true }, ogrn: String, @@ -46,6 +45,10 @@ const companySchema = new mongoose.Schema({ productsNeeded: String, partnerIndustries: [String], partnerGeography: [String], + verified: { + type: Boolean, + default: false + }, createdAt: { type: Date, default: Date.now @@ -54,11 +57,14 @@ const companySchema = new mongoose.Schema({ type: Date, default: Date.now } -}) +}, { + collection: 'companies', + minimize: false +}); // Индексы для поиска -companySchema.index({ fullName: 'text', shortName: 'text', description: 'text' }) -companySchema.index({ industry: 1 }) -companySchema.index({ rating: -1 }) +companySchema.index({ fullName: 'text', shortName: 'text', description: 'text' }); +companySchema.index({ industry: 1 }); +companySchema.index({ rating: -1 }); -module.exports = mongoose.model('Company', companySchema) +module.exports = mongoose.model('Company', companySchema); diff --git a/server/routers/procurement/models/Message.js b/server/routers/procurement/models/Message.js index 3e29204..e8afd5a 100644 --- a/server/routers/procurement/models/Message.js +++ b/server/routers/procurement/models/Message.js @@ -1,4 +1,4 @@ -const mongoose = require('mongoose') +const mongoose = require('mongoose'); const messageSchema = new mongoose.Schema({ threadId: { @@ -29,9 +29,9 @@ const messageSchema = new mongoose.Schema({ default: Date.now, index: true } -}) +}); // Индекс для быстрого поиска сообщений потока -messageSchema.index({ threadId: 1, timestamp: -1 }) +messageSchema.index({ threadId: 1, timestamp: -1 }); -module.exports = mongoose.model('Message', messageSchema) +module.exports = mongoose.model('Message', messageSchema); diff --git a/server/routers/procurement/models/Product.js b/server/routers/procurement/models/Product.js index 4926923..2f194f7 100644 --- a/server/routers/procurement/models/Product.js +++ b/server/routers/procurement/models/Product.js @@ -1,4 +1,4 @@ -const mongoose = require('mongoose') +const mongoose = require('mongoose'); const productSchema = new mongoose.Schema({ name: { @@ -22,25 +22,36 @@ const productSchema = new mongoose.Schema({ }, productUrl: String, companyId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Company', - required: true + type: String, + required: true, + index: true }, price: String, unit: String, minOrder: String, createdAt: { type: Date, - default: Date.now + default: Date.now, + index: true }, updatedAt: { type: Date, default: Date.now } -}) +}); // Индекс для поиска -productSchema.index({ companyId: 1, type: 1 }) -productSchema.index({ name: 'text', description: 'text' }) +productSchema.index({ companyId: 1, type: 1 }); +productSchema.index({ name: 'text', description: 'text' }); -module.exports = mongoose.model('Product', productSchema) +// Transform _id to id in JSON output +productSchema.set('toJSON', { + transform: (doc, ret) => { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + return ret; + } +}); + +module.exports = mongoose.model('Product', productSchema); diff --git a/server/routers/procurement/models/Review.js b/server/routers/procurement/models/Review.js new file mode 100644 index 0000000..327c027 --- /dev/null +++ b/server/routers/procurement/models/Review.js @@ -0,0 +1,58 @@ +const mongoose = require('mongoose'); + +const reviewSchema = new mongoose.Schema({ + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true, + index: true + }, + authorCompanyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true + }, + authorName: { + type: String, + required: true + }, + authorCompany: { + type: String, + required: true + }, + rating: { + type: Number, + required: true, + min: 1, + max: 5 + }, + comment: { + type: String, + required: true, + minlength: 10, + maxlength: 1000 + }, + date: { + type: Date, + default: Date.now + }, + verified: { + type: Boolean, + default: true + }, + createdAt: { + type: Date, + default: Date.now, + index: true + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +// Индексы для оптимизации поиска +reviewSchema.index({ companyId: 1, createdAt: -1 }); +reviewSchema.index({ authorCompanyId: 1 }); + +module.exports = mongoose.model('Review', reviewSchema); diff --git a/server/routers/procurement/models/User.js b/server/routers/procurement/models/User.js index 7a604d9..0a2ea56 100644 --- a/server/routers/procurement/models/User.js +++ b/server/routers/procurement/models/User.js @@ -1,5 +1,5 @@ -const mongoose = require('mongoose') -const bcrypt = require('bcryptjs') +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); const userSchema = new mongoose.Schema({ email: { @@ -37,31 +37,37 @@ const userSchema = new mongoose.Schema({ type: Date, default: Date.now } -}) +}, { + collection: 'users', + minimize: false, + toObject: { versionKey: false } +}); + +userSchema.set('toObject', { virtuals: false, versionKey: false }); // Хешировать пароль перед сохранением userSchema.pre('save', async function(next) { - if (!this.isModified('password')) return next() + if (!this.isModified('password')) return next(); try { - const salt = await bcrypt.genSalt(10) - this.password = await bcrypt.hash(this.password, salt) - next() + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); + next(); } catch (error) { - next(error) + next(error); } -}) +}); // Метод для сравнения паролей userSchema.methods.comparePassword = async function(candidatePassword) { - return await bcrypt.compare(candidatePassword, this.password) -} + return await bcrypt.compare(candidatePassword, this.password); +}; // Скрыть пароль при преобразовании в JSON userSchema.methods.toJSON = function() { - const obj = this.toObject() - delete obj.password - return obj -} + const obj = this.toObject(); + delete obj.password; + return obj; +}; -module.exports = mongoose.model('User', userSchema) +module.exports = mongoose.model('User', userSchema); diff --git a/server/routers/procurement/routes/__tests__/buyProducts.test.js b/server/routers/procurement/routes/__tests__/buyProducts.test.js new file mode 100644 index 0000000..b254a38 --- /dev/null +++ b/server/routers/procurement/routes/__tests__/buyProducts.test.js @@ -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') + }) + }) +}) diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index cd7bdc7..1ac6b62 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -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: 'ООО "ТеxПроект"', + shortName: 'ООО "ТеxПроект"', + 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: 'ООО "ТеxПроект"', + shortName: 'ООО "ТеxПроект"', + 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; diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index 8155341..8218ace 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -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' }) diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js new file mode 100644 index 0000000..175b104 --- /dev/null +++ b/server/routers/procurement/routes/buyProducts.js @@ -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; diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js index 380fecf..0c70e21 100644 --- a/server/routers/procurement/routes/companies.js +++ b/server/routers/procurement/routes/companies.js @@ -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; diff --git a/server/routers/procurement/routes/experience.js b/server/routers/procurement/routes/experience.js index 5ca6d47..fa224d4 100644 --- a/server/routers/procurement/routes/experience.js +++ b/server/routers/procurement/routes/experience.js @@ -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; diff --git a/server/routers/procurement/routes/home.js b/server/routers/procurement/routes/home.js new file mode 100644 index 0000000..eabc4f4 --- /dev/null +++ b/server/routers/procurement/routes/home.js @@ -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; diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js index 6b52eca..9de6796 100644 --- a/server/routers/procurement/routes/messages.js +++ b/server/routers/procurement/routes/messages.js @@ -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; diff --git a/server/routers/procurement/routes/products.js b/server/routers/procurement/routes/products.js index 03f7a29..858d575 100644 --- a/server/routers/procurement/routes/products.js +++ b/server/routers/procurement/routes/products.js @@ -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; diff --git a/server/routers/procurement/routes/reviews.js b/server/routers/procurement/routes/reviews.js new file mode 100644 index 0000000..05462db --- /dev/null +++ b/server/routers/procurement/routes/reviews.js @@ -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; diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index d64002b..d00eed3 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -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) diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 9e4ea96..09cd954 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -1,38 +1,38 @@ -const mongoose = require('mongoose') -require('dotenv').config() +const mongoose = require('mongoose'); +require('dotenv').config(); // Импорт моделей -const User = require('../models/User') -const Company = require('../models/Company') +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' + const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; - console.log('\n🔄 Подключение к MongoDB...') + console.log('\n🔄 Подключение к MongoDB...'); await mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - }) - console.log('✅ Подключено к MongoDB\n') + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('✅ Подключено к MongoDB\n'); // Удалить старого тестового пользователя - console.log('🗑️ Удаление старого тестового пользователя...') - const oldUser = await User.findOne({ email: 'admin@test-company.ru' }) + 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 Company.findByIdAndDelete(oldUser.companyId); + console.log(' ✓ Старая компания удалена'); } - await User.findByIdAndDelete(oldUser._id) - console.log(' ✓ Старый пользователь удален') + await User.findByIdAndDelete(oldUser._id); + console.log(' ✓ Старый пользователь удален'); } else { - console.log(' ℹ️ Старый пользователь не найден') + console.log(' ℹ️ Старый пользователь не найден'); } // Создать новую компанию с правильной кодировкой UTF-8 - console.log('\n🏢 Создание тестовой компании...') + console.log('\n🏢 Создание тестовой компании...'); const company = await Company.create({ fullName: 'ООО "Тестовая Компания"', inn: '1234567890', @@ -46,11 +46,11 @@ const recreateTestUser = async () => { rating: 4.5, reviewsCount: 10, dealsCount: 25, - }) - console.log(' ✓ Компания создана:', company.fullName) + }); + console.log(' ✓ Компания создана:', company.fullName); // Создать нового пользователя с правильной кодировкой UTF-8 - console.log('\n👤 Создание тестового пользователя...') + console.log('\n👤 Создание тестового пользователя...'); const user = await User.create({ email: 'admin@test-company.ru', password: 'SecurePass123!', @@ -59,32 +59,32 @@ const recreateTestUser = async () => { position: 'Директор', phone: '+7 (999) 123-45-67', companyId: company._id, - }) - console.log(' ✓ Пользователь создан:', user.firstName, user.lastName) + }); + 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✅ Проверка данных:'); + 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('') + 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) + await mongoose.connection.close(); + process.exit(0); } catch (error) { - console.error('\n❌ Ошибка:', error.message) - console.error(error) - process.exit(1) + console.error('\n❌ Ошибка:', error.message); + console.error(error); + process.exit(1); } -} +}; // Запуск -recreateTestUser() +recreateTestUser(); diff --git a/server/utils/const.ts b/server/utils/const.ts index 5d19080..3ab73e4 100644 --- a/server/utils/const.ts +++ b/server/utils/const.ts @@ -1,4 +1,4 @@ import 'dotenv/config'; // Connection URL -export const mongoUrl = process.env.MONGO_ADDR +export const mongoUrl = process.env.MONGO_ADDR || 'mongodb://localhost:27017'