From 284be82e1e44a98f7ba25a35a2888edeac932554 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Wed, 5 Nov 2025 19:06:11 +0300 Subject: [PATCH] Refactor file handling in BuyProduct and Request models; implement file schema for better structure. Update routes to handle file uploads and downloads with improved error handling and logging. Adjust MongoDB connection management across scripts and routes for consistency. --- .../routers/procurement/models/BuyProduct.js | 42 ++-- server/routers/procurement/models/Request.js | 24 +-- server/routers/procurement/routes/auth.js | 31 +-- server/routers/procurement/routes/buy.js | 3 +- .../routers/procurement/routes/buyProducts.js | 160 +++++++++++--- .../routers/procurement/routes/companies.js | 3 +- .../routers/procurement/routes/experience.js | 3 +- server/routers/procurement/routes/home.js | 32 +-- server/routers/procurement/routes/messages.js | 5 +- server/routers/procurement/routes/requests.js | 202 +++++++++++++++--- server/routers/procurement/routes/search.js | 125 ++++++++++- .../procurement/scripts/migrate-messages.js | 23 +- .../procurement/scripts/recreate-test-user.js | 133 ++++++++---- .../procurement/scripts/seed-activities.js | 16 +- .../procurement/scripts/seed-requests.js | 12 +- 15 files changed, 630 insertions(+), 184 deletions(-) diff --git a/server/routers/procurement/models/BuyProduct.js b/server/routers/procurement/models/BuyProduct.js index 6828b12..24ee7e0 100644 --- a/server/routers/procurement/models/BuyProduct.js +++ b/server/routers/procurement/models/BuyProduct.js @@ -1,5 +1,34 @@ 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({ companyId: { type: String, @@ -24,18 +53,7 @@ const buyProductSchema = new mongoose.Schema({ type: String, default: 'шт' }, - files: [{ - id: String, - name: String, - url: String, - type: String, - size: Number, - storagePath: String, - uploadedAt: { - type: Date, - default: Date.now - } - }], + files: [fileSchema], acceptedBy: [{ companyId: { type: mongoose.Schema.Types.ObjectId, diff --git a/server/routers/procurement/models/Request.js b/server/routers/procurement/models/Request.js index 88f921d..6cd2412 100644 --- a/server/routers/procurement/models/Request.js +++ b/server/routers/procurement/models/Request.js @@ -22,12 +22,12 @@ const requestSchema = new mongoose.Schema({ required: true }, files: [{ - id: String, - name: String, - url: String, - type: String, - size: Number, - storagePath: String, + id: { type: String }, + name: { type: String }, + url: { type: String }, + type: { type: String }, + size: { type: Number }, + storagePath: { type: String }, uploadedAt: { type: Date, default: Date.now @@ -47,12 +47,12 @@ const requestSchema = new mongoose.Schema({ default: null }, responseFiles: [{ - id: String, - name: String, - url: String, - type: String, - size: Number, - storagePath: String, + id: { type: String }, + name: { type: String }, + url: { type: String }, + type: { type: String }, + size: { type: Number }, + storagePath: { type: String }, uploadedAt: { type: Date, default: Date.now diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index 9b5437c..ec54893 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -7,7 +7,7 @@ const Request = require('../models/Request'); const BuyProduct = require('../models/BuyProduct'); const Message = require('../models/Message'); const Review = require('../models/Review'); -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); const { Types } = mongoose; const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796'); @@ -116,9 +116,6 @@ const waitForDatabaseConnection = async () => { const verifyAuth = async () => { try { - if (!mongoose.connection.db) { - return false; - } await mongoose.connection.db.admin().command({ listDatabases: 1 }); return true; } catch (error) { @@ -139,15 +136,17 @@ const waitForDatabaseConnection = async () => { } try { - // Ожидаем подключения (подключение происходит автоматически через server/utils/mongoose.ts) - await new Promise(resolve => setTimeout(resolve, 500)); - - if (mongoose.connection.readyState === 1) { - const authed = await verifyAuth(); - if (authed) { - return; - } + const connection = await connectDB(); + if (!connection) { + break; } + + const authed = await verifyAuth(); + if (authed) { + return; + } + + await mongoose.connection.close().catch(() => {}); } catch (error) { if (!isAuthFailure(error)) { throw error; @@ -218,8 +217,12 @@ const initializeTestUser = async () => { } catch (error) { console.error('Error initializing test data:', error.message); if (error?.code === 13 || /auth/i.test(error?.message || '')) { - if (process.env.DEV === 'true') { - console.error('Auth error detected. Connection managed by server/utils/mongoose.ts'); + try { + await connectDB(); + } catch (connectError) { + if (process.env.DEV === 'true') { + console.error('Failed to re-connect after auth error:', connectError.message); + } } } } diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index da95f94..69f22f2 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -1,10 +1,11 @@ const express = require('express') const fs = require('fs') +const path = require('path') const router = express.Router() const BuyDocument = require('../models/BuyDocument') // 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)) { fs.mkdirSync(docsDir, { recursive: true }) } diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js index f480130..b844b18 100644 --- a/server/routers/procurement/routes/buyProducts.js +++ b/server/routers/procurement/routes/buyProducts.js @@ -2,9 +2,10 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const BuyProduct = require('../models/BuyProduct'); +const path = require('path'); const fs = require('fs'); 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) => { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); @@ -23,18 +24,6 @@ const ALLOWED_MIME_TYPES = new Set([ '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({ destination: (req, file, cb) => { const productId = req.params.id || 'common'; @@ -43,10 +32,12 @@ const storage = multer.diskStorage({ cb(null, productDir); }, filename: (req, file, cb) => { - const originalExtension = getExtension(file.originalname); - const baseName = getBasename(file.originalname) - .replace(/[^a-zA-Z0-9-_]+/g, '_') - .toLowerCase(); + // Исправляем кодировку имени файла из Latin1 в UTF-8 + const fixedName = Buffer.from(file.originalname, 'latin1').toString('utf8'); + const originalExtension = path.extname(fixedName) || ''; + const baseName = path + .basename(fixedName, originalExtension) + .replace(/[<>:"/\\|?*\x00-\x1F]+/g, '_'); // Убираем только недопустимые символы Windows, оставляем кириллицу 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' }); } @@ -253,28 +253,75 @@ router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res) 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 = { - id: `file-${Date.now()}`, - name: req.file.originalname, - url: `/uploads/${relativePath}`, + id: `file-${fileTimestamp}`, // Используем тот же timestamp, что и в имени файла + name: fixedFileName, + url: `/uploads/buy-products/${relativePath}`, type: req.file.mimetype, size: req.file.size, uploadedAt: new Date(), storagePath: relativePath, }; - product.files.push(file); - await product.save(); + console.log('[BuyProducts] Adding file to product:', { + 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); - res.json(product); + res.json(updatedProduct); } catch (error) { 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({ error: 'Internal server error', message: error.message, + details: error.errors || {}, }); } }); @@ -303,7 +350,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => { await product.save(); 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) => { 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; diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js index 914cd72..8da9bd5 100644 --- a/server/routers/procurement/routes/companies.js +++ b/server/routers/procurement/routes/companies.js @@ -5,7 +5,8 @@ const Company = require('../models/Company'); const Experience = require('../models/Experience'); const Request = require('../models/Request'); const Message = require('../models/Message'); -const { Types } = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); +const { Types } = mongoose; // GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id router.get('/my/info', verifyToken, async (req, res) => { diff --git a/server/routers/procurement/routes/experience.js b/server/routers/procurement/routes/experience.js index dcea942..47a2d27 100644 --- a/server/routers/procurement/routes/experience.js +++ b/server/routers/procurement/routes/experience.js @@ -2,7 +2,8 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Experience = require('../models/Experience'); -const { Types } = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); +const { Types } = mongoose; // GET /experience - Получить список опыта работы компании router.get('/', verifyToken, async (req, res) => { diff --git a/server/routers/procurement/routes/home.js b/server/routers/procurement/routes/home.js index 82c87d2..3914a31 100644 --- a/server/routers/procurement/routes/home.js +++ b/server/routers/procurement/routes/home.js @@ -21,21 +21,23 @@ router.get('/aggregates', verifyToken, async (req, res) => { const companyId = user.companyId.toString(); - const [docsCount, acceptsCount, requestsCount] = await Promise.all([ - BuyProduct.countDocuments({ companyId }), - Request.countDocuments({ - $or: [ - { senderCompanyId: companyId, status: 'accepted' }, - { recipientCompanyId: companyId, status: 'accepted' } - ] - }), - Request.countDocuments({ - $or: [ - { senderCompanyId: companyId }, - { recipientCompanyId: companyId } - ] - }) - ]); + // Получить все BuyProduct для подсчета файлов и акцептов + const buyProducts = await BuyProduct.find({ companyId }); + + // Подсчет документов - сумма всех файлов во всех BuyProduct + const docsCount = buyProducts.reduce((total, product) => { + return total + (product.files ? product.files.length : 0); + }, 0); + + // Подсчет акцептов - сумма всех acceptedBy во всех BuyProduct + const acceptsCount = buyProducts.reduce((total, product) => { + return total + (product.acceptedBy ? product.acceptedBy.length : 0); + }, 0); + + // Подсчет исходящих запросов (только отправленные этой компанией) + const requestsCount = await Request.countDocuments({ + senderCompanyId: companyId + }); res.json({ docsCount, diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js index 7573d1f..61766ac 100644 --- a/server/routers/procurement/routes/messages.js +++ b/server/routers/procurement/routes/messages.js @@ -2,6 +2,8 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Message = require('../models/Message'); +const mongoose = require('../../../utils/mongoose'); +const { ObjectId } = mongoose.Types; // Функция для логирования с проверкой DEV переменной const log = (message, data = '') => { @@ -18,7 +20,6 @@ const log = (message, data = '') => { router.get('/threads', verifyToken, async (req, res) => { try { const companyId = req.companyId; - const { ObjectId } = require('mongoose').Types; log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId); @@ -146,7 +147,6 @@ router.post('/:threadId', verifyToken, async (req, res) => { // Найти recipientCompanyId по ObjectId если нужно let recipientObjectId = recipientCompanyId; - const { ObjectId } = require('mongoose').Types; try { if (typeof recipientCompanyId === 'string' && ObjectId.isValid(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 (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) { - const { ObjectId } = require('mongoose').Types; let recipientObjectId = expectedRecipient; try { if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) { diff --git a/server/routers/procurement/routes/requests.js b/server/routers/procurement/routes/requests.js index 31d0f48..93c072b 100644 --- a/server/routers/procurement/routes/requests.js +++ b/server/routers/procurement/routes/requests.js @@ -3,8 +3,10 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Request = require('../models/Request'); const BuyProduct = require('../models/BuyProduct'); +const path = require('path'); const fs = require('fs'); const multer = require('multer'); +const mongoose = require('../../../utils/mongoose'); // Функция для логирования с проверкой DEV переменной 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) => { if (!fs.existsSync(dirPath)) { @@ -37,28 +39,17 @@ const ALLOWED_REQUEST_MIME_TYPES = new Set([ '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({ destination: (req, file, cb) => { const subfolder = req.requestUploadSubfolder || ''; - const destinationDir = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}` : REQUESTS_UPLOAD_ROOT; + const destinationDir = `${REQUESTS_UPLOAD_ROOT}/${subfolder}`; ensureDirectory(destinationDir); cb(null, destinationDir); }, filename: (req, file, cb) => { - const extension = getExtension(file.originalname); - const baseName = getBasename(file.originalname) + const extension = path.extname(file.originalname) || ''; + const baseName = path + .basename(file.originalname, extension) .replace(/[^a-zA-Z0-9-_]+/g, '_') .toLowerCase(); cb(null, `${Date.now()}_${baseName}${extension}`); @@ -107,7 +98,7 @@ const cleanupUploadedFiles = async (req) => { const subfolder = req.requestUploadSubfolder || ''; 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) => { if (error.code !== 'ENOENT') { console.error('[Requests] Failed to cleanup uploaded file:', error.message); @@ -125,7 +116,7 @@ const mapFilesToMetadata = (req) => { const subfolder = req.requestUploadSubfolder || ''; return req.files.map((file) => { - const relativePath = subfolder ? `requests/${subfolder}/${file.filename}` : `requests/${file.filename}`; + const relativePath = `requests/${subfolder}/${file.filename}`; return { id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, name: file.originalname, @@ -169,7 +160,7 @@ const removeStoredFiles = async (files = []) => { const tasks = files .filter((file) => file && file.storagePath) .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) => { if (error.code !== 'ENOENT') { 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' }); } - 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 { 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) { - 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) { - 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) { await cleanupUploadedFiles(req); return res.status(400).json({ error: 'Subject is required' }); } - const uploadedFiles = mapFilesToMetadata(req); - const results = []; for (const recipientCompanyId of recipients) { try { @@ -331,9 +359,17 @@ router.put( async (req, res) => { try { 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 statusRaw = (req.body.status || 'accepted').toLowerCase(); const status = statusRaw === 'rejected' ? 'rejected' : 'accepted'; + + console.log('[Requests] Response text:', responseText); + console.log('[Requests] Status:', status); if (req.invalidFiles && req.invalidFiles.length > 0) { await cleanupUploadedFiles(req); @@ -361,6 +397,8 @@ router.put( } 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) { await removeStoredFiles(request.responseFiles || []); @@ -372,18 +410,126 @@ router.put( request.respondedAt = 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(request); + res.json(savedRequest); } catch (error) { 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 }); } } ); +// 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 - удалить запрос router.delete('/:id', verifyToken, async (req, res) => { try { diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index 84e38e6..3f064b0 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -54,10 +54,13 @@ router.get('/recommendations', verifyToken, async (req, res) => { // GET /search - Поиск компаний router.get('/', verifyToken, async (req, res) => { try { + console.log('[Search] === NEW VERSION WITH FIXED SIZE FILTER ==='); + const { query = '', page = 1, limit = 10, + offset, // Добавляем поддержку offset для точной пагинации industries, companySize, geography, @@ -65,8 +68,12 @@ router.get('/', verifyToken, async (req, res) => { hasReviews, hasAcceptedDocs, sortBy = 'relevance', - sortOrder = 'desc' + sortOrder = 'desc', + minEmployees, // Кастомный фильтр: минимум сотрудников + maxEmployees // Кастомный фильтр: максимум сотрудников } = req.query; + + console.log('[Search] Filters:', { minEmployees, maxEmployees, companySize }); // Получить компанию пользователя, чтобы исключить её из результатов const User = require('../models/User'); @@ -135,12 +142,99 @@ router.get('/', verifyToken, async (req, res) => { } } - // Фильтр по размеру компании - if (companySize) { - const sizeList = Array.isArray(companySize) ? companySize : [companySize]; - if (sizeList.length > 0) { - filters.push({ companySize: { $in: sizeList } }); + // Функция для парсинга диапазона из строки вида "51-250" или "500+" + const parseEmployeeRange = (sizeStr) => { + if (sizeStr.includes('+')) { + const min = parseInt(sizeStr.replace('+', '')); + 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 }); } + // Применяем фильтр по размеру компании (если был задан) + 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 } : {}; - // Пагинация - const pageNum = parseInt(page) || 1; + // Пагинация - используем offset если передан, иначе вычисляем из page 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 = {}; @@ -228,3 +334,4 @@ router.get('/', verifyToken, async (req, res) => { module.exports = router; + diff --git a/server/routers/procurement/scripts/migrate-messages.js b/server/routers/procurement/scripts/migrate-messages.js index d342f44..f77ba9b 100644 --- a/server/routers/procurement/scripts/migrate-messages.js +++ b/server/routers/procurement/scripts/migrate-messages.js @@ -1,18 +1,18 @@ -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); +const { ObjectId } = mongoose.Types; const Message = require('../models/Message'); -require('dotenv').config({ path: '../../.env' }); - -const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; +require('dotenv').config(); async function migrateMessages() { try { - console.log('[Migration] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); + // Подключение к MongoDB происходит через server/utils/mongoose.ts + console.log('[Migration] Checking MongoDB connection...'); + if (mongoose.connection.readyState !== 1) { + console.log('[Migration] Waiting for MongoDB connection...'); + await new Promise((resolve) => { + mongoose.connection.once('connected', resolve); + }); + } console.log('[Migration] Connected to MongoDB'); // Найти все сообщения @@ -54,7 +54,6 @@ async function migrateMessages() { console.log(' Expected:', expectedRecipient); // Конвертируем в ObjectId если нужно - const { ObjectId } = require('mongoose').Types; let recipientObjectId = expectedRecipient; try { if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) { diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 80db85d..af2c530 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -1,57 +1,57 @@ -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); require('dotenv').config(); -// Импорт моделей - прямые пути без path.join и __dirname +// Импорт моделей const User = require('../models/User'); const Company = require('../models/Company'); const Request = require('../models/Request'); -const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; -const fallbackUri = - process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -const connectWithFallback = async () => { - // Сначала пробуем FALLBACK (с аутентификацией) - try { - console.log('\n📡 Подключение к MongoDB (с аутентификацией)...'); - await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true }); - console.log('✅ Подключено к MongoDB'); +// Подключение к MongoDB происходит через server/utils/mongoose.ts +// Проверяем, подключено ли уже +const ensureConnection = async () => { + if (mongoose.connection.readyState === 1) { + console.log('✅ MongoDB уже подключено'); 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 () => { try { - await connectWithFallback(); + await ensureConnection(); const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796'); const presetUserEmail = 'admin@test-company.ru'; + + const presetCompanyId2 = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06797'); + const presetUserEmail2 = 'manager@partner-company.ru'; - // Удалить старого тестового пользователя - console.log('🗑️ Удаление старого тестового пользователя...'); - const oldUser = await User.findOne({ email: presetUserEmail }); - if (oldUser) { - // Удалить связанную компанию - if (oldUser.companyId) { - await Company.findByIdAndDelete(oldUser.companyId); - console.log(' ✓ Старая компания удалена'); + // Удалить старых тестовых пользователей + console.log('🗑️ Удаление старых тестовых пользователей...'); + const testEmails = [presetUserEmail, presetUserEmail2]; + + for (const email of testEmails) { + const oldUser = await User.findOne({ email }); + if (oldUser) { + // Удалить связанную компанию + 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 @@ -82,8 +82,8 @@ const recreateTestUser = async () => { }); console.log(' ✓ Компания создана:', company.fullName); - // Создать нового пользователя с правильной кодировкой UTF-8 - console.log('\n👤 Создание тестового пользователя...'); + // Создать первого пользователя с правильной кодировкой UTF-8 + console.log('\n👤 Создание первого тестового пользователя...'); const user = await User.create({ email: presetUserEmail, password: 'SecurePass123!', @@ -95,18 +95,71 @@ const recreateTestUser = async () => { }); 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 Пользователь 1:'); console.log(' Email:', user.email); console.log(' Имя:', user.firstName); console.log(' Фамилия:', user.lastName); console.log(' Компания:', company.fullName); 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 Пользователь 1:'); console.log(' Email: admin@test-company.ru'); console.log(' Пароль: SecurePass123!'); + console.log('\n Пользователь 2:'); + console.log(' Email: manager@partner-company.ru'); + console.log(' Пароль: SecurePass123!'); console.log(''); // Создать дополнительные тестовые компании для поиска diff --git a/server/routers/procurement/scripts/seed-activities.js b/server/routers/procurement/scripts/seed-activities.js index 9490f28..7e6e949 100644 --- a/server/routers/procurement/scripts/seed-activities.js +++ b/server/routers/procurement/scripts/seed-activities.js @@ -1,13 +1,11 @@ -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); require('dotenv').config(); -// Подключение моделей - прямые пути без path.join и __dirname +// Подключение моделей const Activity = require('../models/Activity'); const User = require('../models/User'); const Company = require('../models/Company'); -const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement-platform'; - const activityTemplates = [ { type: 'request_received', @@ -53,8 +51,14 @@ const activityTemplates = [ async function seedActivities() { try { - console.log('🌱 Connecting to MongoDB...'); - await mongoose.connect(MONGODB_URI); + // Подключение к MongoDB происходит через server/utils/mongoose.ts + 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'); // Найти тестового пользователя diff --git a/server/routers/procurement/scripts/seed-requests.js b/server/routers/procurement/scripts/seed-requests.js index f435260..59b4883 100644 --- a/server/routers/procurement/scripts/seed-requests.js +++ b/server/routers/procurement/scripts/seed-requests.js @@ -1,13 +1,17 @@ -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); const Request = require('../models/Request'); const Company = require('../models/Company'); const User = require('../models/User'); -const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - async function seedRequests() { 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'); // Получаем все компании