new procurement
This commit is contained in:
29
server/routers/procurement/config/db.js
Normal file
29
server/routers/procurement/config/db.js
Normal file
@@ -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;
|
||||
@@ -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
|
||||
module.exports = app;
|
||||
@@ -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 };
|
||||
|
||||
58
server/routers/procurement/models/BuyProduct.js
Normal file
58
server/routers/procurement/models/BuyProduct.js
Normal file
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
58
server/routers/procurement/models/Review.js
Normal file
58
server/routers/procurement/models/Review.js
Normal file
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
239
server/routers/procurement/routes/__tests__/buyProducts.test.js
Normal file
239
server/routers/procurement/routes/__tests__/buyProducts.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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;
|
||||
|
||||
@@ -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' })
|
||||
|
||||
144
server/routers/procurement/routes/buyProducts.js
Normal file
144
server/routers/procurement/routes/buyProducts.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
48
server/routers/procurement/routes/home.js
Normal file
48
server/routers/procurement/routes/home.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
88
server/routers/procurement/routes/reviews.js
Normal file
88
server/routers/procurement/routes/reviews.js
Normal 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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user