diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js index 3d03054..31e7ae4 100644 --- a/server/routers/procurement/config/db.js +++ b/server/routers/procurement/config/db.js @@ -7,22 +7,24 @@ const connectDB = async () => { console.log('\nπŸ“‘ ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ MongoDB...'); console.log(` URI: ${mongoUri}`); - await mongoose.connect(mongoUri, { + const connection = await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, serverSelectionTimeoutMS: 5000, connectTimeoutMS: 5000, }); console.log('βœ… MongoDB ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½Π° ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ!'); - console.log(` Π₯ост: ${mongoose.connection.host}`); - console.log(` Π‘Π”: ${mongoose.connection.name}\n`); + console.log(` Π₯ост: ${connection.connection.host}`); + console.log(` Π‘Π”: ${connection.connection.name}\n`); - return true; + return connection; } catch (error) { console.error('\n❌ Ошибка ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ MongoDB:'); console.error(` ${error.message}\n`); console.warn('⚠️ ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ Ρ€Π°Π±ΠΎΡ‚Ρƒ с mock Π΄Π°Π½Π½Ρ‹ΠΌΠΈ\n'); - return false; + return null; } }; diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 289b809..e08a238 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -6,6 +6,14 @@ const connectDB = require('./config/db'); // Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния dotenv.config(); +// Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΈ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅: установитС DEV=true Π² .env ΠΈΠ»ΠΈ ΠΏΡ€ΠΈ запускС +// export DEV=true && npm start (для Linux/Mac) +// set DEV=true && npm start (для Windows) +// По ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ Π»ΠΎΠ³ΠΈ ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½Ρ‹. ВсС console.log Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ΡΡ Ссли DEV !== 'true' +if (process.env.DEV === 'true') { + console.log('ℹ️ DEBUG MODE ENABLED - All logs are visible'); +} + // Π˜ΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹ const authRoutes = require('./routes/auth'); const companiesRoutes = require('./routes/companies'); @@ -16,6 +24,7 @@ const experienceRoutes = require('./routes/experience'); const productsRoutes = require('./routes/products'); const reviewsRoutes = require('./routes/reviews'); const buyProductsRoutes = require('./routes/buyProducts'); +const requestsRoutes = require('./routes/requests'); const homeRoutes = require('./routes/home'); const app = express(); @@ -73,6 +82,7 @@ app.use('/buy-products', buyProductsRoutes); app.use('/experience', experienceRoutes); app.use('/products', productsRoutes); app.use('/reviews', reviewsRoutes); +app.use('/requests', requestsRoutes); app.use('/home', homeRoutes); // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок diff --git a/server/routers/procurement/middleware/auth.js b/server/routers/procurement/middleware/auth.js index ba36cd6..da6b4c1 100644 --- a/server/routers/procurement/middleware/auth.js +++ b/server/routers/procurement/middleware/auth.js @@ -1,5 +1,15 @@ const jwt = require('jsonwebtoken'); +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + const verifyToken = (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', ''); @@ -12,7 +22,7 @@ const verifyToken = (req, res, next) => { req.userId = decoded.userId; req.companyId = decoded.companyId; req.user = decoded; - console.log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId); + log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId); next(); } catch (error) { console.error('[Auth] Token verification failed:', error.message); @@ -20,10 +30,10 @@ const verifyToken = (req, res, next) => { } }; -const generateToken = (userId, companyId) => { - console.log('[Auth] Generating token for userId:', userId, 'companyId:', companyId); +const generateToken = (userId, companyId, firstName = '', lastName = '', companyName = '') => { + log('[Auth] Generating token for userId:', userId, 'companyId:', companyId); return jwt.sign( - { userId, companyId }, + { userId, companyId, firstName, lastName, companyName }, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '7d' } ); diff --git a/server/routers/procurement/models/BuyProduct.js b/server/routers/procurement/models/BuyProduct.js index 346623d..5396ebd 100644 --- a/server/routers/procurement/models/BuyProduct.js +++ b/server/routers/procurement/models/BuyProduct.js @@ -35,6 +35,16 @@ const buyProductSchema = new mongoose.Schema({ default: Date.now } }], + acceptedBy: [{ + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company' + }, + acceptedAt: { + type: Date, + default: Date.now + } + }], status: { type: String, enum: ['draft', 'published'], diff --git a/server/routers/procurement/models/Request.js b/server/routers/procurement/models/Request.js new file mode 100644 index 0000000..6a14fab --- /dev/null +++ b/server/routers/procurement/models/Request.js @@ -0,0 +1,62 @@ +const mongoose = require('mongoose'); + +const requestSchema = new mongoose.Schema({ + senderCompanyId: { + type: String, + required: true, + index: true + }, + recipientCompanyId: { + type: String, + required: true, + index: true + }, + text: { + type: String, + required: true + }, + files: [{ + id: String, + name: String, + url: String, + type: String, + size: Number, + uploadedAt: { + type: Date, + default: Date.now + } + }], + productId: { + type: String, + ref: 'BuyProduct' + }, + status: { + type: String, + enum: ['pending', 'accepted', 'rejected'], + default: 'pending' + }, + response: { + type: String, + default: null + }, + respondedAt: { + type: Date, + default: null + }, + createdAt: { + type: Date, + default: Date.now, + index: true + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +// Π˜Π½Π΄Π΅ΠΊΡΡ‹ для ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ поиска +requestSchema.index({ senderCompanyId: 1, createdAt: -1 }); +requestSchema.index({ recipientCompanyId: 1, createdAt: -1 }); +requestSchema.index({ senderCompanyId: 1, recipientCompanyId: 1 }); + +module.exports = mongoose.model('Request', requestSchema); diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index 1ac6b62..9f55a1f 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -4,6 +4,17 @@ const { generateToken } = require('../middleware/auth'); const User = require('../models/User'); const Company = require('../models/Company'); +// Ѐункция для логирования с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ DEV ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + // In-memory storage для логирования let users = []; @@ -21,6 +32,7 @@ const initializeTestUser = async () => { legalForm: 'ООО', industry: 'ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΡΡ‚Π²ΠΎ', companySize: '50-100', + partnerGeography: ['moscow', 'russia_all'], website: 'https://test-company.ru', verified: true, rating: 4.5, @@ -38,7 +50,7 @@ const initializeTestUser = async () => { companyId: company._id }); - console.log('βœ… Test user initialized'); + log('βœ… Test user initialized'); } // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ Π΄Ρ€ΡƒΠ³ΠΈΡ… тСстовых ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΉ @@ -50,7 +62,8 @@ const initializeTestUser = async () => { ogrn: '1027700132196', legalForm: 'ООО', industry: 'Π‘Ρ‚Ρ€ΠΎΠΈΡ‚Π΅Π»ΡŒΡΡ‚Π²ΠΎ', - companySize: '100-250', + companySize: '51-250', + partnerGeography: ['moscow', 'russia_all'], website: 'https://stroykomplekt.ru', verified: true, rating: 4.8, @@ -65,6 +78,7 @@ const initializeTestUser = async () => { legalForm: 'АО', industry: 'Π‘Ρ‚Ρ€ΠΎΠΈΡ‚Π΅Π»ΡŒΡΡ‚Π²ΠΎ', companySize: '500+', + partnerGeography: ['moscow', 'russia_all'], website: 'https://moscow-stroy.ru', verified: true, rating: 4.9, @@ -78,7 +92,8 @@ const initializeTestUser = async () => { ogrn: '1027700132198', legalForm: 'ООО', industry: 'IT', - companySize: '50-100', + companySize: '11-50', + partnerGeography: ['moscow', 'russia_all'], website: 'https://techproject.ru', verified: true, rating: 4.6, @@ -91,8 +106,9 @@ const initializeTestUser = async () => { inn: '7707083897', ogrn: '1027700132199', legalForm: 'ООО', - industry: 'Ворговля', - companySize: '100-250', + industry: 'ΠžΠΏΡ‚ΠΎΠ²Π°Ρ торговля', + companySize: '51-250', + partnerGeography: ['moscow', 'russia_all'], website: 'https://torgpartner.ru', verified: true, rating: 4.3, @@ -106,7 +122,8 @@ const initializeTestUser = async () => { ogrn: '1027700132200', legalForm: 'ООО', industry: 'Π­Π½Π΅Ρ€Π³Π΅Ρ‚ΠΈΠΊΠ°', - companySize: '250-500', + companySize: '251-500', + partnerGeography: ['moscow', 'russia_all'], website: 'https://energoplus.ru', verified: true, rating: 4.7, @@ -119,7 +136,7 @@ const initializeTestUser = async () => { const existingCompany = await Company.findOne({ inn: mockCompanyData.inn }); if (!existingCompany) { await Company.create(mockCompanyData); - console.log(`βœ… Mock company created: ${mockCompanyData.fullName}`); + log(`βœ… Mock company created: ${mockCompanyData.fullName}`); } } } catch (error) { @@ -160,11 +177,12 @@ router.post('/register', async (req, res) => { verified: false, rating: 0, description: '', - slogan: '' + slogan: '', + partnerGeography: ['moscow', 'russia_all'] }); const savedCompany = await company.save(); company = savedCompany; - console.log('βœ… Company saved:', company._id, 'Result:', savedCompany ? 'Success' : 'Failed'); + 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 }); @@ -182,9 +200,9 @@ router.post('/register', async (req, res) => { companyId: company._id }); - console.log('βœ… User created:', newUser._id); + log('βœ… User created:', newUser._id); - const token = generateToken(newUser._id.toString(), newUser.companyId.toString()); + const token = generateToken(newUser._id.toString(), newUser.companyId.toString(), newUser.firstName, newUser.lastName, company.fullName); return res.status(201).json({ tokens: { accessToken: token, @@ -242,7 +260,8 @@ router.post('/login', async (req, res) => { ogrn: '1027700132196', legalForm: 'ООО', industry: 'Π‘Ρ‚Ρ€ΠΎΠΈΡ‚Π΅Π»ΡŒΡΡ‚Π²ΠΎ', - companySize: '100-250', + companySize: '51-250', + partnerGeography: ['moscow', 'russia_all'], website: 'https://stroykomplekt.ru', verified: true, rating: 4.8, @@ -257,6 +276,7 @@ router.post('/login', async (req, res) => { legalForm: 'АО', industry: 'Π‘Ρ‚Ρ€ΠΎΠΈΡ‚Π΅Π»ΡŒΡΡ‚Π²ΠΎ', companySize: '500+', + partnerGeography: ['moscow', 'russia_all'], website: 'https://moscow-stroy.ru', verified: true, rating: 4.9, @@ -270,7 +290,8 @@ router.post('/login', async (req, res) => { ogrn: '1027700132198', legalForm: 'ООО', industry: 'IT', - companySize: '50-100', + companySize: '11-50', + partnerGeography: ['moscow', 'russia_all'], website: 'https://techproject.ru', verified: true, rating: 4.6, @@ -283,8 +304,9 @@ router.post('/login', async (req, res) => { inn: '7707083897', ogrn: '1027700132199', legalForm: 'ООО', - industry: 'Ворговля', - companySize: '100-250', + industry: 'ΠžΠΏΡ‚ΠΎΠ²Π°Ρ торговля', + companySize: '51-250', + partnerGeography: ['moscow', 'russia_all'], website: 'https://torgpartner.ru', verified: true, rating: 4.3, @@ -298,7 +320,8 @@ router.post('/login', async (req, res) => { ogrn: '1027700132200', legalForm: 'ООО', industry: 'Π­Π½Π΅Ρ€Π³Π΅Ρ‚ΠΈΠΊΠ°', - companySize: '250-500', + companySize: '251-500', + partnerGeography: ['moscow', 'russia_all'], website: 'https://energoplus.ru', verified: true, rating: 4.7, @@ -318,12 +341,17 @@ router.post('/login', async (req, res) => { } } - const token = generateToken(user._id.toString(), user.companyId.toString()); - console.log('βœ… Token generated for user:', user._id); - - // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ компанию - const company = await Company.findById(user.companyId); + // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ компанию Π΄ΠΎ использования Π² generateToken + let companyData = null; + try { + companyData = await Company.findById(user.companyId); + } catch (err) { + console.error('Failed to fetch company:', err.message); + } + const token = generateToken(user._id.toString(), user.companyId.toString(), user.firstName, user.lastName, companyData?.fullName || 'Company'); + log('βœ… Token generated for user:', user._id); + res.json({ tokens: { accessToken: token, @@ -337,10 +365,10 @@ router.post('/login', async (req, res) => { position: user.position, companyId: user.companyId.toString() }, - company: company ? { - id: company._id.toString(), - name: company.fullName, - inn: company.inn + company: companyData ? { + id: companyData._id.toString(), + name: companyData.fullName, + inn: companyData.inn } : null }); } catch (error) { diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index 8218ace..53dadb5 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -1,9 +1,10 @@ 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 = '../../remote-assets/docs' +const docsDir = path.join(__dirname, '../../remote-assets/docs') if (!fs.existsSync(docsDir)) { fs.mkdirSync(docsDir, { recursive: true }) } @@ -47,7 +48,7 @@ router.post('/docs', (req, res) => { // Save file to disk try { const binaryData = Buffer.from(fileData, 'base64') - const filePath = `${docsDir}/${id}.${type}` + const filePath = path.join(docsDir, `${id}.${type}`) fs.writeFileSync(filePath, binaryData) console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`) @@ -150,7 +151,7 @@ router.get('/docs/:id/file', (req, res) => { return res.status(404).json({ error: 'Document not found' }) } - const filePath = `${docsDir}/${id}.${doc.type}` + const filePath = path.join(docsDir, `${id}.${doc.type}`) if (!fs.existsSync(filePath)) { console.log('[BUY API] File not found on disk:', filePath) return res.status(404).json({ error: 'File not found on disk' }) diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js index 175b104..2aabc93 100644 --- a/server/routers/procurement/routes/buyProducts.js +++ b/server/routers/procurement/routes/buyProducts.js @@ -3,18 +3,29 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const BuyProduct = require('../models/BuyProduct'); +// Ѐункция для логирования с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ DEV ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + // 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); + 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); + log('[BuyProducts] Found', products.length, 'products for company', companyId); + log('[BuyProducts] Products:', products); res.json(products); } catch (error) { @@ -32,7 +43,7 @@ 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 }); + log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.user.companyId }); if (!name || !description || !quantity) { return res.status(400).json({ @@ -56,11 +67,11 @@ router.post('/', verifyToken, async (req, res) => { files: [], }); - console.log('[BuyProducts] Attempting to save product to DB...'); + 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); + log('[BuyProducts] New product created successfully:', savedProduct._id); + log('[BuyProducts] Product data:', savedProduct); res.status(201).json(savedProduct); } catch (error) { @@ -100,7 +111,7 @@ router.put('/:id', verifyToken, async (req, res) => { const updatedProduct = await product.save(); - console.log('[BuyProducts] Product updated:', id); + log('[BuyProducts] Product updated:', id); res.json(updatedProduct); } catch (error) { @@ -129,7 +140,7 @@ router.delete('/:id', verifyToken, async (req, res) => { await BuyProduct.findByIdAndDelete(id); - console.log('[BuyProducts] Product deleted:', id); + log('[BuyProducts] Product deleted:', id); res.json({ message: 'Product deleted successfully' }); } catch (error) { @@ -141,4 +152,143 @@ router.delete('/:id', verifyToken, async (req, res) => { } }); +// POST /buy-products/:id/files - Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Ρ„Π°ΠΉΠ» ΠΊ Ρ‚ΠΎΠ²Π°Ρ€Ρƒ +router.post('/:id/files', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const { fileName, fileUrl, fileType, fileSize } = req.body; + + 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' }); + } + + const file = { + id: 'file-' + Date.now(), + name: fileName, + url: fileUrl, + type: fileType, + size: fileSize, + uploadedAt: new Date() + }; + + product.files.push(file); + await product.save(); + + log('[BuyProducts] File added to product:', id); + + res.json(product); + } catch (error) { + console.error('[BuyProducts] Error adding file:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// DELETE /buy-products/:id/files/:fileId - ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ Ρ„Π°ΠΉΠ» +router.delete('/:id/files/:fileId', verifyToken, async (req, res) => { + try { + const { id, fileId } = 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' }); + } + + product.files = product.files.filter(f => f.id !== fileId); + await product.save(); + + log('[BuyProducts] File deleted from product:', id); + + res.json(product); + } catch (error) { + console.error('[BuyProducts] Error deleting file:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// POST /buy-products/:id/accept - Π°ΠΊΡ†Π΅ΠΏΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ‚ΠΎΠ²Π°Ρ€ +router.post('/:id/accept', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const companyId = req.user.companyId; + + const product = await BuyProduct.findById(id); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + // НС ΠΌΠΎΠΆΠ΅ΠΌ Π°ΠΊΡ†Π΅ΠΏΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ собствСнный Ρ‚ΠΎΠ²Π°Ρ€ + if (product.companyId.toString() === companyId.toString()) { + return res.status(403).json({ error: 'Cannot accept own product' }); + } + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ, Π½Π΅ Π°ΠΊΡ†Π΅ΠΏΡ‚ΠΈΡ€ΠΎΠ²Π°Π» Π»ΠΈ ΡƒΠΆΠ΅ + const alreadyAccepted = product.acceptedBy.some( + a => a.companyId.toString() === companyId.toString() + ); + + if (alreadyAccepted) { + return res.status(400).json({ error: 'Already accepted' }); + } + + product.acceptedBy.push({ + companyId, + acceptedAt: new Date() + }); + + await product.save(); + + log('[BuyProducts] Product accepted by company:', companyId); + + res.json(product); + } catch (error) { + console.error('[BuyProducts] Error accepting product:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// GET /buy-products/:id/acceptances - ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π°ΠΊΡ†Π΅ΠΏΡ‚ΠΎΠ²Π°Π»ΠΈ +router.get('/:id/acceptances', verifyToken, async (req, res) => { + try { + const { id } = req.params; + + const product = await BuyProduct.findById(id).populate('acceptedBy.companyId', 'shortName fullName'); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + log('[BuyProducts] Returned acceptances for product:', id); + + res.json(product.acceptedBy); + } catch (error) { + console.error('[BuyProducts] Error fetching acceptances:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + module.exports = router; diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js index 9de6796..b60a055 100644 --- a/server/routers/procurement/routes/messages.js +++ b/server/routers/procurement/routes/messages.js @@ -1,35 +1,88 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); +const Message = require('../models/Message'); -// In-memory storage -let messages = []; +// Ѐункция для логирования с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ DEV ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; // GET /messages/threads - ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ всС ΠΏΠΎΡ‚ΠΎΠΊΠΈ для ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ router.get('/threads', verifyToken, async (req, res) => { try { const companyId = req.user.companyId; + const { ObjectId } = require('mongoose').Types; - // Π“Ρ€ΡƒΠΏΠΏΠΈΡ€ΠΎΠ²ΠΊΠ° сообщСний ΠΏΠΎ threadId - const threads = {}; + log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId); + + // ΠŸΡ€Π΅ΠΎΠ±Ρ€Π°Π·ΠΎΠ²Π°Ρ‚ΡŒ Π² ObjectId Ссли это строка + let companyObjectId = companyId; + let companyIdString = companyId.toString ? companyId.toString() : companyId; - messages.forEach(msg => { - if (msg.senderCompanyId === companyId || msg.recipientCompanyId === companyId) { - if (!threads[msg.threadId]) { - threads[msg.threadId] = msg; - } + try { + if (typeof companyId === 'string' && ObjectId.isValid(companyId)) { + companyObjectId = new ObjectId(companyId); + } + } catch (e) { + log('[Messages] Could not convert to ObjectId:', e.message); + } + + log('[Messages] Using companyObjectId:', companyObjectId, 'companyIdString:', companyIdString); + + // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ всС сообщСния Π³Π΄Π΅ тСкущая компания ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚Π΅Π»ΡŒ ΠΈΠ»ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚Π΅Π»ΡŒ + // ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌ ΠΎΠ±Π° Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π° - ObjectId ΠΈ строки + const allMessages = await Message.find({ + $or: [ + { senderCompanyId: companyObjectId }, + { senderCompanyId: companyIdString }, + { recipientCompanyId: companyObjectId }, + { recipientCompanyId: companyIdString }, + // Π’Π°ΠΊΠΆΠ΅ ΠΈΡ‰Π΅ΠΌ ΠΏΠΎ threadId ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΌΠΎΠΆΠ΅Ρ‚ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ ID ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ + { threadId: { $regex: companyIdString } } + ] + }) + .sort({ timestamp: -1 }) + .limit(500); + + log('[Messages] Found', allMessages.length, 'messages for company'); + + if (allMessages.length === 0) { + log('[Messages] No messages found'); + res.json([]); + return; + } + + // Π“Ρ€ΡƒΠΏΠΏΠΈΡ€ΡƒΠ΅ΠΌ ΠΏΠΎ ΠΏΠΎΡ‚ΠΎΠΊΠ°ΠΌ ΠΈ Π±Π΅Ρ€Π΅ΠΌ послСднСС сообщСниС ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΏΠΎΡ‚ΠΎΠΊΠ° + const threadsMap = new Map(); + allMessages.forEach(msg => { + const threadId = msg.threadId; + if (!threadsMap.has(threadId)) { + threadsMap.set(threadId, { + threadId, + lastMessage: msg.text, + lastMessageAt: msg.timestamp, + senderCompanyId: msg.senderCompanyId, + recipientCompanyId: msg.recipientCompanyId + }); } }); - // ΠŸΡ€Π΅ΠΎΠ±Ρ€Π°Π·ΠΎΠ²Π°Π½ΠΈΠ΅ Π² массив ΠΈ сортировка ΠΏΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ - const threadsArray = Object.values(threads) - .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + const threads = Array.from(threadsMap.values()).sort((a, b) => + new Date(b.lastMessageAt) - new Date(a.lastMessageAt) + ); - console.log('[Messages] Returned', threadsArray.length, 'threads for company', companyId); + log('[Messages] Returned', threads.length, 'unique threads'); - res.json(threadsArray); + res.json(threads); } catch (error) { - console.error('[Messages] Error:', error.message); + console.error('[Messages] Error fetching threads:', error.message, error.stack); res.status(500).json({ error: error.message }); } }); @@ -38,16 +91,24 @@ router.get('/threads', verifyToken, async (req, res) => { router.get('/:threadId', verifyToken, async (req, res) => { try { const { threadId } = req.params; + const companyId = req.user.companyId; - const threadMessages = messages - .filter(msg => msg.threadId === threadId) - .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); + // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ всС сообщСния ΠΏΠΎΡ‚ΠΎΠΊΠ° + const threadMessages = await Message.find({ threadId }) + .sort({ timestamp: 1 }) + .exec(); - console.log('[Messages] Returned', threadMessages.length, 'messages for thread', threadId); + // ΠžΡ‚ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ сообщСния ΠΊΠ°ΠΊ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½Π½Ρ‹Π΅ для Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΉ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ + await Message.updateMany( + { threadId, recipientCompanyId: companyId, read: false }, + { read: true } + ); + + log('[Messages] Returned', threadMessages.length, 'messages for thread', threadId); res.json(threadMessages); } catch (error) { - console.error('[Messages] Error:', error.message); + console.error('[Messages] Error fetching messages:', error.message); res.status(500).json({ error: error.message }); } }); @@ -63,61 +124,139 @@ router.post('/:threadId', verifyToken, async (req, res) => { } // ΠžΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ получатСля Π½Π° основС threadId - const threadParts = threadId.split('-'); + // threadId Ρ„ΠΎΡ€ΠΌΠ°Ρ‚: "thread-id1-id2" + const threadParts = threadId.replace('thread-', '').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 currentSender = senderCompanyId || req.user.companyId; + const currentSenderString = currentSender.toString ? currentSender.toString() : currentSender; + + if (threadParts.length >= 2) { + const companyId1 = threadParts[0]; + const companyId2 = threadParts[1]; + // ΠŸΠΎΠ»ΡƒΡ‡Π°Ρ‚Π΅Π»ΡŒ - это другая сторона + recipientCompanyId = currentSenderString === companyId1 ? companyId2 : companyId1; } - const message = { - _id: 'msg-' + Date.now(), + log('[Messages] POST /messages/:threadId'); + log('[Messages] threadId:', threadId); + log('[Messages] Sender:', currentSender); + log('[Messages] SenderString:', currentSenderString); + log('[Messages] Recipient:', recipientCompanyId); + + // Найти recipientCompanyId ΠΏΠΎ ObjectId Ссли Π½ΡƒΠΆΠ½ΠΎ + let recipientObjectId = recipientCompanyId; + const { ObjectId } = require('mongoose').Types; + try { + if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) { + recipientObjectId = new ObjectId(recipientCompanyId); + } + } catch (e) { + log('[Messages] Could not convert recipientId to ObjectId'); + } + + const message = new Message({ threadId, - senderCompanyId: senderCompanyId || req.user.companyId, - recipientCompanyId, + senderCompanyId: currentSender, + recipientCompanyId: recipientObjectId, text: text.trim(), + read: false, timestamp: new Date() - }; + }); - messages.push(message); + const savedMessage = await message.save(); - console.log('[Messages] New message created:', message._id); + log('[Messages] New message created:', savedMessage._id); + log('[Messages] Message data:', { + threadId: savedMessage.threadId, + senderCompanyId: savedMessage.senderCompanyId, + recipientCompanyId: savedMessage.recipientCompanyId + }); - res.status(201).json(message); + res.status(201).json(savedMessage); } catch (error) { - console.error('[Messages] Error:', error.message); + console.error('[Messages] Error creating message:', error.message, error.stack); res.status(500).json({ error: error.message }); } }); -// POST /messages - ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ сообщСниС (старый endpoint для совмСстимости) -router.post('/', verifyToken, async (req, res) => { +// MIGRATION ENDPOINT - Fix recipientCompanyId for all messages +router.post('/admin/migrate-fix-recipients', async (req, res) => { try { - const { threadId, text, recipientCompanyId } = req.body; + const allMessages = await Message.find().exec(); + log('[Messages] Migrating', allMessages.length, 'messages...'); - if (!text || !threadId) { - return res.status(400).json({ error: 'Text and threadId required' }); + let fixedCount = 0; + let errorCount = 0; + + for (const message of allMessages) { + try { + const threadId = message.threadId; + if (!threadId) continue; + + // Parse threadId Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ "thread-id1-id2" ΠΈΠ»ΠΈ "id1-id2" + const ids = threadId.replace('thread-', '').split('-'); + if (ids.length < 2) { + errorCount++; + continue; + } + + const companyId1 = ids[0]; + const companyId2 = ids[1]; + + // Compare with senderCompanyId + const senderIdString = message.senderCompanyId.toString ? message.senderCompanyId.toString() : message.senderCompanyId; + const expectedRecipient = senderIdString === companyId1 ? companyId2 : companyId1; + + // If recipientCompanyId is not set or wrong - fix it + if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) { + const { ObjectId } = require('mongoose').Types; + let recipientObjectId = expectedRecipient; + try { + if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) { + recipientObjectId = new ObjectId(expectedRecipient); + } + } catch (e) { + // continue + } + + await Message.updateOne( + { _id: message._id }, + { recipientCompanyId: recipientObjectId } + ); + + fixedCount++; + } + } catch (err) { + console.error('[Messages] Migration error:', err.message); + errorCount++; + } } - const message = { - _id: 'msg-' + Date.now(), - threadId, - 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); + log('[Messages] Migration completed! Fixed:', fixedCount, 'Errors:', errorCount); + res.json({ success: true, fixed: fixedCount, errors: errorCount, total: allMessages.length }); + } catch (error) { + console.error('[Messages] Migration error:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// DEBUG ENDPOINT +router.get('/debug/all-messages', async (req, res) => { + try { + const allMessages = await Message.find().limit(10).exec(); + log('[Debug] Total messages in DB:', allMessages.length); + + const info = allMessages.map(m => ({ + _id: m._id, + threadId: m.threadId, + senderCompanyId: m.senderCompanyId?.toString ? m.senderCompanyId.toString() : m.senderCompanyId, + recipientCompanyId: m.recipientCompanyId?.toString ? m.recipientCompanyId.toString() : m.recipientCompanyId, + text: m.text.substring(0, 30) + })); + + res.json({ totalCount: allMessages.length, messages: info }); } catch (error) { - console.error('[Messages] Error:', error.message); res.status(500).json({ error: error.message }); } }); diff --git a/server/routers/procurement/routes/products.js b/server/routers/procurement/routes/products.js index 858d575..44490d4 100644 --- a/server/routers/procurement/routes/products.js +++ b/server/routers/procurement/routes/products.js @@ -3,6 +3,17 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Product = require('../models/Product'); +// Ѐункция для логирования с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ DEV ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + // Helper to transform _id to id const transformProduct = (doc) => { if (!doc) return null; @@ -19,13 +30,13 @@ router.get('/', verifyToken, async (req, res) => { try { const companyId = req.user.companyId; - console.log('[Products] GET Fetching products for companyId:', companyId); + 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'); + log('[Products] Found', products.length, 'products'); res.json(products.map(transformProduct)); } catch (error) { console.error('[Products] Get error:', error.message); @@ -35,20 +46,20 @@ router.get('/', verifyToken, async (req, res) => { // POST /products - Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚/услугу router.post('/', verifyToken, async (req, res) => { - try { + // try { const { name, category, description, type, productUrl, price, unit, minOrder } = req.body; const companyId = req.user.companyId; - console.log('[Products] POST Creating product:', { name, category, type }); + 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' }); - } + // // Валидация + // if (!name || !category || !description || !type) { + // return res.status(400).json({ error: 'name, category, description, and type are required' }); + // } - if (description.length < 20) { - return res.status(400).json({ error: 'Description must be at least 20 characters' }); - } + // if (description.length < 20) { + // return res.status(400).json({ error: 'Description must be at least 20 characters' }); + // } const newProduct = new Product({ name: name.trim(), @@ -63,13 +74,13 @@ router.post('/', verifyToken, async (req, res) => { }); const savedProduct = await newProduct.save(); - console.log('[Products] Product created with ID:', savedProduct._id); + log('[Products] Product created with ID:', savedProduct._id); res.status(201).json(transformProduct(savedProduct)); - } catch (error) { - console.error('[Products] Create error:', error.message); - res.status(500).json({ error: 'Internal server error', message: error.message }); - } + // } catch (error) { + // console.error('[Products] Create error:', error.message); + // res.status(500).json({ error: 'Internal server error', message: error.message }); + // } }); // PUT /products/:id - ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚/услугу @@ -96,7 +107,7 @@ router.put('/:id', verifyToken, async (req, res) => { { new: true, runValidators: true } ); - console.log('[Products] Product updated:', id); + log('[Products] Product updated:', id); res.json(transformProduct(updatedProduct)); } catch (error) { console.error('[Products] Update error:', error.message); @@ -127,7 +138,7 @@ router.patch('/:id', verifyToken, async (req, res) => { { new: true, runValidators: true } ); - console.log('[Products] Product patched:', id); + log('[Products] Product patched:', id); res.json(transformProduct(updatedProduct)); } catch (error) { console.error('[Products] Patch error:', error.message); @@ -153,7 +164,7 @@ router.delete('/:id', verifyToken, async (req, res) => { await Product.findByIdAndDelete(id); - console.log('[Products] Product deleted:', id); + log('[Products] Product deleted:', id); res.json({ message: 'Product deleted successfully' }); } catch (error) { console.error('[Products] Delete error:', error.message); diff --git a/server/routers/procurement/routes/requests.js b/server/routers/procurement/routes/requests.js new file mode 100644 index 0000000..a7ab999 --- /dev/null +++ b/server/routers/procurement/routes/requests.js @@ -0,0 +1,171 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); +const Request = require('../models/Request'); + +// Ѐункция для логирования с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ DEV ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + +// GET /requests/sent - ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½Π½Ρ‹Π΅ запросы +router.get('/sent', verifyToken, async (req, res) => { + try { + const companyId = req.user.companyId; + + const requests = await Request.find({ senderCompanyId: companyId }) + .sort({ createdAt: -1 }) + .exec(); + + log('[Requests] Returned', requests.length, 'sent requests for company', companyId); + + res.json(requests); + } catch (error) { + console.error('[Requests] Error fetching sent requests:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// GET /requests/received - ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹Π΅ запросы +router.get('/received', verifyToken, async (req, res) => { + try { + const companyId = req.user.companyId; + + const requests = await Request.find({ recipientCompanyId: companyId }) + .sort({ createdAt: -1 }) + .exec(); + + log('[Requests] Returned', requests.length, 'received requests for company', companyId); + + res.json(requests); + } catch (error) { + console.error('[Requests] Error fetching received requests:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// POST /requests - ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ запрос +router.post('/', verifyToken, async (req, res) => { + try { + const { text, recipientCompanyIds, productId, files } = req.body; + const senderCompanyId = req.user.companyId; + + if (!text || !recipientCompanyIds || !Array.isArray(recipientCompanyIds) || recipientCompanyIds.length === 0) { + return res.status(400).json({ error: 'text and recipientCompanyIds array required' }); + } + + // ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ запрос ΠΊΠ°ΠΆΠ΄ΠΎΠΉ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ + const results = []; + for (const recipientCompanyId of recipientCompanyIds) { + try { + const request = new Request({ + senderCompanyId, + recipientCompanyId, + text, + productId, + files: files || [], + status: 'pending' + }); + + await request.save(); + results.push({ + companyId: recipientCompanyId, + success: true, + message: 'Request sent successfully' + }); + + log('[Requests] Request sent to company:', recipientCompanyId); + } catch (err) { + results.push({ + companyId: recipientCompanyId, + success: false, + message: err.message + }); + } + } + + // Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ ΠΎΡ‚Ρ‡Π΅Ρ‚ + const report = { + text, + result: results, + createdAt: new Date() + }; + + res.status(201).json({ + id: 'bulk-' + Date.now(), + ...report, + files: files || [] + }); + } catch (error) { + console.error('[Requests] Error creating request:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// PUT /requests/:id - ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° запрос +router.put('/:id', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const { response, status } = req.body; + + const request = await Request.findById(id); + + if (!request) { + return res.status(404).json({ error: 'Request not found' }); + } + + // Волько ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚Π΅Π»ΡŒ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° запрос + if (request.recipientCompanyId !== req.user.companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } + + request.response = response; + request.status = status || 'accepted'; + request.respondedAt = new Date(); + request.updatedAt = new Date(); + + await request.save(); + + log('[Requests] Request responded:', id); + + res.json(request); + } catch (error) { + console.error('[Requests] Error responding to request:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /requests/:id - ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ запрос +router.delete('/:id', verifyToken, async (req, res) => { + try { + const { id } = req.params; + + const request = await Request.findById(id); + + if (!request) { + return res.status(404).json({ error: 'Request not found' }); + } + + // ΠœΠΎΠΆΠ΅Ρ‚ ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚Π΅Π»ΡŒ ΠΈΠ»ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚Π΅Π»ΡŒ + if (request.senderCompanyId !== req.user.companyId && request.recipientCompanyId !== req.user.companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } + + await Request.findByIdAndDelete(id); + + log('[Requests] Request deleted:', id); + + res.json({ message: 'Request deleted successfully' }); + } catch (error) { + console.error('[Requests] Error deleting request:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/server/routers/procurement/routes/reviews.js b/server/routers/procurement/routes/reviews.js index 05462db..40bcd7d 100644 --- a/server/routers/procurement/routes/reviews.js +++ b/server/routers/procurement/routes/reviews.js @@ -1,16 +1,17 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); +const Review = require('../models/Review'); -// In-memory storage for reviews -let reviews = []; - -// Reference to companies from search routes -let companies = []; - -// Бинхронизация с companies ΠΈΠ· Π΄Ρ€ΡƒΠ³ΠΈΡ… routes -const syncCompanies = () => { - // ПослС создания review обновляСм Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ +// Ѐункция для логирования с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ DEV ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } }; // GET /reviews/company/:companyId - ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΎΡ‚Π·Ρ‹Π²Ρ‹ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ @@ -18,15 +19,15 @@ 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)); + const companyReviews = await Review.find({ companyId }) + .sort({ createdAt: -1 }) + .exec(); - console.log('[Reviews] Returned', companyReviews.length, 'reviews for company', companyId); + log('[Reviews] Returned', companyReviews.length, 'reviews for company', companyId); res.json(companyReviews); } catch (error) { - console.error('[Reviews] Error:', error.message); + console.error('[Reviews] Error fetching reviews:', error.message); res.status(500).json({ error: 'Internal server error', message: error.message, @@ -51,15 +52,14 @@ router.post('/', verifyToken, async (req, res) => { }); } - if (comment.length < 10 || comment.length > 1000) { + if (comment.trim().length < 10 || comment.trim().length > 1000) { return res.status(400).json({ error: 'Comment must be between 10 and 1000 characters', }); } // Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²Ρ‹ΠΉ ΠΎΡ‚Π·Ρ‹Π² - const newReview = { - _id: 'review-' + Date.now(), + const newReview = new Review({ companyId, authorCompanyId: req.user.companyId, authorName: req.user.firstName + ' ' + req.user.lastName, @@ -69,15 +69,15 @@ router.post('/', verifyToken, async (req, res) => { verified: true, createdAt: new Date(), updatedAt: new Date() - }; + }); - reviews.push(newReview); + const savedReview = await newReview.save(); - console.log('[Reviews] New review created:', newReview._id); + log('[Reviews] New review created:', savedReview._id); - res.status(201).json(newReview); + res.status(201).json(savedReview); } catch (error) { - console.error('[Reviews] Error:', error.message); + console.error('[Reviews] Error creating review:', error.message); res.status(500).json({ error: 'Internal server error', message: error.message, diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index d00eed3..84e38e6 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -3,6 +3,17 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Company = require('../models/Company'); +// Ѐункция для логирования с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ DEV ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + // GET /search/recommendations - ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΉ (Π”ΠžΠ›Π–Π•Π Π±Ρ‹Ρ‚ΡŒ ΠŸΠ•Π Π•Π” /*) router.get('/recommendations', verifyToken, async (req, res) => { try { @@ -28,7 +39,7 @@ router.get('/recommendations', verifyToken, async (req, res) => { reason: 'Matches your search criteria' })); - console.log('[Search] Returned recommendations:', recommendations.length); + log('[Search] Returned recommendations:', recommendations.length); res.json(recommendations); } catch (error) { @@ -47,6 +58,9 @@ router.get('/', verifyToken, async (req, res) => { query = '', page = 1, limit = 10, + industries, + companySize, + geography, minRating = 0, hasReviews, hasAcceptedDocs, @@ -58,6 +72,29 @@ router.get('/', verifyToken, async (req, res) => { const User = require('../models/User'); const user = await User.findById(req.userId); + log('[Search] Request params:', { query, industries, companySize, geography, minRating, hasReviews, hasAcceptedDocs, sortBy, sortOrder }); + + // Маппинг ΠΊΠΎΠ΄ΠΎΠ² Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ΠΎΠ² Π½Π° значСния Π² Π‘Π” + const industryMap = { + 'it': 'IT', + 'finance': 'Ѐинансы', + 'manufacturing': 'ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΡΡ‚Π²ΠΎ', + 'construction': 'Π‘Ρ‚Ρ€ΠΎΠΈΡ‚Π΅Π»ΡŒΡΡ‚Π²ΠΎ', + 'retail': 'Розничная торговля', + 'wholesale': 'ΠžΠΏΡ‚ΠΎΠ²Π°Ρ торговля', + 'logistics': 'Логистика', + 'healthcare': 'Π—Π΄Ρ€Π°Π²ΠΎΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅', + 'education': 'ΠžΠ±Ρ€Π°Π·ΠΎΠ²Π°Π½ΠΈΠ΅', + 'consulting': 'ΠšΠΎΠ½ΡΠ°Π»Ρ‚ΠΈΠ½Π³', + 'marketing': 'ΠœΠ°Ρ€ΠΊΠ΅Ρ‚ΠΈΠ½Π³', + 'realestate': 'ΠΠ΅Π΄Π²ΠΈΠΆΠΈΠΌΠΎΡΡ‚ΡŒ', + 'food': 'ΠŸΠΈΡ‰Π΅Π²Π°Ρ ΠΏΡ€ΠΎΠΌΡ‹ΡˆΠ»Π΅Π½Π½ΠΎΡΡ‚ΡŒ', + 'agriculture': 'БСльскоС хозяйство', + 'energy': 'Π­Π½Π΅Ρ€Π³Π΅Ρ‚ΠΈΠΊΠ°', + 'telecom': 'Π’Π΅Π»Π΅ΠΊΠΎΠΌΠΌΡƒΠ½ΠΈΠΊΠ°Ρ†ΠΈΠΈ', + 'media': 'МСдиа' + }; + // ΠΠ°Ρ‡Π°Π»ΡŒΠ½Ρ‹ΠΉ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€: ΠΈΡΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ ΡΠΎΠ±ΡΡ‚Π²Π΅Π½Π½ΡƒΡŽ компанию let filters = []; @@ -78,6 +115,43 @@ router.get('/', verifyToken, async (req, res) => { }); } + // Π€ΠΈΠ»ΡŒΡ‚Ρ€ ΠΏΠΎ отраслям - ΠΏΡ€Π΅ΠΎΠ±Ρ€Π°Π·ΡƒΠ΅ΠΌ ΠΊΠΎΠ΄Ρ‹ Π² значСния Π‘Π” + if (industries) { + const industryList = Array.isArray(industries) ? industries : [industries]; + if (industryList.length > 0) { + const dbIndustries = industryList + .map(code => industryMap[code]) + .filter(val => val !== undefined); + + log('[Search] Raw industries param:', industries); + log('[Search] Industry codes:', industryList, 'Mapped to:', dbIndustries); + + if (dbIndustries.length > 0) { + filters.push({ industry: { $in: dbIndustries } }); + log('[Search] Added industry filter:', { industry: { $in: dbIndustries } }); + } else { + log('[Search] No industries mapped! Codes were:', industryList); + } + } + } + + // Π€ΠΈΠ»ΡŒΡ‚Ρ€ ΠΏΠΎ Ρ€Π°Π·ΠΌΠ΅Ρ€Ρƒ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ + if (companySize) { + const sizeList = Array.isArray(companySize) ? companySize : [companySize]; + if (sizeList.length > 0) { + filters.push({ companySize: { $in: sizeList } }); + } + } + + // Π€ΠΈΠ»ΡŒΡ‚Ρ€ ΠΏΠΎ Π³Π΅ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΈ + if (geography) { + const geoList = Array.isArray(geography) ? geography : [geography]; + if (geoList.length > 0) { + filters.push({ partnerGeography: { $in: geoList } }); + log('[Search] Geography filter:', { partnerGeography: { $in: geoList } }); + } + } + // Π€ΠΈΠ»ΡŒΡ‚Ρ€ ΠΏΠΎ Ρ€Π΅ΠΉΡ‚ΠΈΠ½Π³Ρƒ if (minRating) { const rating = parseFloat(minRating); @@ -112,6 +186,12 @@ router.get('/', verifyToken, async (req, res) => { sortOptions.rating = sortOrder === 'asc' ? 1 : -1; } + log('[Search] Final MongoDB filter:', JSON.stringify(filter, null, 2)); + + let filterDebug = filters.length > 0 ? { $and: filters } : {}; + const allCompanies = await Company.find({}); + log('[Search] All companies in DB:', allCompanies.map(c => ({ name: c.fullName, geography: c.partnerGeography, industry: c.industry }))); + const total = await Company.countDocuments(filter); const companies = await Company.find(filter) .sort(sortOptions) @@ -123,13 +203,19 @@ router.get('/', verifyToken, async (req, res) => { id: c._id })); - console.log('[Search] Returned', paginatedResults.length, 'companies'); + log('[Search] Query:', query, 'Industries:', industries, 'Size:', companySize, 'Geo:', geography); + log('[Search] Total found:', total, 'Returning:', paginatedResults.length, 'companies'); + log('[Search] Company details:', paginatedResults.map(c => ({ name: c.fullName, industry: c.industry }))); res.json({ companies: paginatedResults, total, page: pageNum, - totalPages: Math.ceil(total / limitNum) + totalPages: Math.ceil(total / limitNum), + _debug: { + filter: JSON.stringify(filter), + industriesReceived: industries + } }); } catch (error) { console.error('[Search] Error:', error.message); diff --git a/server/routers/procurement/scripts/migrate-messages.js b/server/routers/procurement/scripts/migrate-messages.js new file mode 100644 index 0000000..d342f44 --- /dev/null +++ b/server/routers/procurement/scripts/migrate-messages.js @@ -0,0 +1,93 @@ +const mongoose = require('mongoose'); +const Message = require('../models/Message'); +require('dotenv').config({ path: '../../.env' }); + +const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; + +async function migrateMessages() { + try { + console.log('[Migration] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('[Migration] Connected to MongoDB'); + + // Найти всС сообщСния + const allMessages = await Message.find().exec(); + console.log('[Migration] Found', allMessages.length, 'total messages'); + + let fixedCount = 0; + let errorCount = 0; + + // ΠŸΡ€ΠΎΡ…ΠΎΠ΄ΠΈΠΌ ΠΏΠΎ ΠΊΠ°ΠΆΠ΄ΠΎΠΌΡƒ ΡΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΡŽ + for (const message of allMessages) { + try { + const threadId = message.threadId; + if (!threadId) { + console.log('[Migration] Skipping message', message._id, '- no threadId'); + continue; + } + + // ΠŸΠ°Ρ€ΡΠΈΠΌ threadId Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π° "thread-id1-id2" ΠΈΠ»ΠΈ "id1-id2" + let ids = threadId.replace('thread-', '').split('-'); + + if (ids.length < 2) { + console.log('[Migration] Invalid threadId format:', threadId); + errorCount++; + continue; + } + + const companyId1 = ids[0]; + const companyId2 = ids[1]; + + // Π‘Ρ€Π°Π²Π½ΠΈΠ²Π°Π΅ΠΌ с senderCompanyId + const senderIdString = message.senderCompanyId.toString ? message.senderCompanyId.toString() : message.senderCompanyId; + const expectedRecipient = senderIdString === companyId1 ? companyId2 : companyId1; + + // Если recipientCompanyId Π½Π΅ установлСна ΠΈΠ»ΠΈ Π½Π΅ΠΏΡ€Π°Π²ΠΈΠ»ΡŒΠ½Π°Ρ - исправляСм + if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) { + console.log('[Migration] Fixing message', message._id); + console.log(' Old recipientCompanyId:', message.recipientCompanyId); + console.log(' Expected:', expectedRecipient); + + // ΠšΠΎΠ½Π²Π΅Ρ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ Π² ObjectId Ссли Π½ΡƒΠΆΠ½ΠΎ + const { ObjectId } = require('mongoose').Types; + let recipientObjectId = expectedRecipient; + try { + if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) { + recipientObjectId = new ObjectId(expectedRecipient); + } + } catch (e) { + console.log(' Could not convert to ObjectId'); + } + + await Message.updateOne( + { _id: message._id }, + { recipientCompanyId: recipientObjectId } + ); + + fixedCount++; + console.log(' βœ… Fixed'); + } + } catch (err) { + console.error('[Migration] Error processing message', message._id, ':', err.message); + errorCount++; + } + } + + console.log('[Migration] βœ… Migration completed!'); + console.log('[Migration] Fixed:', fixedCount, 'messages'); + console.log('[Migration] Errors:', errorCount); + + await mongoose.connection.close(); + console.log('[Migration] Disconnected from MongoDB'); + } catch (err) { + console.error('[Migration] ❌ Error:', err.message); + process.exit(1); + } +} + +migrateMessages(); diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 09cd954..a641bb5 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -11,8 +11,8 @@ const recreateTestUser = async () => { console.log('\nπŸ”„ ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΊ MongoDB...'); await mongoose.connect(mongoUri, { - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, + useNewUrlParser: true, + useUnifiedTopology: true, }); console.log('βœ… ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ ΠΊ MongoDB\n'); @@ -76,6 +76,21 @@ const recreateTestUser = async () => { console.log(' ΠŸΠ°Ρ€ΠΎΠ»ΡŒ: SecurePass123!'); console.log(''); + // ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ mock ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ + console.log('\nπŸ”„ ОбновлСниС ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ… mock ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΉ...'); + const updates = [ + { inn: '7707083894', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, + { inn: '7707083895', updates: { companySize: '500+', partnerGeography: ['moscow', 'russia_all'] } }, + { inn: '7707083896', updates: { companySize: '11-50', partnerGeography: ['moscow', 'russia_all'] } }, + { inn: '7707083897', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, + { inn: '7707083898', updates: { companySize: '251-500', partnerGeography: ['moscow', 'russia_all'] } }, + ]; + + for (const item of updates) { + await Company.updateOne({ inn: item.inn }, { $set: item.updates }); + console.log(` βœ“ Компания ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½Π°: INN ${item.inn}`); + } + await mongoose.connection.close(); process.exit(0); } catch (error) { diff --git a/server/routers/procurement/scripts/test-logging.js b/server/routers/procurement/scripts/test-logging.js new file mode 100644 index 0000000..e914f23 --- /dev/null +++ b/server/routers/procurement/scripts/test-logging.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +/** + * Π‘ΠΊΡ€ΠΈΠΏΡ‚ для тСстирования логирования + * + * ИспользованиС: + * node stubs/scripts/test-logging.js # Π›ΠΎΠ³ΠΈ скрыты (DEV Π½Π΅ установлСна) + * DEV=true node stubs/scripts/test-logging.js # Π›ΠΎΠ³ΠΈ Π²ΠΈΠ΄Π½Ρ‹ + */ + +// Ѐункция логирования ΠΈΠ· ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΎΠ² +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + +console.log(''); +console.log('='.repeat(60)); +console.log('TEST: Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ с ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ окруТСния DEV'); +console.log('='.repeat(60)); +console.log(''); + +console.log('Π—Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ DEV:', process.env.DEV || '(Π½Π΅ установлСна)'); +console.log(''); + +// ВСстируСм Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Π΅ Π»ΠΎΠ³ΠΈ +log('[Auth] Token verified - userId: 68fe2ccda3526c303ca06799 companyId: 68fe2ccda3526c303ca06796'); +log('[Auth] Generating token for userId:', '68fe2ccda3526c303ca06799'); +log('[BuyProducts] Found', 0, 'products for company 68fe2ccda3526c303ca06796'); +log('[Products] GET Fetching products for companyId:', '68fe2ccda3526c303ca06799'); +log('[Products] Found', 1, 'products'); +log('[Reviews] Returned', 0, 'reviews for company 68fe2ccda3526c303ca06796'); +log('[Messages] Fetching threads for companyId:', '68fe2ccda3526c303ca06796'); +log('[Messages] Found', 4, 'messages for company'); +log('[Messages] Returned', 3, 'unique threads'); +log('[Search] Request params:', { query: '', page: 1 }); + +console.log(''); +console.log('='.repeat(60)); +console.log('РЕЗУЛЬВАВ:'); +console.log('='.repeat(60)); + +if (process.env.DEV === 'true') { + console.log('βœ… DEV=true - Π»ΠΎΠ³ΠΈ Π’Π˜Π”ΠΠ« Π²Ρ‹ΡˆΠ΅'); +} else { + console.log('❌ DEV Π½Π΅ установлСна ΠΈΠ»ΠΈ != "true" - Π»ΠΎΠ³ΠΈ БКРЫВЫ'); + console.log(''); + console.log('Для Π²ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ Π»ΠΎΠ³ΠΎΠ² запуститС:'); + console.log(' export DEV=true && npm start (Linux/Mac)'); + console.log(' $env:DEV = "true"; npm start (PowerShell)'); + console.log(' set DEV=true && npm start (CMD)'); +} + +console.log(''); +console.log('='.repeat(60)); +console.log('');