feature/worker #111
@@ -1,5 +1,34 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
// Явно определяем схему для файлов
|
||||||
|
const fileSchema = new mongoose.Schema({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
storagePath: String,
|
||||||
|
uploadedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}, { _id: false });
|
||||||
|
|
||||||
const buyProductSchema = new mongoose.Schema({
|
const buyProductSchema = new mongoose.Schema({
|
||||||
companyId: {
|
companyId: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -24,18 +53,7 @@ const buyProductSchema = new mongoose.Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'шт'
|
default: 'шт'
|
||||||
},
|
},
|
||||||
files: [{
|
files: [fileSchema],
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
url: String,
|
|
||||||
type: String,
|
|
||||||
size: Number,
|
|
||||||
storagePath: String,
|
|
||||||
uploadedAt: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
acceptedBy: [{
|
acceptedBy: [{
|
||||||
companyId: {
|
companyId: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ const requestSchema = new mongoose.Schema({
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
files: [{
|
files: [{
|
||||||
id: String,
|
id: { type: String },
|
||||||
name: String,
|
name: { type: String },
|
||||||
url: String,
|
url: { type: String },
|
||||||
type: String,
|
type: { type: String },
|
||||||
size: Number,
|
size: { type: Number },
|
||||||
storagePath: String,
|
storagePath: { type: String },
|
||||||
uploadedAt: {
|
uploadedAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now
|
default: Date.now
|
||||||
@@ -47,12 +47,12 @@ const requestSchema = new mongoose.Schema({
|
|||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
responseFiles: [{
|
responseFiles: [{
|
||||||
id: String,
|
id: { type: String },
|
||||||
name: String,
|
name: { type: String },
|
||||||
url: String,
|
url: { type: String },
|
||||||
type: String,
|
type: { type: String },
|
||||||
size: Number,
|
size: { type: Number },
|
||||||
storagePath: String,
|
storagePath: { type: String },
|
||||||
uploadedAt: {
|
uploadedAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now
|
default: Date.now
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Request = require('../models/Request');
|
|||||||
const BuyProduct = require('../models/BuyProduct');
|
const BuyProduct = require('../models/BuyProduct');
|
||||||
const Message = require('../models/Message');
|
const Message = require('../models/Message');
|
||||||
const Review = require('../models/Review');
|
const Review = require('../models/Review');
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
const { Types } = mongoose;
|
const { Types } = mongoose;
|
||||||
|
|
||||||
const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796');
|
const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796');
|
||||||
@@ -116,9 +116,6 @@ const waitForDatabaseConnection = async () => {
|
|||||||
|
|
||||||
const verifyAuth = async () => {
|
const verifyAuth = async () => {
|
||||||
try {
|
try {
|
||||||
if (!mongoose.connection.db) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await mongoose.connection.db.admin().command({ listDatabases: 1 });
|
await mongoose.connection.db.admin().command({ listDatabases: 1 });
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -139,15 +136,17 @@ const waitForDatabaseConnection = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ожидаем подключения (подключение происходит автоматически через server/utils/mongoose.ts)
|
const connection = await connectDB();
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
if (!connection) {
|
||||||
|
break;
|
||||||
if (mongoose.connection.readyState === 1) {
|
|
||||||
const authed = await verifyAuth();
|
|
||||||
if (authed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authed = await verifyAuth();
|
||||||
|
if (authed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mongoose.connection.close().catch(() => {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isAuthFailure(error)) {
|
if (!isAuthFailure(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -218,8 +217,12 @@ const initializeTestUser = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing test data:', error.message);
|
console.error('Error initializing test data:', error.message);
|
||||||
if (error?.code === 13 || /auth/i.test(error?.message || '')) {
|
if (error?.code === 13 || /auth/i.test(error?.message || '')) {
|
||||||
if (process.env.DEV === 'true') {
|
try {
|
||||||
console.error('Auth error detected. Connection managed by server/utils/mongoose.ts');
|
await connectDB();
|
||||||
|
} catch (connectError) {
|
||||||
|
if (process.env.DEV === 'true') {
|
||||||
|
console.error('Failed to re-connect after auth error:', connectError.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const BuyDocument = require('../models/BuyDocument')
|
const BuyDocument = require('../models/BuyDocument')
|
||||||
|
|
||||||
// Create remote-assets/docs directory if it doesn't exist
|
// Create remote-assets/docs directory if it doesn't exist
|
||||||
const docsDir = 'server/remote-assets/docs'
|
const docsDir = 'server/routers/remote-assets/docs'
|
||||||
if (!fs.existsSync(docsDir)) {
|
if (!fs.existsSync(docsDir)) {
|
||||||
fs.mkdirSync(docsDir, { recursive: true })
|
fs.mkdirSync(docsDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
const BuyProduct = require('../models/BuyProduct');
|
const BuyProduct = require('../models/BuyProduct');
|
||||||
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const UPLOADS_ROOT = 'server/remote-assets/uploads/buy-products';
|
const UPLOADS_ROOT = 'server/routers/remote-assets/uploads/buy-products';
|
||||||
const ensureDirectory = (dirPath) => {
|
const ensureDirectory = (dirPath) => {
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
@@ -23,18 +24,6 @@ const ALLOWED_MIME_TYPES = new Set([
|
|||||||
'text/csv',
|
'text/csv',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const getExtension = (filename) => {
|
|
||||||
const lastDot = filename.lastIndexOf('.');
|
|
||||||
return lastDot > 0 ? filename.slice(lastDot) : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBasename = (filename) => {
|
|
||||||
const lastDot = filename.lastIndexOf('.');
|
|
||||||
const name = lastDot > 0 ? filename.slice(0, lastDot) : filename;
|
|
||||||
const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
|
|
||||||
return lastSlash >= 0 ? name.slice(lastSlash + 1) : name;
|
|
||||||
};
|
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
const productId = req.params.id || 'common';
|
const productId = req.params.id || 'common';
|
||||||
@@ -43,10 +32,12 @@ const storage = multer.diskStorage({
|
|||||||
cb(null, productDir);
|
cb(null, productDir);
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const originalExtension = getExtension(file.originalname);
|
// Исправляем кодировку имени файла из Latin1 в UTF-8
|
||||||
const baseName = getBasename(file.originalname)
|
const fixedName = Buffer.from(file.originalname, 'latin1').toString('utf8');
|
||||||
.replace(/[^a-zA-Z0-9-_]+/g, '_')
|
const originalExtension = path.extname(fixedName) || '';
|
||||||
.toLowerCase();
|
const baseName = path
|
||||||
|
.basename(fixedName, originalExtension)
|
||||||
|
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '_'); // Убираем только недопустимые символы Windows, оставляем кириллицу
|
||||||
cb(null, `${Date.now()}_${baseName}${originalExtension}`);
|
cb(null, `${Date.now()}_${baseName}${originalExtension}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -241,7 +232,16 @@ router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Только владелец товара может добавить файл
|
// Только владелец товара может добавить файл
|
||||||
if (product.companyId.toString() !== req.companyId.toString()) {
|
const productCompanyId = product.companyId?.toString() || product.companyId;
|
||||||
|
const requestCompanyId = req.companyId?.toString() || req.companyId;
|
||||||
|
|
||||||
|
console.log('[BuyProducts] Comparing company IDs:', {
|
||||||
|
productCompanyId,
|
||||||
|
requestCompanyId,
|
||||||
|
match: productCompanyId === requestCompanyId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (productCompanyId !== requestCompanyId) {
|
||||||
return res.status(403).json({ error: 'Not authorized' });
|
return res.status(403).json({ error: 'Not authorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,28 +253,75 @@ router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res)
|
|||||||
return res.status(400).json({ error: 'File is required' });
|
return res.status(400).json({ error: 'File is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const relativePath = `buy-products/${id}/${req.file.filename}`;
|
// Исправляем кодировку имени файла из Latin1 в UTF-8
|
||||||
|
const fixedFileName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
|
||||||
|
|
||||||
|
// Извлекаем timestamp из имени файла, созданного multer (формат: {timestamp}_{name}.ext)
|
||||||
|
const fileTimestamp = req.file.filename.split('_')[0];
|
||||||
|
|
||||||
|
// storagePath относительно UPLOADS_ROOT (который уже включает 'buy-products')
|
||||||
|
const relativePath = `${id}/${req.file.filename}`;
|
||||||
const file = {
|
const file = {
|
||||||
id: `file-${Date.now()}`,
|
id: `file-${fileTimestamp}`, // Используем тот же timestamp, что и в имени файла
|
||||||
name: req.file.originalname,
|
name: fixedFileName,
|
||||||
url: `/uploads/${relativePath}`,
|
url: `/uploads/buy-products/${relativePath}`,
|
||||||
type: req.file.mimetype,
|
type: req.file.mimetype,
|
||||||
size: req.file.size,
|
size: req.file.size,
|
||||||
uploadedAt: new Date(),
|
uploadedAt: new Date(),
|
||||||
storagePath: relativePath,
|
storagePath: relativePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
product.files.push(file);
|
console.log('[BuyProducts] Adding file to product:', {
|
||||||
await product.save();
|
productId: id,
|
||||||
|
fileName: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
filePath: relativePath
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[BuyProducts] File object:', JSON.stringify(file, null, 2));
|
||||||
|
|
||||||
|
// Используем findByIdAndUpdate вместо save() для избежания проблем с валидацией
|
||||||
|
let updatedProduct;
|
||||||
|
try {
|
||||||
|
console.log('[BuyProducts] Calling findByIdAndUpdate with id:', id);
|
||||||
|
updatedProduct = await BuyProduct.findByIdAndUpdate(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
$push: { files: file },
|
||||||
|
$set: { updatedAt: new Date() }
|
||||||
|
},
|
||||||
|
{ new: true, runValidators: false }
|
||||||
|
);
|
||||||
|
console.log('[BuyProducts] findByIdAndUpdate completed');
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error('[BuyProducts] findByIdAndUpdate error:', {
|
||||||
|
message: updateError.message,
|
||||||
|
name: updateError.name,
|
||||||
|
code: updateError.code
|
||||||
|
});
|
||||||
|
throw updateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updatedProduct) {
|
||||||
|
throw new Error('Failed to update product with file');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[BuyProducts] File added successfully to product:', id);
|
||||||
|
|
||||||
log('[BuyProducts] File added to product:', id, file.name);
|
log('[BuyProducts] File added to product:', id, file.name);
|
||||||
|
|
||||||
res.json(product);
|
res.json(updatedProduct);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BuyProducts] Error adding file:', error.message);
|
console.error('[BuyProducts] Error adding file:', error.message);
|
||||||
|
console.error('[BuyProducts] Error stack:', error.stack);
|
||||||
|
console.error('[BuyProducts] Error name:', error.name);
|
||||||
|
if (error.errors) {
|
||||||
|
console.error('[BuyProducts] Validation errors:', JSON.stringify(error.errors, null, 2));
|
||||||
|
}
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
details: error.errors || {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -303,7 +350,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => {
|
|||||||
await product.save();
|
await product.save();
|
||||||
|
|
||||||
const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, '');
|
const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, '');
|
||||||
const absolutePath = `server/remote-assets/uploads/${storedPath}`;
|
const absolutePath = `server/routers/remote-assets/uploads/${storedPath}`;
|
||||||
|
|
||||||
fs.promises.unlink(absolutePath).catch((unlinkError) => {
|
fs.promises.unlink(absolutePath).catch((unlinkError) => {
|
||||||
if (unlinkError && unlinkError.code !== 'ENOENT') {
|
if (unlinkError && unlinkError.code !== 'ENOENT') {
|
||||||
@@ -391,4 +438,65 @@ router.get('/:id/acceptances', verifyToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /buy-products/download/:id/:fileId - скачать файл
|
||||||
|
router.get('/download/:id/:fileId', verifyToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log('[BuyProducts] Download request received:', {
|
||||||
|
productId: req.params.id,
|
||||||
|
fileId: req.params.fileId,
|
||||||
|
userId: req.userId,
|
||||||
|
companyId: req.companyId,
|
||||||
|
headers: req.headers.authorization
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id, fileId } = req.params;
|
||||||
|
const product = await BuyProduct.findById(id);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return res.status(404).json({ error: 'Product not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = product.files.find((f) => f.id === fileId);
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({ error: 'File not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем абсолютный путь к файлу
|
||||||
|
const filePath = path.resolve(UPLOADS_ROOT, file.storagePath);
|
||||||
|
|
||||||
|
console.log('[BuyProducts] Trying to download file:', {
|
||||||
|
fileId: file.id,
|
||||||
|
fileName: file.name,
|
||||||
|
storagePath: file.storagePath,
|
||||||
|
absolutePath: filePath,
|
||||||
|
exists: fs.existsSync(filePath)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем существование файла
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error('[BuyProducts] File not found on disk:', filePath);
|
||||||
|
return res.status(404).json({ error: 'File not found on disk' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем правильные заголовки для скачивания с поддержкой кириллицы
|
||||||
|
const encodedFileName = encodeURIComponent(file.name);
|
||||||
|
res.setHeader('Content-Type', file.type || 'application/octet-stream');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
|
||||||
|
res.setHeader('Content-Length', file.size);
|
||||||
|
|
||||||
|
// Отправляем файл
|
||||||
|
res.sendFile(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('[BuyProducts] Error sending file:', err.message);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: 'Error downloading file' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[BuyProducts] Error downloading file:', error.message);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const Company = require('../models/Company');
|
|||||||
const Experience = require('../models/Experience');
|
const Experience = require('../models/Experience');
|
||||||
const Request = require('../models/Request');
|
const Request = require('../models/Request');
|
||||||
const Message = require('../models/Message');
|
const Message = require('../models/Message');
|
||||||
const { Types } = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
const { Types } = mongoose;
|
||||||
|
|
||||||
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
|
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
|
||||||
router.get('/my/info', verifyToken, async (req, res) => {
|
router.get('/my/info', verifyToken, async (req, res) => {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
const Experience = require('../models/Experience');
|
const Experience = require('../models/Experience');
|
||||||
const { Types } = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
const { Types } = mongoose;
|
||||||
|
|
||||||
// GET /experience - Получить список опыта работы компании
|
// GET /experience - Получить список опыта работы компании
|
||||||
router.get('/', verifyToken, async (req, res) => {
|
router.get('/', verifyToken, async (req, res) => {
|
||||||
|
|||||||
@@ -21,21 +21,23 @@ router.get('/aggregates', verifyToken, async (req, res) => {
|
|||||||
|
|
||||||
const companyId = user.companyId.toString();
|
const companyId = user.companyId.toString();
|
||||||
|
|
||||||
const [docsCount, acceptsCount, requestsCount] = await Promise.all([
|
// Получить все BuyProduct для подсчета файлов и акцептов
|
||||||
BuyProduct.countDocuments({ companyId }),
|
const buyProducts = await BuyProduct.find({ companyId });
|
||||||
Request.countDocuments({
|
|
||||||
$or: [
|
// Подсчет документов - сумма всех файлов во всех BuyProduct
|
||||||
{ senderCompanyId: companyId, status: 'accepted' },
|
const docsCount = buyProducts.reduce((total, product) => {
|
||||||
{ recipientCompanyId: companyId, status: 'accepted' }
|
return total + (product.files ? product.files.length : 0);
|
||||||
]
|
}, 0);
|
||||||
}),
|
|
||||||
Request.countDocuments({
|
// Подсчет акцептов - сумма всех acceptedBy во всех BuyProduct
|
||||||
$or: [
|
const acceptsCount = buyProducts.reduce((total, product) => {
|
||||||
{ senderCompanyId: companyId },
|
return total + (product.acceptedBy ? product.acceptedBy.length : 0);
|
||||||
{ recipientCompanyId: companyId }
|
}, 0);
|
||||||
]
|
|
||||||
})
|
// Подсчет исходящих запросов (только отправленные этой компанией)
|
||||||
]);
|
const requestsCount = await Request.countDocuments({
|
||||||
|
senderCompanyId: companyId
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
docsCount,
|
docsCount,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
const Message = require('../models/Message');
|
const Message = require('../models/Message');
|
||||||
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
const { ObjectId } = mongoose.Types;
|
||||||
|
|
||||||
// Функция для логирования с проверкой DEV переменной
|
// Функция для логирования с проверкой DEV переменной
|
||||||
const log = (message, data = '') => {
|
const log = (message, data = '') => {
|
||||||
@@ -18,7 +20,6 @@ const log = (message, data = '') => {
|
|||||||
router.get('/threads', verifyToken, async (req, res) => {
|
router.get('/threads', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const companyId = req.companyId;
|
const companyId = req.companyId;
|
||||||
const { ObjectId } = require('mongoose').Types;
|
|
||||||
|
|
||||||
log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId);
|
log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId);
|
||||||
|
|
||||||
@@ -146,7 +147,6 @@ router.post('/:threadId', verifyToken, async (req, res) => {
|
|||||||
|
|
||||||
// Найти recipientCompanyId по ObjectId если нужно
|
// Найти recipientCompanyId по ObjectId если нужно
|
||||||
let recipientObjectId = recipientCompanyId;
|
let recipientObjectId = recipientCompanyId;
|
||||||
const { ObjectId } = require('mongoose').Types;
|
|
||||||
try {
|
try {
|
||||||
if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) {
|
if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) {
|
||||||
recipientObjectId = new ObjectId(recipientCompanyId);
|
recipientObjectId = new ObjectId(recipientCompanyId);
|
||||||
@@ -210,7 +210,6 @@ router.post('/admin/migrate-fix-recipients', async (req, res) => {
|
|||||||
|
|
||||||
// If recipientCompanyId is not set or wrong - fix it
|
// If recipientCompanyId is not set or wrong - fix it
|
||||||
if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) {
|
if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) {
|
||||||
const { ObjectId } = require('mongoose').Types;
|
|
||||||
let recipientObjectId = expectedRecipient;
|
let recipientObjectId = expectedRecipient;
|
||||||
try {
|
try {
|
||||||
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
|
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ const router = express.Router();
|
|||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
const Request = require('../models/Request');
|
const Request = require('../models/Request');
|
||||||
const BuyProduct = require('../models/BuyProduct');
|
const BuyProduct = require('../models/BuyProduct');
|
||||||
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
|
||||||
// Функция для логирования с проверкой DEV переменной
|
// Функция для логирования с проверкой DEV переменной
|
||||||
const log = (message, data = '') => {
|
const log = (message, data = '') => {
|
||||||
@@ -17,7 +19,7 @@ const log = (message, data = '') => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const REQUESTS_UPLOAD_ROOT = 'server/remote-assets/uploads/requests';
|
const REQUESTS_UPLOAD_ROOT = 'server/routers/remote-assets/uploads/requests';
|
||||||
|
|
||||||
const ensureDirectory = (dirPath) => {
|
const ensureDirectory = (dirPath) => {
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
@@ -37,28 +39,17 @@ const ALLOWED_REQUEST_MIME_TYPES = new Set([
|
|||||||
'text/csv',
|
'text/csv',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const getExtension = (filename) => {
|
|
||||||
const lastDot = filename.lastIndexOf('.');
|
|
||||||
return lastDot > 0 ? filename.slice(lastDot) : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBasename = (filename) => {
|
|
||||||
const lastDot = filename.lastIndexOf('.');
|
|
||||||
const name = lastDot > 0 ? filename.slice(0, lastDot) : filename;
|
|
||||||
const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
|
|
||||||
return lastSlash >= 0 ? name.slice(lastSlash + 1) : name;
|
|
||||||
};
|
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
const subfolder = req.requestUploadSubfolder || '';
|
const subfolder = req.requestUploadSubfolder || '';
|
||||||
const destinationDir = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}` : REQUESTS_UPLOAD_ROOT;
|
const destinationDir = `${REQUESTS_UPLOAD_ROOT}/${subfolder}`;
|
||||||
ensureDirectory(destinationDir);
|
ensureDirectory(destinationDir);
|
||||||
cb(null, destinationDir);
|
cb(null, destinationDir);
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const extension = getExtension(file.originalname);
|
const extension = path.extname(file.originalname) || '';
|
||||||
const baseName = getBasename(file.originalname)
|
const baseName = path
|
||||||
|
.basename(file.originalname, extension)
|
||||||
.replace(/[^a-zA-Z0-9-_]+/g, '_')
|
.replace(/[^a-zA-Z0-9-_]+/g, '_')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
cb(null, `${Date.now()}_${baseName}${extension}`);
|
cb(null, `${Date.now()}_${baseName}${extension}`);
|
||||||
@@ -107,7 +98,7 @@ const cleanupUploadedFiles = async (req) => {
|
|||||||
|
|
||||||
const subfolder = req.requestUploadSubfolder || '';
|
const subfolder = req.requestUploadSubfolder || '';
|
||||||
const removalTasks = req.files.map((file) => {
|
const removalTasks = req.files.map((file) => {
|
||||||
const filePath = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}/${file.filename}` : `${REQUESTS_UPLOAD_ROOT}/${file.filename}`;
|
const filePath = `${REQUESTS_UPLOAD_ROOT}/${subfolder}/${file.filename}`;
|
||||||
return fs.promises.unlink(filePath).catch((error) => {
|
return fs.promises.unlink(filePath).catch((error) => {
|
||||||
if (error.code !== 'ENOENT') {
|
if (error.code !== 'ENOENT') {
|
||||||
console.error('[Requests] Failed to cleanup uploaded file:', error.message);
|
console.error('[Requests] Failed to cleanup uploaded file:', error.message);
|
||||||
@@ -125,7 +116,7 @@ const mapFilesToMetadata = (req) => {
|
|||||||
|
|
||||||
const subfolder = req.requestUploadSubfolder || '';
|
const subfolder = req.requestUploadSubfolder || '';
|
||||||
return req.files.map((file) => {
|
return req.files.map((file) => {
|
||||||
const relativePath = subfolder ? `requests/${subfolder}/${file.filename}` : `requests/${file.filename}`;
|
const relativePath = `requests/${subfolder}/${file.filename}`;
|
||||||
return {
|
return {
|
||||||
id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
name: file.originalname,
|
name: file.originalname,
|
||||||
@@ -169,7 +160,7 @@ const removeStoredFiles = async (files = []) => {
|
|||||||
const tasks = files
|
const tasks = files
|
||||||
.filter((file) => file && file.storagePath)
|
.filter((file) => file && file.storagePath)
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
const absolutePath = `server/remote-assets/uploads/${file.storagePath}`;
|
const absolutePath = `server/routers/remote-assets/uploads/${file.storagePath}`;
|
||||||
return fs.promises.unlink(absolutePath).catch((error) => {
|
return fs.promises.unlink(absolutePath).catch((error) => {
|
||||||
if (error.code !== 'ENOENT') {
|
if (error.code !== 'ENOENT') {
|
||||||
console.error('[Requests] Failed to remove stored file:', error.message);
|
console.error('[Requests] Failed to remove stored file:', error.message);
|
||||||
@@ -255,24 +246,61 @@ router.post(
|
|||||||
return res.status(400).json({ error: 'At least one recipient is required' });
|
return res.status(400).json({ error: 'At least one recipient is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!subject && productId) {
|
let uploadedFiles = mapFilesToMetadata(req);
|
||||||
|
|
||||||
|
console.log('========================');
|
||||||
|
console.log('[Requests] Initial uploadedFiles:', uploadedFiles.length);
|
||||||
|
console.log('[Requests] ProductId:', productId);
|
||||||
|
|
||||||
|
// Если есть productId, получаем данные товара
|
||||||
|
if (productId) {
|
||||||
try {
|
try {
|
||||||
const product = await BuyProduct.findById(productId);
|
const product = await BuyProduct.findById(productId);
|
||||||
|
console.log('[Requests] Product found:', product ? product.name : 'null');
|
||||||
|
console.log('[Requests] Product files count:', product?.files?.length || 0);
|
||||||
|
if (product && product.files) {
|
||||||
|
console.log('[Requests] Product files:', JSON.stringify(product.files, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
if (product) {
|
if (product) {
|
||||||
subject = product.name;
|
// Берем subject из товара, если не указан
|
||||||
|
if (!subject) {
|
||||||
|
subject = product.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если файлы не загружены вручную, используем файлы из товара
|
||||||
|
if (uploadedFiles.length === 0 && product.files && product.files.length > 0) {
|
||||||
|
console.log('[Requests] ✅ Copying files from product...');
|
||||||
|
// Копируем файлы из товара, изменяя путь для запроса
|
||||||
|
uploadedFiles = product.files.map(file => ({
|
||||||
|
id: file.id || `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
name: file.name,
|
||||||
|
url: file.url,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
uploadedAt: file.uploadedAt || new Date(),
|
||||||
|
storagePath: file.storagePath || file.url.replace('/uploads/', ''),
|
||||||
|
}));
|
||||||
|
console.log('[Requests] ✅ Using', uploadedFiles.length, 'files from product:', productId);
|
||||||
|
console.log('[Requests] ✅ Copied files:', JSON.stringify(uploadedFiles, null, 2));
|
||||||
|
} else {
|
||||||
|
console.log('[Requests] ❌ NOT copying files. uploadedFiles.length:', uploadedFiles.length, 'product.files.length:', product.files?.length || 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (lookupError) {
|
} catch (lookupError) {
|
||||||
console.error('[Requests] Failed to lookup product for subject:', lookupError.message);
|
console.error('[Requests] ❌ Failed to lookup product:', lookupError.message);
|
||||||
|
console.error(lookupError.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[Requests] Final uploadedFiles for saving:', JSON.stringify(uploadedFiles, null, 2));
|
||||||
|
console.log('========================');
|
||||||
|
|
||||||
if (!subject) {
|
if (!subject) {
|
||||||
await cleanupUploadedFiles(req);
|
await cleanupUploadedFiles(req);
|
||||||
return res.status(400).json({ error: 'Subject is required' });
|
return res.status(400).json({ error: 'Subject is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedFiles = mapFilesToMetadata(req);
|
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const recipientCompanyId of recipients) {
|
for (const recipientCompanyId of recipients) {
|
||||||
try {
|
try {
|
||||||
@@ -331,9 +359,17 @@ router.put(
|
|||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
console.log('[Requests] PUT /requests/:id called with id:', id);
|
||||||
|
console.log('[Requests] Request body:', req.body);
|
||||||
|
console.log('[Requests] Files:', req.files);
|
||||||
|
console.log('[Requests] CompanyId:', req.companyId);
|
||||||
|
|
||||||
const responseText = (req.body.response || '').trim();
|
const responseText = (req.body.response || '').trim();
|
||||||
const statusRaw = (req.body.status || 'accepted').toLowerCase();
|
const statusRaw = (req.body.status || 'accepted').toLowerCase();
|
||||||
const status = statusRaw === 'rejected' ? 'rejected' : 'accepted';
|
const status = statusRaw === 'rejected' ? 'rejected' : 'accepted';
|
||||||
|
|
||||||
|
console.log('[Requests] Response text:', responseText);
|
||||||
|
console.log('[Requests] Status:', status);
|
||||||
|
|
||||||
if (req.invalidFiles && req.invalidFiles.length > 0) {
|
if (req.invalidFiles && req.invalidFiles.length > 0) {
|
||||||
await cleanupUploadedFiles(req);
|
await cleanupUploadedFiles(req);
|
||||||
@@ -361,6 +397,8 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uploadedResponseFiles = mapFilesToMetadata(req);
|
const uploadedResponseFiles = mapFilesToMetadata(req);
|
||||||
|
console.log('[Requests] Uploaded response files count:', uploadedResponseFiles.length);
|
||||||
|
console.log('[Requests] Uploaded response files:', JSON.stringify(uploadedResponseFiles, null, 2));
|
||||||
|
|
||||||
if (uploadedResponseFiles.length > 0) {
|
if (uploadedResponseFiles.length > 0) {
|
||||||
await removeStoredFiles(request.responseFiles || []);
|
await removeStoredFiles(request.responseFiles || []);
|
||||||
@@ -372,18 +410,126 @@ router.put(
|
|||||||
request.respondedAt = new Date();
|
request.respondedAt = new Date();
|
||||||
request.updatedAt = new Date();
|
request.updatedAt = new Date();
|
||||||
|
|
||||||
await request.save();
|
let savedRequest;
|
||||||
|
try {
|
||||||
|
savedRequest = await request.save();
|
||||||
|
log('[Requests] Request responded:', id);
|
||||||
|
} catch (saveError) {
|
||||||
|
console.error('[Requests] Mongoose save failed, trying direct MongoDB update:', saveError.message);
|
||||||
|
// Fallback: использовать MongoDB драйвер напрямую
|
||||||
|
const updateData = {
|
||||||
|
response: responseText,
|
||||||
|
status: status,
|
||||||
|
respondedAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
if (uploadedResponseFiles.length > 0) {
|
||||||
|
updateData.responseFiles = uploadedResponseFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await mongoose.connection.collection('requests').findOneAndUpdate(
|
||||||
|
{ _id: new mongoose.Types.ObjectId(id) },
|
||||||
|
{ $set: updateData },
|
||||||
|
{ returnDocument: 'after' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Failed to update request');
|
||||||
|
}
|
||||||
|
savedRequest = result;
|
||||||
|
log('[Requests] Request responded via direct MongoDB update:', id);
|
||||||
|
}
|
||||||
|
|
||||||
log('[Requests] Request responded:', id);
|
res.json(savedRequest);
|
||||||
|
|
||||||
res.json(request);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Requests] Error responding to request:', error.message);
|
console.error('[Requests] Error responding to request:', error.message);
|
||||||
|
console.error('[Requests] Error stack:', error.stack);
|
||||||
|
if (error.name === 'ValidationError') {
|
||||||
|
console.error('[Requests] Validation errors:', JSON.stringify(error.errors, null, 2));
|
||||||
|
}
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// GET /requests/download/:id/:fileId - скачать файл ответа
|
||||||
|
router.get('/download/:id/:fileId', verifyToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log('[Requests] Download request received:', {
|
||||||
|
requestId: req.params.id,
|
||||||
|
fileId: req.params.fileId,
|
||||||
|
userId: req.userId,
|
||||||
|
companyId: req.companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id, fileId } = req.params;
|
||||||
|
const request = await Request.findById(id);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return res.status(404).json({ error: 'Request not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что пользователь имеет доступ к запросу (отправитель или получатель)
|
||||||
|
if (request.senderCompanyId !== req.companyId && request.recipientCompanyId !== req.companyId) {
|
||||||
|
return res.status(403).json({ error: 'Not authorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем файл в responseFiles или в обычных files
|
||||||
|
let file = request.responseFiles?.find((f) => f.id === fileId);
|
||||||
|
if (!file) {
|
||||||
|
file = request.files?.find((f) => f.id === fileId);
|
||||||
|
}
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({ error: 'File not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем абсолютный путь к файлу
|
||||||
|
// Если storagePath не начинается с 'requests/', значит это файл из buy-products
|
||||||
|
let fullPath = file.storagePath;
|
||||||
|
if (!fullPath.startsWith('requests/')) {
|
||||||
|
fullPath = `buy-products/${fullPath}`;
|
||||||
|
}
|
||||||
|
const filePath = path.resolve(`server/routers/remote-assets/uploads/${fullPath}`);
|
||||||
|
|
||||||
|
console.log('[Requests] Trying to download file:', {
|
||||||
|
fileId: file.id,
|
||||||
|
fileName: file.name,
|
||||||
|
storagePath: file.storagePath,
|
||||||
|
absolutePath: filePath,
|
||||||
|
exists: fs.existsSync(filePath),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем существование файла
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error('[Requests] File not found on disk:', filePath);
|
||||||
|
return res.status(404).json({ error: 'File not found on disk' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем правильные заголовки для скачивания с поддержкой кириллицы
|
||||||
|
const encodedFileName = encodeURIComponent(file.name);
|
||||||
|
res.setHeader('Content-Type', file.type || 'application/octet-stream');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
|
||||||
|
res.setHeader('Content-Length', file.size);
|
||||||
|
|
||||||
|
// Отправляем файл
|
||||||
|
res.sendFile(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('[Requests] Error sending file:', err.message);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: 'Error sending file' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('[Requests] File downloaded:', file.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Requests] Error downloading file:', error.message);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// DELETE /requests/:id - удалить запрос
|
// DELETE /requests/:id - удалить запрос
|
||||||
router.delete('/:id', verifyToken, async (req, res) => {
|
router.delete('/:id', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -54,10 +54,13 @@ router.get('/recommendations', verifyToken, async (req, res) => {
|
|||||||
// GET /search - Поиск компаний
|
// GET /search - Поиск компаний
|
||||||
router.get('/', verifyToken, async (req, res) => {
|
router.get('/', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('[Search] === NEW VERSION WITH FIXED SIZE FILTER ===');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query = '',
|
query = '',
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
|
offset, // Добавляем поддержку offset для точной пагинации
|
||||||
industries,
|
industries,
|
||||||
companySize,
|
companySize,
|
||||||
geography,
|
geography,
|
||||||
@@ -65,8 +68,12 @@ router.get('/', verifyToken, async (req, res) => {
|
|||||||
hasReviews,
|
hasReviews,
|
||||||
hasAcceptedDocs,
|
hasAcceptedDocs,
|
||||||
sortBy = 'relevance',
|
sortBy = 'relevance',
|
||||||
sortOrder = 'desc'
|
sortOrder = 'desc',
|
||||||
|
minEmployees, // Кастомный фильтр: минимум сотрудников
|
||||||
|
maxEmployees // Кастомный фильтр: максимум сотрудников
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
|
console.log('[Search] Filters:', { minEmployees, maxEmployees, companySize });
|
||||||
|
|
||||||
// Получить компанию пользователя, чтобы исключить её из результатов
|
// Получить компанию пользователя, чтобы исключить её из результатов
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
@@ -135,12 +142,99 @@ router.get('/', verifyToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по размеру компании
|
// Функция для парсинга диапазона из строки вида "51-250" или "500+"
|
||||||
if (companySize) {
|
const parseEmployeeRange = (sizeStr) => {
|
||||||
const sizeList = Array.isArray(companySize) ? companySize : [companySize];
|
if (sizeStr.includes('+')) {
|
||||||
if (sizeList.length > 0) {
|
const min = parseInt(sizeStr.replace('+', ''));
|
||||||
filters.push({ companySize: { $in: sizeList } });
|
return { min, max: Infinity };
|
||||||
}
|
}
|
||||||
|
const parts = sizeStr.split('-');
|
||||||
|
return {
|
||||||
|
min: parseInt(parts[0]),
|
||||||
|
max: parts[1] ? parseInt(parts[1]) : parseInt(parts[0])
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для проверки пересечения двух диапазонов
|
||||||
|
const rangesOverlap = (range1, range2) => {
|
||||||
|
return range1.min <= range2.max && range1.max >= range2.min;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Фильтр по размеру компании (чекбоксы) или кастомный диапазон
|
||||||
|
// Важно: этот фильтр должен получить все компании для корректной работы пересечения диапазонов
|
||||||
|
let sizeFilteredIds = null;
|
||||||
|
if ((companySize && companySize.length > 0) || minEmployees || maxEmployees) {
|
||||||
|
// Получаем все компании (без других фильтров, так как размер компании - это property-based фильтр)
|
||||||
|
const allCompanies = await Company.find({});
|
||||||
|
|
||||||
|
log('[Search] Employee size filter - checking companies:', allCompanies.length);
|
||||||
|
|
||||||
|
let matchingIds = [];
|
||||||
|
|
||||||
|
// Если есть кастомный диапазон - используем его
|
||||||
|
if (minEmployees || maxEmployees) {
|
||||||
|
const customRange = {
|
||||||
|
min: minEmployees ? parseInt(minEmployees, 10) : 0,
|
||||||
|
max: maxEmployees ? parseInt(maxEmployees, 10) : Infinity
|
||||||
|
};
|
||||||
|
|
||||||
|
log('[Search] Custom employee range filter:', customRange);
|
||||||
|
|
||||||
|
matchingIds = allCompanies
|
||||||
|
.filter(company => {
|
||||||
|
if (!company.companySize) {
|
||||||
|
log('[Search] Company has no size:', company.fullName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyRange = parseEmployeeRange(company.companySize);
|
||||||
|
const overlaps = rangesOverlap(companyRange, customRange);
|
||||||
|
|
||||||
|
log('[Search] Checking overlap:', {
|
||||||
|
company: company.fullName,
|
||||||
|
companyRange,
|
||||||
|
customRange,
|
||||||
|
overlaps
|
||||||
|
});
|
||||||
|
|
||||||
|
return overlaps;
|
||||||
|
})
|
||||||
|
.map(c => c._id);
|
||||||
|
|
||||||
|
log('[Search] Matching companies by custom range:', matchingIds.length);
|
||||||
|
}
|
||||||
|
// Иначе используем чекбоксы
|
||||||
|
else if (companySize && companySize.length > 0) {
|
||||||
|
const sizeList = Array.isArray(companySize) ? companySize : [companySize];
|
||||||
|
|
||||||
|
log('[Search] Company size checkboxes filter:', sizeList);
|
||||||
|
|
||||||
|
matchingIds = allCompanies
|
||||||
|
.filter(company => {
|
||||||
|
if (!company.companySize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyRange = parseEmployeeRange(company.companySize);
|
||||||
|
|
||||||
|
// Проверяем пересечение с любым из выбранных диапазонов
|
||||||
|
const matches = sizeList.some(selectedSize => {
|
||||||
|
const filterRange = parseEmployeeRange(selectedSize);
|
||||||
|
const overlaps = rangesOverlap(companyRange, filterRange);
|
||||||
|
log('[Search] Check:', company.fullName, companyRange, 'vs', filterRange, '=', overlaps);
|
||||||
|
return overlaps;
|
||||||
|
});
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
})
|
||||||
|
.map(c => c._id);
|
||||||
|
|
||||||
|
log('[Search] Matching companies by size checkboxes:', matchingIds.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем ID для дальнейшей фильтрации
|
||||||
|
sizeFilteredIds = matchingIds;
|
||||||
|
log('[Search] Size filtered IDs count:', sizeFilteredIds.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтр по географии
|
// Фильтр по географии
|
||||||
@@ -170,13 +264,25 @@ router.get('/', verifyToken, async (req, res) => {
|
|||||||
filters.push({ verified: true });
|
filters.push({ verified: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Применяем фильтр по размеру компании (если был задан)
|
||||||
|
if (sizeFilteredIds !== null) {
|
||||||
|
if (sizeFilteredIds.length > 0) {
|
||||||
|
filters.push({ _id: { $in: sizeFilteredIds } });
|
||||||
|
log('[Search] Applied size filter, IDs:', sizeFilteredIds.length);
|
||||||
|
} else {
|
||||||
|
// Если нет подходящих компаний по размеру, возвращаем пустой результат
|
||||||
|
filters.push({ _id: null });
|
||||||
|
log('[Search] No companies match size criteria');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Комбинировать все фильтры
|
// Комбинировать все фильтры
|
||||||
let filter = filters.length > 0 ? { $and: filters } : {};
|
let filter = filters.length > 0 ? { $and: filters } : {};
|
||||||
|
|
||||||
// Пагинация
|
// Пагинация - используем offset если передан, иначе вычисляем из page
|
||||||
const pageNum = parseInt(page) || 1;
|
|
||||||
const limitNum = parseInt(limit) || 10;
|
const limitNum = parseInt(limit) || 10;
|
||||||
const skip = (pageNum - 1) * limitNum;
|
const skip = offset !== undefined ? parseInt(offset) : ((parseInt(page) || 1) - 1) * limitNum;
|
||||||
|
const pageNum = offset !== undefined ? Math.floor(skip / limitNum) + 1 : parseInt(page) || 1;
|
||||||
|
|
||||||
// Сортировка
|
// Сортировка
|
||||||
let sortOptions = {};
|
let sortOptions = {};
|
||||||
@@ -228,3 +334,4 @@ router.get('/', verifyToken, async (req, res) => {
|
|||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
|
const { ObjectId } = mongoose.Types;
|
||||||
const Message = require('../models/Message');
|
const Message = require('../models/Message');
|
||||||
require('dotenv').config({ path: '../../.env' });
|
require('dotenv').config();
|
||||||
|
|
||||||
const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
|
|
||||||
|
|
||||||
async function migrateMessages() {
|
async function migrateMessages() {
|
||||||
try {
|
try {
|
||||||
console.log('[Migration] Connecting to MongoDB...');
|
// Подключение к MongoDB происходит через server/utils/mongoose.ts
|
||||||
await mongoose.connect(mongoUrl, {
|
console.log('[Migration] Checking MongoDB connection...');
|
||||||
useNewUrlParser: true,
|
if (mongoose.connection.readyState !== 1) {
|
||||||
useUnifiedTopology: true,
|
console.log('[Migration] Waiting for MongoDB connection...');
|
||||||
serverSelectionTimeoutMS: 5000,
|
await new Promise((resolve) => {
|
||||||
connectTimeoutMS: 5000,
|
mongoose.connection.once('connected', resolve);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
console.log('[Migration] Connected to MongoDB');
|
console.log('[Migration] Connected to MongoDB');
|
||||||
|
|
||||||
// Найти все сообщения
|
// Найти все сообщения
|
||||||
@@ -54,7 +54,6 @@ async function migrateMessages() {
|
|||||||
console.log(' Expected:', expectedRecipient);
|
console.log(' Expected:', expectedRecipient);
|
||||||
|
|
||||||
// Конвертируем в ObjectId если нужно
|
// Конвертируем в ObjectId если нужно
|
||||||
const { ObjectId } = require('mongoose').Types;
|
|
||||||
let recipientObjectId = expectedRecipient;
|
let recipientObjectId = expectedRecipient;
|
||||||
try {
|
try {
|
||||||
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
|
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
// Импорт моделей - прямые пути без path.join и __dirname
|
// Импорт моделей
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const Company = require('../models/Company');
|
const Company = require('../models/Company');
|
||||||
const Request = require('../models/Request');
|
const Request = require('../models/Request');
|
||||||
|
|
||||||
const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db';
|
// Подключение к MongoDB происходит через server/utils/mongoose.ts
|
||||||
const fallbackUri =
|
// Проверяем, подключено ли уже
|
||||||
process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
|
const ensureConnection = async () => {
|
||||||
|
if (mongoose.connection.readyState === 1) {
|
||||||
const connectWithFallback = async () => {
|
console.log('✅ MongoDB уже подключено');
|
||||||
// Сначала пробуем FALLBACK (с аутентификацией)
|
|
||||||
try {
|
|
||||||
console.log('\n📡 Подключение к MongoDB (с аутентификацией)...');
|
|
||||||
await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true });
|
|
||||||
console.log('✅ Подключено к MongoDB');
|
|
||||||
return;
|
return;
|
||||||
} catch (fallbackError) {
|
|
||||||
console.log('❌ Ошибка подключения с аутентификацией:', fallbackError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если не получилось, пробуем без аутентификации
|
|
||||||
try {
|
|
||||||
console.log('\n📡 Подключение к MongoDB (без аутентификации)...');
|
|
||||||
await mongoose.connect(primaryUri, { useNewUrlParser: true, useUnifiedTopology: true });
|
|
||||||
console.log('✅ Подключено к MongoDB');
|
|
||||||
} catch (primaryError) {
|
|
||||||
console.error('❌ Не удалось подключиться к MongoDB:', primaryError.message);
|
|
||||||
throw primaryError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('⏳ Ожидание подключения к MongoDB...');
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
if (mongoose.connection.readyState === 1) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
mongoose.connection.once('connected', resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('✅ Подключено к MongoDB');
|
||||||
};
|
};
|
||||||
|
|
||||||
const recreateTestUser = async () => {
|
const recreateTestUser = async () => {
|
||||||
try {
|
try {
|
||||||
await connectWithFallback();
|
await ensureConnection();
|
||||||
|
|
||||||
const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796');
|
const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796');
|
||||||
const presetUserEmail = 'admin@test-company.ru';
|
const presetUserEmail = 'admin@test-company.ru';
|
||||||
|
|
||||||
|
const presetCompanyId2 = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06797');
|
||||||
|
const presetUserEmail2 = 'manager@partner-company.ru';
|
||||||
|
|
||||||
// Удалить старого тестового пользователя
|
// Удалить старых тестовых пользователей
|
||||||
console.log('🗑️ Удаление старого тестового пользователя...');
|
console.log('🗑️ Удаление старых тестовых пользователей...');
|
||||||
const oldUser = await User.findOne({ email: presetUserEmail });
|
const testEmails = [presetUserEmail, presetUserEmail2];
|
||||||
if (oldUser) {
|
|
||||||
// Удалить связанную компанию
|
for (const email of testEmails) {
|
||||||
if (oldUser.companyId) {
|
const oldUser = await User.findOne({ email });
|
||||||
await Company.findByIdAndDelete(oldUser.companyId);
|
if (oldUser) {
|
||||||
console.log(' ✓ Старая компания удалена');
|
// Удалить связанную компанию
|
||||||
|
if (oldUser.companyId) {
|
||||||
|
await Company.findByIdAndDelete(oldUser.companyId);
|
||||||
|
console.log(` ✓ Старая компания для ${email} удалена`);
|
||||||
|
}
|
||||||
|
await User.findByIdAndDelete(oldUser._id);
|
||||||
|
console.log(` ✓ Старый пользователь ${email} удален`);
|
||||||
|
} else {
|
||||||
|
console.log(` ℹ️ Пользователь ${email} не найден`);
|
||||||
}
|
}
|
||||||
await User.findByIdAndDelete(oldUser._id);
|
|
||||||
console.log(' ✓ Старый пользователь удален');
|
|
||||||
} else {
|
|
||||||
console.log(' ℹ️ Старый пользователь не найден');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создать новую компанию с правильной кодировкой UTF-8
|
// Создать новую компанию с правильной кодировкой UTF-8
|
||||||
@@ -82,8 +82,8 @@ const recreateTestUser = async () => {
|
|||||||
});
|
});
|
||||||
console.log(' ✓ Компания создана:', company.fullName);
|
console.log(' ✓ Компания создана:', company.fullName);
|
||||||
|
|
||||||
// Создать нового пользователя с правильной кодировкой UTF-8
|
// Создать первого пользователя с правильной кодировкой UTF-8
|
||||||
console.log('\n👤 Создание тестового пользователя...');
|
console.log('\n👤 Создание первого тестового пользователя...');
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
email: presetUserEmail,
|
email: presetUserEmail,
|
||||||
password: 'SecurePass123!',
|
password: 'SecurePass123!',
|
||||||
@@ -95,18 +95,71 @@ const recreateTestUser = async () => {
|
|||||||
});
|
});
|
||||||
console.log(' ✓ Пользователь создан:', user.firstName, user.lastName);
|
console.log(' ✓ Пользователь создан:', user.firstName, user.lastName);
|
||||||
|
|
||||||
|
// Создать вторую компанию
|
||||||
|
console.log('\n🏢 Создание второй тестовой компании...');
|
||||||
|
const company2 = await Company.create({
|
||||||
|
_id: presetCompanyId2,
|
||||||
|
fullName: 'ООО "Партнер"',
|
||||||
|
shortName: 'Партнер',
|
||||||
|
inn: '9876543210',
|
||||||
|
ogrn: '1089876543210',
|
||||||
|
legalForm: 'ООО',
|
||||||
|
industry: 'Торговля',
|
||||||
|
companySize: '11-50',
|
||||||
|
website: 'https://partner-company.ru',
|
||||||
|
phone: '+7 (495) 987-65-43',
|
||||||
|
email: 'info@partner-company.ru',
|
||||||
|
description: 'Надежный партнер для бизнеса',
|
||||||
|
legalAddress: 'г. Санкт-Петербург, пр. Невский, д. 100',
|
||||||
|
actualAddress: 'г. Санкт-Петербург, пр. Невский, д. 100',
|
||||||
|
foundedYear: 2018,
|
||||||
|
employeeCount: '11-50',
|
||||||
|
revenue: 'До 60 млн ₽',
|
||||||
|
rating: 4.3,
|
||||||
|
reviews: 5,
|
||||||
|
verified: true,
|
||||||
|
partnerGeography: ['spb', 'russia_all'],
|
||||||
|
slogan: 'Качество и надежность',
|
||||||
|
});
|
||||||
|
console.log(' ✓ Компания создана:', company2.fullName);
|
||||||
|
|
||||||
|
// Создать второго пользователя
|
||||||
|
console.log('\n👤 Создание второго тестового пользователя...');
|
||||||
|
const user2 = await User.create({
|
||||||
|
email: presetUserEmail2,
|
||||||
|
password: 'SecurePass123!',
|
||||||
|
firstName: 'Петр',
|
||||||
|
lastName: 'Петров',
|
||||||
|
position: 'Менеджер',
|
||||||
|
phone: '+7 (495) 987-65-43',
|
||||||
|
companyId: company2._id,
|
||||||
|
});
|
||||||
|
console.log(' ✓ Пользователь создан:', user2.firstName, user2.lastName);
|
||||||
|
|
||||||
// Проверка что данные сохранены правильно
|
// Проверка что данные сохранены правильно
|
||||||
console.log('\n✅ Проверка данных:');
|
console.log('\n✅ Проверка данных:');
|
||||||
|
console.log('\n Пользователь 1:');
|
||||||
console.log(' Email:', user.email);
|
console.log(' Email:', user.email);
|
||||||
console.log(' Имя:', user.firstName);
|
console.log(' Имя:', user.firstName);
|
||||||
console.log(' Фамилия:', user.lastName);
|
console.log(' Фамилия:', user.lastName);
|
||||||
console.log(' Компания:', company.fullName);
|
console.log(' Компания:', company.fullName);
|
||||||
console.log(' Должность:', user.position);
|
console.log(' Должность:', user.position);
|
||||||
|
|
||||||
|
console.log('\n Пользователь 2:');
|
||||||
|
console.log(' Email:', user2.email);
|
||||||
|
console.log(' Имя:', user2.firstName);
|
||||||
|
console.log(' Фамилия:', user2.lastName);
|
||||||
|
console.log(' Компания:', company2.fullName);
|
||||||
|
console.log(' Должность:', user2.position);
|
||||||
|
|
||||||
console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8');
|
console.log('\n✅ ГОТОВО! Тестовые пользователи созданы с правильной кодировкой UTF-8');
|
||||||
console.log('\n📋 Данные для входа:');
|
console.log('\n📋 Данные для входа:');
|
||||||
|
console.log('\n Пользователь 1:');
|
||||||
console.log(' Email: admin@test-company.ru');
|
console.log(' Email: admin@test-company.ru');
|
||||||
console.log(' Пароль: SecurePass123!');
|
console.log(' Пароль: SecurePass123!');
|
||||||
|
console.log('\n Пользователь 2:');
|
||||||
|
console.log(' Email: manager@partner-company.ru');
|
||||||
|
console.log(' Пароль: SecurePass123!');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Создать дополнительные тестовые компании для поиска
|
// Создать дополнительные тестовые компании для поиска
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
// Подключение моделей - прямые пути без path.join и __dirname
|
// Подключение моделей
|
||||||
const Activity = require('../models/Activity');
|
const Activity = require('../models/Activity');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const Company = require('../models/Company');
|
const Company = require('../models/Company');
|
||||||
|
|
||||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement-platform';
|
|
||||||
|
|
||||||
const activityTemplates = [
|
const activityTemplates = [
|
||||||
{
|
{
|
||||||
type: 'request_received',
|
type: 'request_received',
|
||||||
@@ -53,8 +51,14 @@ const activityTemplates = [
|
|||||||
|
|
||||||
async function seedActivities() {
|
async function seedActivities() {
|
||||||
try {
|
try {
|
||||||
console.log('🌱 Connecting to MongoDB...');
|
// Подключение к MongoDB происходит через server/utils/mongoose.ts
|
||||||
await mongoose.connect(MONGODB_URI);
|
console.log('🌱 Checking MongoDB connection...');
|
||||||
|
if (mongoose.connection.readyState !== 1) {
|
||||||
|
console.log('⏳ Waiting for MongoDB connection...');
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
mongoose.connection.once('connected', resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
console.log('✅ Connected to MongoDB');
|
console.log('✅ Connected to MongoDB');
|
||||||
|
|
||||||
// Найти тестового пользователя
|
// Найти тестового пользователя
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('../../../utils/mongoose');
|
||||||
const Request = require('../models/Request');
|
const Request = require('../models/Request');
|
||||||
const Company = require('../models/Company');
|
const Company = require('../models/Company');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
|
|
||||||
const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';
|
|
||||||
|
|
||||||
async function seedRequests() {
|
async function seedRequests() {
|
||||||
try {
|
try {
|
||||||
await mongoose.connect(mongoUri);
|
// Подключение к MongoDB происходит через server/utils/mongoose.ts
|
||||||
|
if (mongoose.connection.readyState !== 1) {
|
||||||
|
console.log('⏳ Waiting for MongoDB connection...');
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
mongoose.connection.once('connected', resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
console.log('✅ Connected to MongoDB');
|
console.log('✅ Connected to MongoDB');
|
||||||
|
|
||||||
// Получаем все компании
|
// Получаем все компании
|
||||||
|
|||||||
Reference in New Issue
Block a user