замечания 3

This commit is contained in:
2025-11-02 12:40:42 +03:00
parent 35493a09b5
commit 0d1dcf21c1
29 changed files with 1498 additions and 1827 deletions

View File

@@ -2,6 +2,10 @@ const express = require('express');
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');
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
@@ -14,10 +18,166 @@ const log = (message, data = '') => {
}
};
const REQUESTS_UPLOAD_ROOT = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', 'requests');
const ensureDirectory = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
ensureDirectory(REQUESTS_UPLOAD_ROOT);
const MAX_REQUEST_FILE_SIZE = 20 * 1024 * 1024; // 20MB
const ALLOWED_REQUEST_MIME_TYPES = new Set([
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
]);
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const subfolder = req.requestUploadSubfolder || '';
const destinationDir = path.join(REQUESTS_UPLOAD_ROOT, subfolder);
ensureDirectory(destinationDir);
cb(null, destinationDir);
},
filename: (req, file, cb) => {
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}`);
},
});
const upload = multer({
storage,
limits: {
fileSize: MAX_REQUEST_FILE_SIZE,
},
fileFilter: (req, file, cb) => {
if (ALLOWED_REQUEST_MIME_TYPES.has(file.mimetype)) {
cb(null, true);
return;
}
if (!req.invalidFiles) {
req.invalidFiles = [];
}
req.invalidFiles.push(file.originalname);
cb(null, false);
},
});
const handleFilesUpload = (fieldName, subfolderResolver, maxCount = 10) => (req, res, next) => {
req.invalidFiles = [];
req.requestUploadSubfolder = subfolderResolver(req);
upload.array(fieldName, maxCount)(req, res, (err) => {
if (err) {
console.error('[Requests] Multer error:', err.message);
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File is too large. Maximum size is 20MB.' });
}
return res.status(400).json({ error: err.message });
}
next();
});
};
const cleanupUploadedFiles = async (req) => {
if (!Array.isArray(req.files) || req.files.length === 0) {
return;
}
const subfolder = req.requestUploadSubfolder || '';
const removalTasks = req.files.map((file) => {
const filePath = path.join(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);
}
});
});
await Promise.all(removalTasks);
};
const mapFilesToMetadata = (req) => {
if (!Array.isArray(req.files) || req.files.length === 0) {
return [];
}
const subfolder = req.requestUploadSubfolder || '';
return req.files.map((file) => {
const relativePath = path.join('requests', subfolder, file.filename).replace(/\\/g, '/');
return {
id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: file.originalname,
url: `/uploads/${relativePath}`,
type: file.mimetype,
size: file.size,
uploadedAt: new Date(),
storagePath: relativePath,
};
});
};
const normalizeToArray = (value) => {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value;
}
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed;
}
} catch (error) {
// ignore JSON parse errors
}
return String(value)
.split(',')
.map((item) => item.trim())
.filter(Boolean);
};
const removeStoredFiles = async (files = []) => {
if (!files || files.length === 0) {
return;
}
const tasks = files
.filter((file) => file && file.storagePath)
.map((file) => {
const absolutePath = path.join(__dirname, '..', '..', '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);
}
});
});
await Promise.all(tasks);
};
// GET /requests/sent - получить отправленные запросы
router.get('/sent', verifyToken, async (req, res) => {
try {
const companyId = req.user.companyId;
const companyId = req.companyId;
if (!companyId) {
return res.status(400).json({ error: 'Company ID is required' });
}
const requests = await Request.find({ senderCompanyId: companyId })
.sort({ createdAt: -1 })
@@ -35,7 +195,11 @@ router.get('/sent', verifyToken, async (req, res) => {
// GET /requests/received - получить полученные запросы
router.get('/received', verifyToken, async (req, res) => {
try {
const companyId = req.user.companyId;
const companyId = req.companyId;
if (!companyId) {
return res.status(400).json({ error: 'Company ID is required' });
}
const requests = await Request.find({ recipientCompanyId: companyId })
.sort({ createdAt: -1 })
@@ -51,95 +215,164 @@ router.get('/received', verifyToken, async (req, res) => {
});
// POST /requests - создать запрос
router.post('/', verifyToken, async (req, res) => {
try {
const { text, recipientCompanyIds, productId, files } = req.body;
const senderCompanyId = req.user.companyId;
router.post(
'/',
verifyToken,
handleFilesUpload('files', (req) => path.join('sent', (req.companyId || 'unknown').toString()), 10),
async (req, res) => {
try {
const senderCompanyId = req.companyId;
const recipients = normalizeToArray(req.body.recipientCompanyIds);
const text = (req.body.text || '').trim();
const productId = req.body.productId ? String(req.body.productId) : null;
let subject = (req.body.subject || '').trim();
if (!text || !recipientCompanyIds || !Array.isArray(recipientCompanyIds) || recipientCompanyIds.length === 0) {
return res.status(400).json({ error: 'text and recipientCompanyIds array required' });
}
// Отправить запрос каждой компании
const results = [];
for (const recipientCompanyId of recipientCompanyIds) {
try {
const request = new Request({
senderCompanyId,
recipientCompanyId,
text,
productId,
files: files || [],
status: 'pending'
});
await request.save();
results.push({
companyId: recipientCompanyId,
success: true,
message: 'Request sent successfully'
});
log('[Requests] Request sent to company:', recipientCompanyId);
} catch (err) {
results.push({
companyId: recipientCompanyId,
success: false,
message: err.message
if (req.invalidFiles && req.invalidFiles.length > 0) {
await cleanupUploadedFiles(req);
return res.status(400).json({
error: 'Unsupported file type. Allowed formats: PDF, DOC, DOCX, XLS, XLSX, CSV.',
details: req.invalidFiles,
});
}
if (!text) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Request text is required' });
}
if (!recipients.length) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'At least one recipient is required' });
}
if (!subject && productId) {
try {
const product = await BuyProduct.findById(productId);
if (product) {
subject = product.name;
}
} catch (lookupError) {
console.error('[Requests] Failed to lookup product for subject:', lookupError.message);
}
}
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 {
const request = new Request({
senderCompanyId,
recipientCompanyId,
text,
productId,
subject,
files: uploadedFiles,
responseFiles: [],
status: 'pending',
});
await request.save();
results.push({
companyId: recipientCompanyId,
success: true,
message: 'Request sent successfully',
});
log('[Requests] Request sent to company:', recipientCompanyId);
} catch (err) {
console.error('[Requests] Error storing request for company:', recipientCompanyId, err.message);
results.push({
companyId: recipientCompanyId,
success: false,
message: err.message,
});
}
}
const createdAt = new Date();
res.status(201).json({
id: 'bulk-' + Date.now(),
text,
subject,
productId,
files: uploadedFiles,
result: results,
createdAt,
});
} catch (error) {
console.error('[Requests] Error creating request:', error.message);
res.status(500).json({ error: error.message });
}
// Сохранить отчет
const report = {
text,
result: results,
createdAt: new Date()
};
res.status(201).json({
id: 'bulk-' + Date.now(),
...report,
files: files || []
});
} catch (error) {
console.error('[Requests] Error creating request:', error.message);
res.status(500).json({ error: error.message });
}
});
);
// PUT /requests/:id - ответить на запрос
router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const { response, status } = req.body;
router.put(
'/:id',
verifyToken,
handleFilesUpload('responseFiles', (req) => path.join('responses', req.params.id || 'unknown'), 5),
async (req, res) => {
try {
const { id } = req.params;
const responseText = (req.body.response || '').trim();
const statusRaw = (req.body.status || 'accepted').toLowerCase();
const status = statusRaw === 'rejected' ? 'rejected' : 'accepted';
const request = await Request.findById(id);
if (req.invalidFiles && req.invalidFiles.length > 0) {
await cleanupUploadedFiles(req);
return res.status(400).json({
error: 'Unsupported file type. Allowed formats: PDF, DOC, DOCX, XLS, XLSX, CSV.',
details: req.invalidFiles,
});
}
if (!request) {
return res.status(404).json({ error: 'Request not found' });
if (!responseText) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Response text is required' });
}
const request = await Request.findById(id);
if (!request) {
await cleanupUploadedFiles(req);
return res.status(404).json({ error: 'Request not found' });
}
if (request.recipientCompanyId !== req.companyId) {
await cleanupUploadedFiles(req);
return res.status(403).json({ error: 'Not authorized' });
}
const uploadedResponseFiles = mapFilesToMetadata(req);
if (uploadedResponseFiles.length > 0) {
await removeStoredFiles(request.responseFiles || []);
request.responseFiles = uploadedResponseFiles;
}
request.response = responseText;
request.status = status;
request.respondedAt = new Date();
request.updatedAt = new Date();
await request.save();
log('[Requests] Request responded:', id);
res.json(request);
} catch (error) {
console.error('[Requests] Error responding to request:', error.message);
res.status(500).json({ error: error.message });
}
// Только получатель может ответить на запрос
if (request.recipientCompanyId !== req.user.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
request.response = response;
request.status = status || 'accepted';
request.respondedAt = new Date();
request.updatedAt = new Date();
await request.save();
log('[Requests] Request responded:', id);
res.json(request);
} catch (error) {
console.error('[Requests] Error responding to request:', error.message);
res.status(500).json({ error: error.message });
}
});
);
// DELETE /requests/:id - удалить запрос
router.delete('/:id', verifyToken, async (req, res) => {
@@ -153,10 +386,13 @@ router.delete('/:id', verifyToken, async (req, res) => {
}
// Может удалить отправитель или получатель
if (request.senderCompanyId !== req.user.companyId && request.recipientCompanyId !== req.user.companyId) {
if (request.senderCompanyId !== req.companyId && request.recipientCompanyId !== req.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
await removeStoredFiles(request.files || []);
await removeStoredFiles(request.responseFiles || []);
await Request.findByIdAndDelete(id);
log('[Requests] Request deleted:', id);