замечания 3
This commit is contained in:
@@ -1,8 +1,101 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { generateToken } = require('../middleware/auth');
|
||||
const { generateToken, verifyToken } = require('../middleware/auth');
|
||||
const User = require('../models/User');
|
||||
const Company = require('../models/Company');
|
||||
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 { Types } = mongoose;
|
||||
const connectDB = require('../config/db');
|
||||
|
||||
const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796');
|
||||
const PRESET_USER_EMAIL = 'admin@test-company.ru';
|
||||
|
||||
const changePasswordFlow = async (userId, currentPassword, newPassword) => {
|
||||
if (!currentPassword || !newPassword) {
|
||||
return { status: 400, body: { error: 'Current password and new password are required' } };
|
||||
}
|
||||
|
||||
if (typeof newPassword !== 'string' || newPassword.trim().length < 8) {
|
||||
return { status: 400, body: { error: 'New password must be at least 8 characters long' } };
|
||||
}
|
||||
|
||||
const user = await User.findById(userId);
|
||||
|
||||
if (!user) {
|
||||
return { status: 404, body: { error: 'User not found' } };
|
||||
}
|
||||
|
||||
const isMatch = await user.comparePassword(currentPassword);
|
||||
|
||||
if (!isMatch) {
|
||||
return { status: 400, body: { error: 'Current password is incorrect' } };
|
||||
}
|
||||
|
||||
user.password = newPassword;
|
||||
user.updatedAt = new Date();
|
||||
await user.save();
|
||||
|
||||
return { status: 200, body: { message: 'Password updated successfully' } };
|
||||
};
|
||||
|
||||
const deleteAccountFlow = async (userId, password) => {
|
||||
if (!password) {
|
||||
return { status: 400, body: { error: 'Password is required to delete account' } };
|
||||
}
|
||||
|
||||
const user = await User.findById(userId);
|
||||
|
||||
if (!user) {
|
||||
return { status: 404, body: { error: 'User not found' } };
|
||||
}
|
||||
|
||||
const validPassword = await user.comparePassword(password);
|
||||
|
||||
if (!validPassword) {
|
||||
return { status: 400, body: { error: 'Password is incorrect' } };
|
||||
}
|
||||
|
||||
const companyId = user.companyId ? user.companyId.toString() : null;
|
||||
const companyObjectId = companyId && Types.ObjectId.isValid(companyId) ? new Types.ObjectId(companyId) : null;
|
||||
|
||||
const cleanupTasks = [];
|
||||
|
||||
if (companyId) {
|
||||
cleanupTasks.push(Request.deleteMany({
|
||||
$or: [{ senderCompanyId: companyId }, { recipientCompanyId: companyId }],
|
||||
}));
|
||||
|
||||
cleanupTasks.push(BuyProduct.deleteMany({ companyId }));
|
||||
|
||||
if (companyObjectId) {
|
||||
cleanupTasks.push(Message.deleteMany({
|
||||
$or: [
|
||||
{ senderCompanyId: companyObjectId },
|
||||
{ recipientCompanyId: companyObjectId },
|
||||
],
|
||||
}));
|
||||
|
||||
cleanupTasks.push(Review.deleteMany({
|
||||
$or: [
|
||||
{ companyId: companyObjectId },
|
||||
{ authorCompanyId: companyObjectId },
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
cleanupTasks.push(Company.findByIdAndDelete(companyId));
|
||||
}
|
||||
|
||||
cleanupTasks.push(User.findByIdAndDelete(user._id));
|
||||
|
||||
await Promise.all(cleanupTasks);
|
||||
|
||||
return { status: 200, body: { message: 'Account deleted successfully' } };
|
||||
};
|
||||
|
||||
// Функция для логирования с проверкой DEV переменной
|
||||
const log = (message, data = '') => {
|
||||
@@ -15,16 +108,65 @@ const log = (message, data = '') => {
|
||||
}
|
||||
};
|
||||
|
||||
// In-memory storage для логирования
|
||||
let users = [];
|
||||
const waitForDatabaseConnection = async () => {
|
||||
const isAuthFailure = (error) => {
|
||||
if (!error) return false;
|
||||
if (error.code === 13 || error.code === 18) return true;
|
||||
return /auth/i.test(String(error.message || ''));
|
||||
};
|
||||
|
||||
const verifyAuth = async () => {
|
||||
try {
|
||||
await mongoose.connection.db.admin().command({ listDatabases: 1 });
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isAuthFailure(error)) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
if (mongoose.connection.readyState === 1) {
|
||||
const authed = await verifyAuth();
|
||||
if (authed) {
|
||||
return;
|
||||
}
|
||||
await mongoose.connection.close().catch(() => {});
|
||||
}
|
||||
|
||||
try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unable to authenticate with MongoDB');
|
||||
};
|
||||
|
||||
// Инициализация тестового пользователя
|
||||
const initializeTestUser = async () => {
|
||||
try {
|
||||
const existingUser = await User.findOne({ email: 'admin@test-company.ru' });
|
||||
if (!existingUser) {
|
||||
// Создать компанию
|
||||
const company = await Company.create({
|
||||
await waitForDatabaseConnection();
|
||||
|
||||
let company = await Company.findById(PRESET_COMPANY_ID);
|
||||
if (!company) {
|
||||
company = await Company.create({
|
||||
_id: PRESET_COMPANY_ID,
|
||||
fullName: 'ООО "Тестовая Компания"',
|
||||
shortName: 'ООО "Тест"',
|
||||
inn: '7707083893',
|
||||
@@ -39,131 +181,61 @@ const initializeTestUser = async () => {
|
||||
description: 'Ведущая компания в области производства',
|
||||
slogan: 'Качество и инновация'
|
||||
});
|
||||
log('✅ Test company initialized');
|
||||
} else {
|
||||
await Company.updateOne(
|
||||
{ _id: PRESET_COMPANY_ID },
|
||||
{
|
||||
$set: {
|
||||
fullName: 'ООО "Тестовая Компания"',
|
||||
shortName: 'ООО "Тест"',
|
||||
industry: 'Производство',
|
||||
companySize: '50-100',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://test-company.ru',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Создать пользователя
|
||||
const user = await User.create({
|
||||
email: 'admin@test-company.ru',
|
||||
let existingUser = await User.findOne({ email: PRESET_USER_EMAIL });
|
||||
if (!existingUser) {
|
||||
existingUser = await User.create({
|
||||
email: PRESET_USER_EMAIL,
|
||||
password: 'SecurePass123!',
|
||||
firstName: 'Иван',
|
||||
lastName: 'Петров',
|
||||
position: 'Генеральный директор',
|
||||
companyId: company._id
|
||||
companyId: PRESET_COMPANY_ID
|
||||
});
|
||||
|
||||
log('✅ Test user initialized');
|
||||
}
|
||||
|
||||
// Инициализация других тестовых компаний
|
||||
const mockCompanies = [
|
||||
{
|
||||
fullName: 'ООО "СтройКомплект"',
|
||||
shortName: 'ООО "СтройКомплект"',
|
||||
inn: '7707083894',
|
||||
ogrn: '1027700132196',
|
||||
legalForm: 'ООО',
|
||||
industry: 'Строительство',
|
||||
companySize: '51-250',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://stroykomplekt.ru',
|
||||
verified: true,
|
||||
rating: 4.8,
|
||||
description: 'Компания строит будущее вместе',
|
||||
slogan: 'Строим будущее вместе'
|
||||
},
|
||||
{
|
||||
fullName: 'АО "Московский Строй"',
|
||||
shortName: 'АО "Московский Строй"',
|
||||
inn: '7707083895',
|
||||
ogrn: '1027700132197',
|
||||
legalForm: 'АО',
|
||||
industry: 'Строительство',
|
||||
companySize: '500+',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://moscow-stroy.ru',
|
||||
verified: true,
|
||||
rating: 4.9,
|
||||
description: 'Качество и надежность с 1995 года',
|
||||
slogan: 'Качество и надежность'
|
||||
},
|
||||
{
|
||||
fullName: 'ООО "ТеxПроект"',
|
||||
shortName: 'ООО "ТеxПроект"',
|
||||
inn: '7707083896',
|
||||
ogrn: '1027700132198',
|
||||
legalForm: 'ООО',
|
||||
industry: 'IT',
|
||||
companySize: '11-50',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://techproject.ru',
|
||||
verified: true,
|
||||
rating: 4.6,
|
||||
description: 'Решения в области информационных технологий',
|
||||
slogan: 'Технологии для бизнеса'
|
||||
},
|
||||
{
|
||||
fullName: 'ООО "ТоргПартнер"',
|
||||
shortName: 'ООО "ТоргПартнер"',
|
||||
inn: '7707083897',
|
||||
ogrn: '1027700132199',
|
||||
legalForm: 'ООО',
|
||||
industry: 'Оптовая торговля',
|
||||
companySize: '51-250',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://torgpartner.ru',
|
||||
verified: true,
|
||||
rating: 4.3,
|
||||
description: 'Оптовые поставки и логистика',
|
||||
slogan: 'Надежный партнер в торговле'
|
||||
},
|
||||
{
|
||||
fullName: 'ООО "ЭнергоПлюс"',
|
||||
shortName: 'ООО "ЭнергоПлюс"',
|
||||
inn: '7707083898',
|
||||
ogrn: '1027700132200',
|
||||
legalForm: 'ООО',
|
||||
industry: 'Энергетика',
|
||||
companySize: '251-500',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://energoplus.ru',
|
||||
verified: true,
|
||||
rating: 4.7,
|
||||
description: 'Энергетические решения и консалтинг',
|
||||
slogan: 'Энергия для развития'
|
||||
}
|
||||
];
|
||||
|
||||
for (const mockCompanyData of mockCompanies) {
|
||||
try {
|
||||
const existingCompany = await Company.findOne({ inn: mockCompanyData.inn });
|
||||
if (!existingCompany) {
|
||||
await Company.create(mockCompanyData);
|
||||
log(`✅ Mock company created: ${mockCompanyData.fullName}`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors for mock company creation - это может быть ошибка аутентификации
|
||||
log(`ℹ️ Mock company init failed: ${mockCompanyData.fullName}`);
|
||||
}
|
||||
} else if (!existingUser.companyId || existingUser.companyId.toString() !== PRESET_COMPANY_ID.toString()) {
|
||||
existingUser.companyId = PRESET_COMPANY_ID;
|
||||
existingUser.updatedAt = new Date();
|
||||
await existingUser.save();
|
||||
log('ℹ️ Test user company reference was fixed');
|
||||
}
|
||||
} catch (error) {
|
||||
// Ошибка аутентификации или другие ошибки БД - продолжаем работу
|
||||
if (error.message && error.message.includes('authentication')) {
|
||||
log('ℹ️ Database authentication required - test data initialization deferred');
|
||||
} else {
|
||||
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 || '')) {
|
||||
try {
|
||||
await connectDB();
|
||||
} catch (connectError) {
|
||||
if (process.env.DEV === 'true') {
|
||||
console.error('Failed to re-connect after auth error:', connectError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Пытаемся инициализировать с задержкой (даёт время на подключение)
|
||||
setTimeout(() => {
|
||||
initializeTestUser().catch(err => {
|
||||
log(`⚠️ Deferred test data initialization failed: ${err.message}`);
|
||||
});
|
||||
}, 2000);
|
||||
initializeTestUser();
|
||||
|
||||
// Регистрация
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
await waitForDatabaseConnection();
|
||||
|
||||
const { email, password, firstName, lastName, position, phone, fullName, inn, ogrn, legalForm, industry, companySize, website } = req.body;
|
||||
|
||||
// Проверка обязательных полей
|
||||
@@ -250,6 +322,14 @@ router.post('/register', async (req, res) => {
|
||||
// Вход
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
if (process.env.DEV === 'true') {
|
||||
console.log('[Auth] /login called');
|
||||
}
|
||||
await waitForDatabaseConnection();
|
||||
if (process.env.DEV === 'true') {
|
||||
console.log('[Auth] DB ready, running login query');
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
@@ -266,104 +346,54 @@ router.post('/login', async (req, res) => {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Инициализация других тестовых компаний
|
||||
const mockCompanies = [
|
||||
{
|
||||
fullName: 'ООО "СтройКомплект"',
|
||||
shortName: 'ООО "СтройКомплект"',
|
||||
inn: '7707083894',
|
||||
ogrn: '1027700132196',
|
||||
legalForm: 'ООО',
|
||||
industry: 'Строительство',
|
||||
companySize: '51-250',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://stroykomplekt.ru',
|
||||
verified: true,
|
||||
rating: 4.8,
|
||||
description: 'Компания строит будущее вместе',
|
||||
slogan: 'Строим будущее вместе'
|
||||
},
|
||||
{
|
||||
fullName: 'АО "Московский Строй"',
|
||||
shortName: 'АО "Московский Строй"',
|
||||
inn: '7707083895',
|
||||
ogrn: '1027700132197',
|
||||
legalForm: 'АО',
|
||||
industry: 'Строительство',
|
||||
companySize: '500+',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://moscow-stroy.ru',
|
||||
verified: true,
|
||||
rating: 4.9,
|
||||
description: 'Качество и надежность с 1995 года',
|
||||
slogan: 'Качество и надежность'
|
||||
},
|
||||
{
|
||||
fullName: 'ООО "ТеxПроект"',
|
||||
shortName: 'ООО "ТеxПроект"',
|
||||
inn: '7707083896',
|
||||
ogrn: '1027700132198',
|
||||
legalForm: 'ООО',
|
||||
industry: 'IT',
|
||||
companySize: '11-50',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://techproject.ru',
|
||||
verified: true,
|
||||
rating: 4.6,
|
||||
description: 'Решения в области информационных технологий',
|
||||
slogan: 'Технологии для бизнеса'
|
||||
},
|
||||
{
|
||||
fullName: 'ООО "ТоргПартнер"',
|
||||
shortName: 'ООО "ТоргПартнер"',
|
||||
inn: '7707083897',
|
||||
ogrn: '1027700132199',
|
||||
legalForm: 'ООО',
|
||||
industry: 'Оптовая торговля',
|
||||
companySize: '51-250',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://torgpartner.ru',
|
||||
verified: true,
|
||||
rating: 4.3,
|
||||
description: 'Оптовые поставки и логистика',
|
||||
slogan: 'Надежный партнер в торговле'
|
||||
},
|
||||
{
|
||||
fullName: 'ООО "ЭнергоПлюс"',
|
||||
shortName: 'ООО "ЭнергоПлюс"',
|
||||
inn: '7707083898',
|
||||
ogrn: '1027700132200',
|
||||
legalForm: 'ООО',
|
||||
industry: 'Энергетика',
|
||||
companySize: '251-500',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://energoplus.ru',
|
||||
verified: true,
|
||||
rating: 4.7,
|
||||
description: 'Энергетические решения и консалтинг',
|
||||
slogan: 'Энергия для развития'
|
||||
}
|
||||
];
|
||||
|
||||
for (const mockCompanyData of mockCompanies) {
|
||||
try {
|
||||
const existingCompany = await Company.findOne({ inn: mockCompanyData.inn });
|
||||
if (!existingCompany) {
|
||||
await Company.create(mockCompanyData);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors for mock company creation
|
||||
}
|
||||
if (
|
||||
user.email === PRESET_USER_EMAIL &&
|
||||
(!user.companyId || user.companyId.toString() !== PRESET_COMPANY_ID.toString())
|
||||
) {
|
||||
await User.updateOne(
|
||||
{ _id: user._id },
|
||||
{ $set: { companyId: PRESET_COMPANY_ID, updatedAt: new Date() } }
|
||||
);
|
||||
user.companyId = PRESET_COMPANY_ID;
|
||||
}
|
||||
|
||||
// Получить компанию до использования в generateToken
|
||||
let companyData = null;
|
||||
try {
|
||||
companyData = await Company.findById(user.companyId);
|
||||
companyData = user.companyId ? await Company.findById(user.companyId) : null;
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch company:', err.message);
|
||||
}
|
||||
|
||||
if (user.email === PRESET_USER_EMAIL) {
|
||||
try {
|
||||
companyData = await Company.findByIdAndUpdate(
|
||||
PRESET_COMPANY_ID,
|
||||
{
|
||||
$set: {
|
||||
fullName: 'ООО "Тестовая Компания"',
|
||||
shortName: 'ООО "Тест"',
|
||||
inn: '7707083893',
|
||||
ogrn: '1027700132195',
|
||||
legalForm: 'ООО',
|
||||
industry: 'Производство',
|
||||
companySize: '50-100',
|
||||
partnerGeography: ['moscow', 'russia_all'],
|
||||
website: 'https://test-company.ru',
|
||||
verified: true,
|
||||
rating: 4.5,
|
||||
description: 'Ведущая компания в области производства',
|
||||
slogan: 'Качество и инновация',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to ensure preset company:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
const token = generateToken(user._id.toString(), user.companyId.toString(), user.firstName, user.lastName, companyData?.fullName || 'Company');
|
||||
log('✅ Token generated for user:', user._id);
|
||||
|
||||
@@ -388,14 +418,56 @@ router.post('/login', async (req, res) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ error: `LOGIN_ERROR: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Смена пароля
|
||||
router.post('/change-password', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body || {};
|
||||
const result = await changePasswordFlow(req.userId, currentPassword, newPassword);
|
||||
res.status(result.status).json(result.body);
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Обновить профиль
|
||||
router.patch('/profile', (req, res) => {
|
||||
// требует авторизации, добавить middleware
|
||||
res.json({ message: 'Update profile endpoint' });
|
||||
// Удаление аккаунта
|
||||
router.delete('/account', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { password } = req.body || {};
|
||||
const result = await deleteAccountFlow(req.userId, password);
|
||||
res.status(result.status).json(result.body);
|
||||
} catch (error) {
|
||||
console.error('Delete account error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Обновить профиль / универсальные действия
|
||||
router.patch('/profile', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const rawAction = req.body?.action || req.query?.action || req.body?.type;
|
||||
const payload = req.body?.payload || req.body || {};
|
||||
const action = typeof rawAction === 'string' ? rawAction : '';
|
||||
|
||||
if (action === 'changePassword') {
|
||||
const result = await changePasswordFlow(req.userId, payload.currentPassword, payload.newPassword);
|
||||
return res.status(result.status).json(result.body);
|
||||
}
|
||||
|
||||
if (action === 'deleteAccount') {
|
||||
const result = await deleteAccountFlow(req.userId, payload.password);
|
||||
return res.status(result.status).json(result.body);
|
||||
}
|
||||
|
||||
res.json({ message: 'Profile endpoint' });
|
||||
} catch (error) {
|
||||
console.error('Profile update error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 = path.join(__dirname, '../../remote-assets/docs')
|
||||
@@ -9,155 +10,189 @@ if (!fs.existsSync(docsDir)) {
|
||||
fs.mkdirSync(docsDir, { recursive: true })
|
||||
}
|
||||
|
||||
// In-memory store for documents metadata
|
||||
const buyDocs = []
|
||||
|
||||
function generateId() {
|
||||
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
// GET /buy/docs?ownerCompanyId=...
|
||||
router.get('/docs', (req, res) => {
|
||||
const { ownerCompanyId } = req.query
|
||||
console.log('[BUY API] GET /docs', { ownerCompanyId, totalDocs: buyDocs.length })
|
||||
let result = buyDocs
|
||||
if (ownerCompanyId) {
|
||||
result = result.filter((d) => d.ownerCompanyId === ownerCompanyId)
|
||||
router.get('/docs', async (req, res) => {
|
||||
try {
|
||||
const { ownerCompanyId } = req.query
|
||||
console.log('[BUY API] GET /docs', { ownerCompanyId })
|
||||
|
||||
let query = {}
|
||||
if (ownerCompanyId) {
|
||||
query.ownerCompanyId = ownerCompanyId
|
||||
}
|
||||
|
||||
const docs = await BuyDocument.find(query).sort({ createdAt: -1 })
|
||||
|
||||
const result = docs.map(doc => ({
|
||||
...doc.toObject(),
|
||||
url: `/api/buy/docs/${doc.id}/file`
|
||||
}))
|
||||
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.error('[BUY API] Error fetching docs:', error)
|
||||
res.status(500).json({ error: 'Failed to fetch documents' })
|
||||
}
|
||||
result = result.map(doc => ({
|
||||
...doc,
|
||||
url: `/api/buy/docs/${doc.id}/file`
|
||||
}))
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
// POST /buy/docs
|
||||
router.post('/docs', (req, res) => {
|
||||
const { ownerCompanyId, name, type, fileData } = req.body || {}
|
||||
console.log('[BUY API] POST /docs', { ownerCompanyId, name, type })
|
||||
if (!ownerCompanyId || !name || !type) {
|
||||
return res.status(400).json({ error: 'ownerCompanyId, name and type are required' })
|
||||
}
|
||||
|
||||
if (!fileData) {
|
||||
return res.status(400).json({ error: 'fileData is required' })
|
||||
}
|
||||
|
||||
const id = generateId()
|
||||
|
||||
// Save file to disk
|
||||
router.post('/docs', async (req, res) => {
|
||||
try {
|
||||
const { ownerCompanyId, name, type, fileData } = req.body || {}
|
||||
console.log('[BUY API] POST /docs', { ownerCompanyId, name, type })
|
||||
|
||||
if (!ownerCompanyId || !name || !type) {
|
||||
return res.status(400).json({ error: 'ownerCompanyId, name and type are required' })
|
||||
}
|
||||
|
||||
if (!fileData) {
|
||||
return res.status(400).json({ error: 'fileData is required' })
|
||||
}
|
||||
|
||||
const id = generateId()
|
||||
|
||||
// Save file to disk
|
||||
const binaryData = Buffer.from(fileData, 'base64')
|
||||
const filePath = path.join(docsDir, `${id}.${type}`)
|
||||
fs.writeFileSync(filePath, binaryData)
|
||||
console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`)
|
||||
|
||||
const size = binaryData.length
|
||||
const url = `/api/buy/docs/${id}/file`
|
||||
const doc = {
|
||||
|
||||
const doc = await BuyDocument.create({
|
||||
id,
|
||||
ownerCompanyId,
|
||||
name,
|
||||
type,
|
||||
size,
|
||||
url,
|
||||
filePath,
|
||||
acceptedBy: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
buyDocs.unshift(doc)
|
||||
acceptedBy: []
|
||||
})
|
||||
|
||||
console.log('[BUY API] Document created:', id)
|
||||
res.status(201).json(doc)
|
||||
|
||||
res.status(201).json({
|
||||
...doc.toObject(),
|
||||
url: `/api/buy/docs/${doc.id}/file`
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(`[BUY API] Error saving file: ${e.message}`)
|
||||
res.status(500).json({ error: 'Failed to save file' })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/docs/:id/accept', (req, res) => {
|
||||
const { id } = req.params
|
||||
const { companyId } = req.body || {}
|
||||
console.log('[BUY API] POST /docs/:id/accept', { id, companyId })
|
||||
const doc = buyDocs.find((d) => d.id === id)
|
||||
if (!doc) {
|
||||
console.log('[BUY API] Document not found:', id)
|
||||
return res.status(404).json({ error: 'Document not found' })
|
||||
router.post('/docs/:id/accept', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { companyId } = req.body || {}
|
||||
console.log('[BUY API] POST /docs/:id/accept', { id, companyId })
|
||||
|
||||
if (!companyId) {
|
||||
return res.status(400).json({ error: 'companyId is required' })
|
||||
}
|
||||
|
||||
const doc = await BuyDocument.findOne({ id })
|
||||
if (!doc) {
|
||||
console.log('[BUY API] Document not found:', id)
|
||||
return res.status(404).json({ error: 'Document not found' })
|
||||
}
|
||||
|
||||
if (!doc.acceptedBy.includes(companyId)) {
|
||||
doc.acceptedBy.push(companyId)
|
||||
await doc.save()
|
||||
}
|
||||
|
||||
res.json({ id: doc.id, acceptedBy: doc.acceptedBy })
|
||||
} catch (error) {
|
||||
console.error('[BUY API] Error accepting document:', error)
|
||||
res.status(500).json({ error: 'Failed to accept document' })
|
||||
}
|
||||
if (!companyId) {
|
||||
return res.status(400).json({ error: 'companyId is required' })
|
||||
}
|
||||
if (!doc.acceptedBy.includes(companyId)) {
|
||||
doc.acceptedBy.push(companyId)
|
||||
}
|
||||
res.json({ id: doc.id, acceptedBy: doc.acceptedBy })
|
||||
})
|
||||
|
||||
router.get('/docs/:id/delete', (req, res) => {
|
||||
const { id } = req.params
|
||||
console.log('[BUY API] GET /docs/:id/delete', { id, totalDocs: buyDocs.length })
|
||||
const index = buyDocs.findIndex((d) => d.id === id)
|
||||
if (index === -1) {
|
||||
console.log('[BUY API] Document not found for deletion:', id)
|
||||
return res.status(404).json({ error: 'Document not found' })
|
||||
}
|
||||
const deletedDoc = buyDocs.splice(index, 1)[0]
|
||||
|
||||
// Delete file from disk
|
||||
if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(deletedDoc.filePath)
|
||||
console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`)
|
||||
} catch (e) {
|
||||
console.error(`[BUY API] Error deleting file: ${e.message}`)
|
||||
router.get('/docs/:id/delete', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
console.log('[BUY API] GET /docs/:id/delete', { id })
|
||||
|
||||
const doc = await BuyDocument.findOne({ id })
|
||||
if (!doc) {
|
||||
console.log('[BUY API] Document not found for deletion:', id)
|
||||
return res.status(404).json({ error: 'Document not found' })
|
||||
}
|
||||
|
||||
// Delete file from disk
|
||||
if (doc.filePath && fs.existsSync(doc.filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(doc.filePath)
|
||||
console.log(`[BUY API] File deleted: ${doc.filePath}`)
|
||||
} catch (e) {
|
||||
console.error(`[BUY API] Error deleting file: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
await BuyDocument.deleteOne({ id })
|
||||
|
||||
console.log('[BUY API] Document deleted via GET:', id)
|
||||
res.json({ id: doc.id, success: true })
|
||||
} catch (error) {
|
||||
console.error('[BUY API] Error deleting document:', error)
|
||||
res.status(500).json({ error: 'Failed to delete document' })
|
||||
}
|
||||
|
||||
console.log('[BUY API] Document deleted via GET:', id, { remainingDocs: buyDocs.length })
|
||||
res.json({ id: deletedDoc.id, success: true })
|
||||
})
|
||||
|
||||
router.delete('/docs/:id', (req, res) => {
|
||||
const { id } = req.params
|
||||
console.log('[BUY API] DELETE /docs/:id', { id, totalDocs: buyDocs.length })
|
||||
const index = buyDocs.findIndex((d) => d.id === id)
|
||||
if (index === -1) {
|
||||
console.log('[BUY API] Document not found for deletion:', id)
|
||||
return res.status(404).json({ error: 'Document not found' })
|
||||
}
|
||||
const deletedDoc = buyDocs.splice(index, 1)[0]
|
||||
|
||||
// Delete file from disk
|
||||
if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(deletedDoc.filePath)
|
||||
console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`)
|
||||
} catch (e) {
|
||||
console.error(`[BUY API] Error deleting file: ${e.message}`)
|
||||
router.delete('/docs/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
console.log('[BUY API] DELETE /docs/:id', { id })
|
||||
|
||||
const doc = await BuyDocument.findOne({ id })
|
||||
if (!doc) {
|
||||
console.log('[BUY API] Document not found for deletion:', id)
|
||||
return res.status(404).json({ error: 'Document not found' })
|
||||
}
|
||||
|
||||
// Delete file from disk
|
||||
if (doc.filePath && fs.existsSync(doc.filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(doc.filePath)
|
||||
console.log(`[BUY API] File deleted: ${doc.filePath}`)
|
||||
} catch (e) {
|
||||
console.error(`[BUY API] Error deleting file: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
await BuyDocument.deleteOne({ id })
|
||||
|
||||
console.log('[BUY API] Document deleted:', id)
|
||||
res.json({ id: doc.id, success: true })
|
||||
} catch (error) {
|
||||
console.error('[BUY API] Error deleting document:', error)
|
||||
res.status(500).json({ error: 'Failed to delete document' })
|
||||
}
|
||||
|
||||
console.log('[BUY API] Document deleted:', id, { remainingDocs: buyDocs.length })
|
||||
res.json({ id: deletedDoc.id, success: true })
|
||||
})
|
||||
|
||||
// GET /buy/docs/:id/file - Serve the file
|
||||
router.get('/docs/:id/file', (req, res) => {
|
||||
const { id } = req.params
|
||||
console.log('[BUY API] GET /docs/:id/file', { id })
|
||||
|
||||
const doc = buyDocs.find(d => d.id === id)
|
||||
if (!doc) {
|
||||
console.log('[BUY API] Document not found:', id)
|
||||
return res.status(404).json({ error: 'Document not found' })
|
||||
}
|
||||
|
||||
const filePath = path.join(docsDir, `${id}.${doc.type}`)
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log('[BUY API] File not found on disk:', filePath)
|
||||
return res.status(404).json({ error: 'File not found on disk' })
|
||||
}
|
||||
|
||||
router.get('/docs/:id/file', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
console.log('[BUY API] GET /docs/:id/file', { id })
|
||||
|
||||
const doc = await BuyDocument.findOne({ id })
|
||||
if (!doc) {
|
||||
console.log('[BUY API] Document not found:', id)
|
||||
return res.status(404).json({ error: 'Document not found' })
|
||||
}
|
||||
|
||||
const filePath = path.join(docsDir, `${id}.${doc.type}`)
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log('[BUY API] File not found on disk:', filePath)
|
||||
return res.status(404).json({ error: 'File not found on disk' })
|
||||
}
|
||||
|
||||
const fileBuffer = fs.readFileSync(filePath)
|
||||
|
||||
const mimeTypes = {
|
||||
@@ -170,7 +205,6 @@ router.get('/docs/:id/file', (req, res) => {
|
||||
const sanitizedName = doc.name.replace(/[^\w\s\-\.]/g, '_')
|
||||
|
||||
res.setHeader('Content-Type', mimeType)
|
||||
// RFC 5987 encoding: filename for ASCII fallback, filename* for UTF-8 with percent-encoding
|
||||
const encodedFilename = encodeURIComponent(`${doc.name}.${doc.type}`)
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${sanitizedName}.${doc.type}"; filename*=UTF-8''${encodedFilename}`)
|
||||
res.setHeader('Content-Length', fileBuffer.length)
|
||||
|
||||
@@ -2,6 +2,74 @@ 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 = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', 'buy-products');
|
||||
const ensureDirectory = (dirPath) => {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
ensureDirectory(UPLOADS_ROOT);
|
||||
|
||||
const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB
|
||||
const ALLOWED_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 productId = req.params.id || 'common';
|
||||
const productDir = path.join(UPLOADS_ROOT, productId);
|
||||
ensureDirectory(productDir);
|
||||
cb(null, productDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const originalExtension = path.extname(file.originalname) || '';
|
||||
const baseName = path
|
||||
.basename(file.originalname, originalExtension)
|
||||
.replace(/[^a-zA-Z0-9-_]+/g, '_')
|
||||
.toLowerCase();
|
||||
cb(null, `${Date.now()}_${baseName}${originalExtension}`);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: {
|
||||
fileSize: MAX_FILE_SIZE,
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (ALLOWED_MIME_TYPES.has(file.mimetype)) {
|
||||
cb(null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
req.fileValidationError = 'UNSUPPORTED_FILE_TYPE';
|
||||
cb(null, false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSingleFileUpload = (req, res, next) => {
|
||||
upload.single('file')(req, res, (err) => {
|
||||
if (err) {
|
||||
console.error('[BuyProducts] Multer error:', err.message);
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({ error: 'File is too large. Maximum size is 15MB.' });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Функция для логирования с проверкой DEV переменной
|
||||
const log = (message, data = '') => {
|
||||
@@ -43,7 +111,7 @@ router.post('/', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { name, description, quantity, unit, status } = req.body;
|
||||
|
||||
log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.user.companyId });
|
||||
log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.companyId });
|
||||
|
||||
if (!name || !description || !quantity) {
|
||||
return res.status(400).json({
|
||||
@@ -58,7 +126,7 @@ router.post('/', verifyToken, async (req, res) => {
|
||||
}
|
||||
|
||||
const newProduct = new BuyProduct({
|
||||
companyId: req.user.companyId,
|
||||
companyId: req.companyId,
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
quantity: quantity.trim(),
|
||||
@@ -97,7 +165,7 @@ router.put('/:id', verifyToken, async (req, res) => {
|
||||
}
|
||||
|
||||
// Проверить, что товар принадлежит текущей компании
|
||||
if (product.companyId !== req.user.companyId) {
|
||||
if (product.companyId !== req.companyId) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
|
||||
@@ -134,7 +202,7 @@ router.delete('/:id', verifyToken, async (req, res) => {
|
||||
return res.status(404).json({ error: 'Product not found' });
|
||||
}
|
||||
|
||||
if (product.companyId.toString() !== req.user.companyId.toString()) {
|
||||
if (product.companyId.toString() !== req.companyId.toString()) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
|
||||
@@ -153,11 +221,9 @@ router.delete('/:id', verifyToken, async (req, res) => {
|
||||
});
|
||||
|
||||
// POST /buy-products/:id/files - добавить файл к товару
|
||||
router.post('/:id/files', verifyToken, async (req, res) => {
|
||||
router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { fileName, fileUrl, fileType, fileSize } = req.body;
|
||||
|
||||
const product = await BuyProduct.findById(id);
|
||||
|
||||
if (!product) {
|
||||
@@ -165,23 +231,33 @@ router.post('/:id/files', verifyToken, async (req, res) => {
|
||||
}
|
||||
|
||||
// Только владелец товара может добавить файл
|
||||
if (product.companyId.toString() !== req.user.companyId.toString()) {
|
||||
if (product.companyId.toString() !== req.companyId.toString()) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
|
||||
if (req.fileValidationError) {
|
||||
return res.status(400).json({ error: 'Unsupported file type. Use PDF, DOC, DOCX, XLS, XLSX or CSV.' });
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'File is required' });
|
||||
}
|
||||
|
||||
const relativePath = path.join('buy-products', id, req.file.filename).replace(/\\/g, '/');
|
||||
const file = {
|
||||
id: 'file-' + Date.now(),
|
||||
name: fileName,
|
||||
url: fileUrl,
|
||||
type: fileType,
|
||||
size: fileSize,
|
||||
uploadedAt: new Date()
|
||||
id: `file-${Date.now()}`,
|
||||
name: req.file.originalname,
|
||||
url: `/uploads/${relativePath}`,
|
||||
type: req.file.mimetype,
|
||||
size: req.file.size,
|
||||
uploadedAt: new Date(),
|
||||
storagePath: relativePath,
|
||||
};
|
||||
|
||||
product.files.push(file);
|
||||
await product.save();
|
||||
|
||||
log('[BuyProducts] File added to product:', id);
|
||||
log('[BuyProducts] File added to product:', id, file.name);
|
||||
|
||||
res.json(product);
|
||||
} catch (error) {
|
||||
@@ -204,14 +280,28 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => {
|
||||
return res.status(404).json({ error: 'Product not found' });
|
||||
}
|
||||
|
||||
if (product.companyId.toString() !== req.user.companyId.toString()) {
|
||||
if (product.companyId.toString() !== req.companyId.toString()) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
|
||||
const fileToRemove = product.files.find((f) => f.id === fileId);
|
||||
if (!fileToRemove) {
|
||||
return res.status(404).json({ error: 'File not found' });
|
||||
}
|
||||
|
||||
product.files = product.files.filter(f => f.id !== fileId);
|
||||
await product.save();
|
||||
|
||||
log('[BuyProducts] File deleted from product:', id);
|
||||
const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, '');
|
||||
const absolutePath = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', storedPath);
|
||||
|
||||
fs.promises.unlink(absolutePath).catch((unlinkError) => {
|
||||
if (unlinkError && unlinkError.code !== 'ENOENT') {
|
||||
console.error('[BuyProducts] Failed to remove file from disk:', unlinkError.message);
|
||||
}
|
||||
});
|
||||
|
||||
log('[BuyProducts] File deleted from product:', id, fileId);
|
||||
|
||||
res.json(product);
|
||||
} catch (error) {
|
||||
@@ -227,7 +317,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => {
|
||||
router.post('/:id/accept', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyId = req.user.companyId;
|
||||
const companyId = req.companyId;
|
||||
|
||||
const product = await BuyProduct.findById(id);
|
||||
|
||||
|
||||
@@ -2,17 +2,10 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const { verifyToken } = require('../middleware/auth');
|
||||
const Company = require('../models/Company');
|
||||
|
||||
// Инициализация данных при запуске
|
||||
const initializeCompanies = async () => {
|
||||
try {
|
||||
// Уже не нужна инициализация, она производится через authAPI
|
||||
} catch (error) {
|
||||
console.error('Error initializing companies:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeCompanies();
|
||||
const Experience = require('../models/Experience');
|
||||
const Request = require('../models/Request');
|
||||
const Message = require('../models/Message');
|
||||
const { Types } = require('mongoose');
|
||||
|
||||
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
|
||||
router.get('/my/info', verifyToken, async (req, res) => {
|
||||
@@ -44,23 +37,64 @@ router.get('/my/info', verifyToken, async (req, res) => {
|
||||
router.get('/my/stats', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const user = await require('../models/User').findById(userId);
|
||||
|
||||
if (!user || !user.companyId) {
|
||||
return res.status(404).json({ error: 'Company not found' });
|
||||
const User = require('../models/User');
|
||||
const user = await User.findById(userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
|
||||
let companyId = user.companyId;
|
||||
|
||||
if (!companyId) {
|
||||
const fallbackCompany = await Company.create({
|
||||
fullName: 'Компания пользователя',
|
||||
shortName: 'Компания пользователя',
|
||||
verified: false,
|
||||
partnerGeography: [],
|
||||
});
|
||||
|
||||
user.companyId = fallbackCompany._id;
|
||||
user.updatedAt = new Date();
|
||||
await user.save();
|
||||
companyId = fallbackCompany._id;
|
||||
}
|
||||
|
||||
let company = await Company.findById(companyId);
|
||||
|
||||
if (!company) {
|
||||
company = await Company.create({
|
||||
_id: companyId,
|
||||
fullName: 'Компания пользователя',
|
||||
verified: false,
|
||||
partnerGeography: [],
|
||||
});
|
||||
}
|
||||
|
||||
const companyIdString = company._id.toString();
|
||||
const companyObjectId = Types.ObjectId.isValid(companyIdString)
|
||||
? new Types.ObjectId(companyIdString)
|
||||
: null;
|
||||
|
||||
const [sentRequests, receivedRequests, unreadMessages] = await Promise.all([
|
||||
Request.countDocuments({ senderCompanyId: companyIdString }),
|
||||
Request.countDocuments({ recipientCompanyId: companyIdString }),
|
||||
companyObjectId
|
||||
? Message.countDocuments({ recipientCompanyId: companyObjectId, read: false })
|
||||
: Promise.resolve(0),
|
||||
]);
|
||||
|
||||
const stats = {
|
||||
profileViews: Math.floor(Math.random() * 1000),
|
||||
profileViewsChange: Math.floor(Math.random() * 20 - 10),
|
||||
sentRequests: Math.floor(Math.random() * 50),
|
||||
sentRequestsChange: Math.floor(Math.random() * 10 - 5),
|
||||
receivedRequests: Math.floor(Math.random() * 30),
|
||||
receivedRequestsChange: Math.floor(Math.random() * 5 - 2),
|
||||
newMessages: Math.floor(Math.random() * 10),
|
||||
rating: Math.random() * 5
|
||||
profileViews: company?.metrics?.profileViews || 0,
|
||||
profileViewsChange: 0,
|
||||
sentRequests,
|
||||
sentRequestsChange: 0,
|
||||
receivedRequests,
|
||||
receivedRequestsChange: 0,
|
||||
newMessages: unreadMessages,
|
||||
rating: Number.isFinite(company?.rating) ? Number(company.rating) : 0,
|
||||
};
|
||||
|
||||
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
console.error('Get company stats error:', error);
|
||||
@@ -68,15 +102,22 @@ router.get('/my/stats', verifyToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Experience endpoints ДОЛЖНЫ быть ДО получения компании по ID
|
||||
let companyExperience = [];
|
||||
|
||||
// GET /:id/experience - получить опыт компании
|
||||
router.get('/:id/experience', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const experience = companyExperience.filter(e => e.companyId === id);
|
||||
res.json(experience);
|
||||
|
||||
if (!Types.ObjectId.isValid(id)) {
|
||||
return res.status(400).json({ error: 'Invalid company ID' });
|
||||
}
|
||||
|
||||
const experience = await Experience.find({ companyId: new Types.ObjectId(id) })
|
||||
.sort({ createdAt: -1 });
|
||||
|
||||
res.json(experience.map(exp => ({
|
||||
...exp.toObject(),
|
||||
id: exp._id
|
||||
})));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
@@ -88,23 +129,24 @@ router.post('/:id/experience', verifyToken, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { confirmed, customer, subject, volume, contact, comment } = req.body;
|
||||
|
||||
const expId = Math.random().toString(36).substr(2, 9);
|
||||
const newExp = {
|
||||
id: expId,
|
||||
_id: expId,
|
||||
companyId: id,
|
||||
if (!Types.ObjectId.isValid(id)) {
|
||||
return res.status(400).json({ error: 'Invalid company ID' });
|
||||
}
|
||||
|
||||
const newExp = await Experience.create({
|
||||
companyId: new Types.ObjectId(id),
|
||||
confirmed: confirmed || false,
|
||||
customer: customer || '',
|
||||
subject: subject || '',
|
||||
volume: volume || '',
|
||||
contact: contact || '',
|
||||
comment: comment || '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
comment: comment || ''
|
||||
});
|
||||
|
||||
companyExperience.push(newExp);
|
||||
res.status(201).json(newExp);
|
||||
res.status(201).json({
|
||||
...newExp.toObject(),
|
||||
id: newExp._id
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
@@ -114,19 +156,28 @@ router.post('/:id/experience', verifyToken, async (req, res) => {
|
||||
router.put('/:id/experience/:expId', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id, expId } = req.params;
|
||||
const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id);
|
||||
|
||||
if (index === -1) {
|
||||
if (!Types.ObjectId.isValid(id) || !Types.ObjectId.isValid(expId)) {
|
||||
return res.status(400).json({ error: 'Invalid IDs' });
|
||||
}
|
||||
|
||||
const experience = await Experience.findByIdAndUpdate(
|
||||
new Types.ObjectId(expId),
|
||||
{
|
||||
...req.body,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!experience || experience.companyId.toString() !== id) {
|
||||
return res.status(404).json({ error: 'Experience not found' });
|
||||
}
|
||||
|
||||
companyExperience[index] = {
|
||||
...companyExperience[index],
|
||||
...req.body,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
res.json(companyExperience[index]);
|
||||
res.json({
|
||||
...experience.toObject(),
|
||||
id: experience._id
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
@@ -136,13 +187,18 @@ router.put('/:id/experience/:expId', verifyToken, async (req, res) => {
|
||||
router.delete('/:id/experience/:expId', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id, expId } = req.params;
|
||||
const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id);
|
||||
|
||||
if (index === -1) {
|
||||
if (!Types.ObjectId.isValid(id) || !Types.ObjectId.isValid(expId)) {
|
||||
return res.status(400).json({ error: 'Invalid IDs' });
|
||||
}
|
||||
|
||||
const experience = await Experience.findById(new Types.ObjectId(expId));
|
||||
|
||||
if (!experience || experience.companyId.toString() !== id) {
|
||||
return res.status(404).json({ error: 'Experience not found' });
|
||||
}
|
||||
|
||||
companyExperience.splice(index, 1);
|
||||
|
||||
await Experience.findByIdAndDelete(new Types.ObjectId(expId));
|
||||
res.json({ message: 'Experience deleted' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -155,7 +211,24 @@ router.get('/:id', async (req, res) => {
|
||||
const company = await Company.findById(req.params.id);
|
||||
|
||||
if (!company) {
|
||||
return res.status(404).json({ error: 'Company not found' });
|
||||
if (!Types.ObjectId.isValid(req.params.id)) {
|
||||
return res.status(404).json({ error: 'Company not found' });
|
||||
}
|
||||
|
||||
const placeholder = await Company.create({
|
||||
_id: new Types.ObjectId(req.params.id),
|
||||
fullName: 'Новая компания',
|
||||
shortName: 'Новая компания',
|
||||
verified: false,
|
||||
partnerGeography: [],
|
||||
industry: '',
|
||||
companySize: '',
|
||||
});
|
||||
|
||||
return res.json({
|
||||
...placeholder.toObject(),
|
||||
id: placeholder._id,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { verifyToken } = require('../middleware/auth');
|
||||
|
||||
// In-memory хранилище для опыта работы (mock)
|
||||
let experiences = [];
|
||||
const Experience = require('../models/Experience');
|
||||
const { Types } = require('mongoose');
|
||||
|
||||
// GET /experience - Получить список опыта работы компании
|
||||
router.get('/', verifyToken, (req, res) => {
|
||||
router.get('/', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { companyId } = req.query;
|
||||
|
||||
@@ -14,8 +13,18 @@ router.get('/', verifyToken, (req, res) => {
|
||||
return res.status(400).json({ error: 'companyId is required' });
|
||||
}
|
||||
|
||||
const companyExperiences = experiences.filter(exp => exp.companyId === companyId);
|
||||
res.json(companyExperiences);
|
||||
if (!Types.ObjectId.isValid(companyId)) {
|
||||
return res.status(400).json({ error: 'Invalid company ID' });
|
||||
}
|
||||
|
||||
const companyExperiences = await Experience.find({
|
||||
companyId: new Types.ObjectId(companyId)
|
||||
}).sort({ createdAt: -1 });
|
||||
|
||||
res.json(companyExperiences.map(exp => ({
|
||||
...exp.toObject(),
|
||||
id: exp._id
|
||||
})));
|
||||
} catch (error) {
|
||||
console.error('Get experience error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
@@ -23,7 +32,7 @@ router.get('/', verifyToken, (req, res) => {
|
||||
});
|
||||
|
||||
// POST /experience - Создать запись опыта работы
|
||||
router.post('/', verifyToken, (req, res) => {
|
||||
router.post('/', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { companyId, data } = req.body;
|
||||
|
||||
@@ -31,28 +40,30 @@ router.post('/', verifyToken, (req, res) => {
|
||||
return res.status(400).json({ error: 'companyId and data are required' });
|
||||
}
|
||||
|
||||
if (!Types.ObjectId.isValid(companyId)) {
|
||||
return res.status(400).json({ error: 'Invalid company ID' });
|
||||
}
|
||||
|
||||
const { confirmed, customer, subject, volume, contact, comment } = data;
|
||||
|
||||
if (!customer || !subject) {
|
||||
return res.status(400).json({ error: 'customer and subject are required' });
|
||||
}
|
||||
|
||||
const newExperience = {
|
||||
id: `exp-${Date.now()}`,
|
||||
companyId,
|
||||
const newExperience = await Experience.create({
|
||||
companyId: new Types.ObjectId(companyId),
|
||||
confirmed: confirmed || false,
|
||||
customer,
|
||||
subject,
|
||||
volume: volume || '',
|
||||
contact: contact || '',
|
||||
comment: comment || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
comment: comment || ''
|
||||
});
|
||||
|
||||
experiences.push(newExperience);
|
||||
|
||||
res.status(201).json(newExperience);
|
||||
res.status(201).json({
|
||||
...newExperience.toObject(),
|
||||
id: newExperience._id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create experience error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
@@ -60,7 +71,7 @@ router.post('/', verifyToken, (req, res) => {
|
||||
});
|
||||
|
||||
// PUT /experience/:id - Обновить запись опыта работы
|
||||
router.put('/:id', verifyToken, (req, res) => {
|
||||
router.put('/:id', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { data } = req.body;
|
||||
@@ -69,21 +80,27 @@ router.put('/:id', verifyToken, (req, res) => {
|
||||
return res.status(400).json({ error: 'data is required' });
|
||||
}
|
||||
|
||||
const index = experiences.findIndex(exp => exp.id === id);
|
||||
if (!Types.ObjectId.isValid(id)) {
|
||||
return res.status(400).json({ error: 'Invalid experience ID' });
|
||||
}
|
||||
|
||||
if (index === -1) {
|
||||
const updatedExperience = await Experience.findByIdAndUpdate(
|
||||
new Types.ObjectId(id),
|
||||
{
|
||||
...data,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!updatedExperience) {
|
||||
return res.status(404).json({ error: 'Experience not found' });
|
||||
}
|
||||
|
||||
const updatedExperience = {
|
||||
...experiences[index],
|
||||
...data,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
experiences[index] = updatedExperience;
|
||||
|
||||
res.json(updatedExperience);
|
||||
res.json({
|
||||
...updatedExperience.toObject(),
|
||||
id: updatedExperience._id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update experience error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
@@ -91,17 +108,19 @@ router.put('/:id', verifyToken, (req, res) => {
|
||||
});
|
||||
|
||||
// DELETE /experience/:id - Удалить запись опыта работы
|
||||
router.delete('/:id', verifyToken, (req, res) => {
|
||||
router.delete('/:id', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const index = experiences.findIndex(exp => exp.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
return res.status(404).json({ error: 'Experience not found' });
|
||||
if (!Types.ObjectId.isValid(id)) {
|
||||
return res.status(400).json({ error: 'Invalid experience ID' });
|
||||
}
|
||||
|
||||
experiences.splice(index, 1);
|
||||
const deletedExperience = await Experience.findByIdAndDelete(new Types.ObjectId(id));
|
||||
|
||||
if (!deletedExperience) {
|
||||
return res.status(404).json({ error: 'Experience not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Experience deleted successfully' });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,16 +1,49 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { verifyToken } = require('../middleware/auth');
|
||||
const BuyProduct = require('../models/BuyProduct');
|
||||
const Request = require('../models/Request');
|
||||
|
||||
// Получить агрегированные данные для главной страницы
|
||||
router.get('/aggregates', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const User = require('../models/User');
|
||||
const user = await User.findById(userId);
|
||||
|
||||
if (!user || !user.companyId) {
|
||||
return res.json({
|
||||
docsCount: 0,
|
||||
acceptsCount: 0,
|
||||
requestsCount: 0
|
||||
});
|
||||
}
|
||||
|
||||
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 }
|
||||
]
|
||||
})
|
||||
]);
|
||||
|
||||
res.json({
|
||||
docsCount: 0,
|
||||
acceptsCount: 0,
|
||||
requestsCount: 0
|
||||
docsCount,
|
||||
acceptsCount,
|
||||
requestsCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting aggregates:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
@@ -18,17 +51,42 @@ router.get('/aggregates', verifyToken, async (req, res) => {
|
||||
// Получить статистику компании
|
||||
router.get('/stats', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const User = require('../models/User');
|
||||
const Company = require('../models/Company');
|
||||
const user = await User.findById(userId);
|
||||
|
||||
if (!user || !user.companyId) {
|
||||
return res.json({
|
||||
profileViews: 0,
|
||||
profileViewsChange: 0,
|
||||
sentRequests: 0,
|
||||
sentRequestsChange: 0,
|
||||
receivedRequests: 0,
|
||||
receivedRequestsChange: 0,
|
||||
newMessages: 0,
|
||||
rating: 0
|
||||
});
|
||||
}
|
||||
|
||||
const companyId = user.companyId.toString();
|
||||
const company = await Company.findById(user.companyId);
|
||||
|
||||
const sentRequests = await Request.countDocuments({ senderCompanyId: companyId });
|
||||
const receivedRequests = await Request.countDocuments({ recipientCompanyId: companyId });
|
||||
|
||||
res.json({
|
||||
profileViews: 12,
|
||||
profileViewsChange: 5,
|
||||
sentRequests: 3,
|
||||
sentRequestsChange: 1,
|
||||
receivedRequests: 7,
|
||||
receivedRequestsChange: 2,
|
||||
newMessages: 4,
|
||||
rating: 4.5
|
||||
profileViews: company?.metrics?.profileViews || 0,
|
||||
profileViewsChange: 0,
|
||||
sentRequests,
|
||||
sentRequestsChange: 0,
|
||||
receivedRequests,
|
||||
receivedRequestsChange: 0,
|
||||
newMessages: 0,
|
||||
rating: company?.rating || 0
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting stats:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
@@ -36,11 +94,40 @@ router.get('/stats', verifyToken, async (req, res) => {
|
||||
// Получить рекомендации партнеров (AI)
|
||||
router.get('/recommendations', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
const User = require('../models/User');
|
||||
const Company = require('../models/Company');
|
||||
const user = await User.findById(userId);
|
||||
|
||||
if (!user || !user.companyId) {
|
||||
return res.json({
|
||||
recommendations: [],
|
||||
message: 'No recommendations available'
|
||||
});
|
||||
}
|
||||
|
||||
// Получить компании кроме текущей
|
||||
const companies = await Company.find({
|
||||
_id: { $ne: user.companyId }
|
||||
})
|
||||
.sort({ rating: -1 })
|
||||
.limit(5);
|
||||
|
||||
const recommendations = companies.map(company => ({
|
||||
id: company._id.toString(),
|
||||
name: company.fullName || company.shortName,
|
||||
industry: company.industry,
|
||||
logo: company.logo,
|
||||
matchScore: company.rating ? Math.min(100, Math.round(company.rating * 20)) : 50,
|
||||
reason: 'Matches your industry'
|
||||
}));
|
||||
|
||||
res.json({
|
||||
recommendations: [],
|
||||
message: 'No recommendations available yet'
|
||||
recommendations,
|
||||
message: recommendations.length > 0 ? 'Recommendations available' : 'No recommendations available'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting recommendations:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ const log = (message, data = '') => {
|
||||
// GET /messages/threads - получить все потоки для компании
|
||||
router.get('/threads', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const companyId = req.user.companyId;
|
||||
const companyId = req.companyId;
|
||||
const { ObjectId } = require('mongoose').Types;
|
||||
|
||||
log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId);
|
||||
@@ -91,7 +91,7 @@ router.get('/threads', verifyToken, async (req, res) => {
|
||||
router.get('/:threadId', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { threadId } = req.params;
|
||||
const companyId = req.user.companyId;
|
||||
const companyId = req.companyId;
|
||||
|
||||
// Получить все сообщения потока
|
||||
const threadMessages = await Message.find({ threadId })
|
||||
@@ -128,7 +128,7 @@ router.post('/:threadId', verifyToken, async (req, res) => {
|
||||
const threadParts = threadId.replace('thread-', '').split('-');
|
||||
let recipientCompanyId = null;
|
||||
|
||||
const currentSender = senderCompanyId || req.user.companyId;
|
||||
const currentSender = senderCompanyId || req.companyId;
|
||||
const currentSenderString = currentSender.toString ? currentSender.toString() : currentSender;
|
||||
|
||||
if (threadParts.length >= 2) {
|
||||
|
||||
@@ -28,7 +28,7 @@ const transformProduct = (doc) => {
|
||||
// GET /products - Получить список продуктов/услуг компании (текущего пользователя)
|
||||
router.get('/', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const companyId = req.user.companyId;
|
||||
const companyId = req.companyId;
|
||||
|
||||
log('[Products] GET Fetching products for companyId:', companyId);
|
||||
|
||||
@@ -48,7 +48,7 @@ router.get('/', verifyToken, async (req, res) => {
|
||||
router.post('/', verifyToken, async (req, res) => {
|
||||
// try {
|
||||
const { name, category, description, type, productUrl, price, unit, minOrder } = req.body;
|
||||
const companyId = req.user.companyId;
|
||||
const companyId = req.companyId;
|
||||
|
||||
log('[Products] POST Creating product:', { name, category, type });
|
||||
|
||||
@@ -88,7 +88,7 @@ router.put('/:id', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
const companyId = req.user.companyId;
|
||||
const companyId = req.companyId;
|
||||
|
||||
const product = await Product.findById(id);
|
||||
|
||||
@@ -120,7 +120,7 @@ router.patch('/:id', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
const companyId = req.user.companyId;
|
||||
const companyId = req.companyId;
|
||||
|
||||
const product = await Product.findById(id);
|
||||
|
||||
@@ -150,7 +150,7 @@ router.patch('/:id', verifyToken, async (req, res) => {
|
||||
router.delete('/:id', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const companyId = req.user.companyId;
|
||||
const companyId = req.companyId;
|
||||
|
||||
const product = await Product.findById(id);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -61,7 +61,7 @@ router.post('/', verifyToken, async (req, res) => {
|
||||
// Создать новый отзыв
|
||||
const newReview = new Review({
|
||||
companyId,
|
||||
authorCompanyId: req.user.companyId,
|
||||
authorCompanyId: req.companyId,
|
||||
authorName: req.user.firstName + ' ' + req.user.lastName,
|
||||
authorCompany: req.user.companyName || 'Company',
|
||||
rating: parseInt(rating),
|
||||
|
||||
@@ -127,14 +127,8 @@ router.get('/', verifyToken, async (req, res) => {
|
||||
log('[Search] Industry codes:', industryList, 'Mapped to:', dbIndustries);
|
||||
|
||||
if (dbIndustries.length > 0) {
|
||||
// Handle both string and array industry values
|
||||
filters.push({
|
||||
$or: [
|
||||
{ industry: { $in: dbIndustries } },
|
||||
{ industry: { $elemMatch: { $in: dbIndustries } } }
|
||||
]
|
||||
});
|
||||
log('[Search] Added industry filter:', { $or: [{ industry: { $in: dbIndustries } }, { industry: { $elemMatch: { $in: dbIndustries } } }] });
|
||||
filters.push({ industry: { $in: dbIndustries } });
|
||||
log('[Search] Added industry filter:', { industry: { $in: dbIndustries } });
|
||||
} else {
|
||||
log('[Search] No industries mapped! Codes were:', industryList);
|
||||
}
|
||||
@@ -219,10 +213,8 @@ router.get('/', verifyToken, async (req, res) => {
|
||||
page: pageNum,
|
||||
totalPages: Math.ceil(total / limitNum),
|
||||
_debug: {
|
||||
requestParams: { query, industries, companySize, geography, minRating, hasReviews, hasAcceptedDocs, sortBy, sortOrder },
|
||||
filter: JSON.stringify(filter),
|
||||
filtersCount: filters.length,
|
||||
appliedFilters: filters.map(f => JSON.stringify(f))
|
||||
industriesReceived: industries
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user