diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js index 0e3adf3..601687e 100644 --- a/server/routers/procurement/config/db.js +++ b/server/routers/procurement/config/db.js @@ -1,31 +1,97 @@ const mongoose = require('mongoose'); -const connectDB = async () => { - try { - const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - - console.log('\n📡 Попытка подключения к MongoDB...'); - console.log(` URI: ${mongoUri.replace(/\/\/:.*@/, '//***:***@')}`); - - const connection = await mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - - console.log('✅ MongoDB подключена успешно!'); - console.log(` Хост: ${connection.connection.host}`); - console.log(` БД: ${connection.connection.name}\n`); - - return connection; - } catch (error) { - console.error('\n❌ Ошибка подключения к MongoDB:'); - console.error(` ${error.message}\n`); - console.warn('⚠️ Приложение продолжит работу с mock данными\n'); - - return null; +// Get MongoDB URL from environment variables +// MONGO_ADDR is a centralized env variable from server/utils/const.ts +const primaryUri = process.env.MONGO_ADDR || 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'; + +/** + * Check if error is related to authentication + */ +const isAuthError = (error) => { + if (!error) { + return false; } + + const authCodes = new Set([18, 13]); + if (error.code && authCodes.has(error.code)) { + return true; + } + + const message = String(error.message || '').toLowerCase(); + return message.includes('auth') || message.includes('authentication'); +}; + +/** + * Try to connect to MongoDB with specific URI + */ +const connectWithUri = async (uri, label) => { + console.log(`\n📡 Попытка подключения к MongoDB (${label})...`); + if (process.env.DEV === 'true') { + console.log(` URI: ${uri}`); + } + + const connection = await mongoose.connect(uri, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + + try { + await connection.connection.db.admin().command({ ping: 1 }); + } catch (pingError) { + if (isAuthError(pingError)) { + await mongoose.connection.close().catch(() => {}); + throw pingError; + } + console.error('⚠️ MongoDB ping error:', pingError.message); + } + + console.log('✅ MongoDB подключена успешно!'); + console.log(` Хост: ${connection.connection.host}`); + console.log(` БД: ${connection.connection.name}\n`); + if (process.env.DEV === 'true') { + console.log(` Пользователь: ${connection.connection.user || 'anonymous'}`); + } + + return connection; +}; + +/** + * Connect to MongoDB with fallback strategy + */ +const connectDB = async () => { + const attempts = []; + + if (fallbackUri) { + attempts.push({ uri: fallbackUri, label: 'AUTH' }); + } + + attempts.push({ uri: primaryUri, label: 'PRIMARY' }); + + let lastError = null; + + for (const attempt of attempts) { + try { + console.log(`[MongoDB] Trying ${attempt.label} connection...`); + return await connectWithUri(attempt.uri, attempt.label); + } catch (error) { + lastError = error; + console.error(`\n❌ Ошибка подключения к MongoDB (${attempt.label}):`); + console.error(` ${error.message}\n`); + + if (!isAuthError(error)) { + break; + } + } + } + + if (lastError) { + console.warn('⚠️ Приложение продолжит работу с mock данными\n'); + } + + return null; }; module.exports = connectDB; diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index fdbc8fd..b20542e 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -1,8 +1,8 @@ const express = require('express'); const cors = require('cors'); const dotenv = require('dotenv'); -const connectDB = require('./config/db'); -const { runMigrations } = require('./scripts/run-migrations'); +const fs = require('fs'); +const path = require('path'); // Загрузить переменные окружения dotenv.config(); @@ -15,7 +15,7 @@ if (process.env.DEV === 'true') { console.log('ℹ️ DEBUG MODE ENABLED - All logs are visible'); } -// Импортировать маршруты +// Импортировать маршруты - прямые пути без path.join и __dirname const authRoutes = require('./routes/auth'); const companiesRoutes = require('./routes/companies'); const messagesRoutes = require('./routes/messages'); @@ -28,34 +28,15 @@ const buyProductsRoutes = require('./routes/buyProducts'); const requestsRoutes = require('./routes/requests'); const homeRoutes = require('./routes/home'); +const connectDB = require('./config/db'); + const app = express(); -// Подключить MongoDB и запустить миграции при инициализации +// Подключить MongoDB при инициализации let dbConnected = false; -let migrationsCompleted = false; - -const initializeApp = async () => { - try { - await connectDB().then(() => { - dbConnected = true; - }); - - // Запустить миграции после успешного подключения - if (dbConnected) { - try { - await runMigrations(false); - migrationsCompleted = true; - } catch (migrationError) { - console.error('⚠️ Migrations failed but app will continue:', migrationError.message); - } - } - } catch (err) { - console.error('Error during app initialization:', err); - } -}; - -// Запустить инициализацию -initializeApp(); +connectDB().then(() => { + dbConnected = true; +}); // Middleware app.use(cors()); @@ -84,13 +65,19 @@ app.use((req, res, next) => { const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms); app.use(delay()); +// Статика для загруженных файлов +const uploadsRoot = path.join(__dirname, '..', '..', 'remote-assets', 'uploads'); +if (!fs.existsSync(uploadsRoot)) { + fs.mkdirSync(uploadsRoot, { recursive: true }); +} +app.use('/uploads', express.static(uploadsRoot)); + // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', api: 'running', database: dbConnected ? 'mongodb' : 'mock', - migrations: migrationsCompleted ? 'completed' : 'pending', timestamp: new Date().toISOString() }); }); diff --git a/server/routers/procurement/mocks/auth.json b/server/routers/procurement/mocks/auth.json deleted file mode 100644 index eabb81d..0000000 --- a/server/routers/procurement/mocks/auth.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "mockAuthResponse": { - "user": { - "id": "user-123", - "email": "test@company.com", - "firstName": "Иван", - "lastName": "Петров", - "position": "Генеральный директор" - }, - "company": { - "id": "company-123", - "name": "ООО \"Тестовая Компания\"", - "inn": "7707083893", - "ogrn": "1027700132195", - "fullName": "Общество с ограниченной ответственностью \"Тестовая Компания\"", - "shortName": "ООО \"Тест\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "50-100", - "website": "https://test-company.ru", - "verified": true, - "rating": 4.5 - }, - "tokens": { - "accessToken": "mock-access-token-{{timestamp}}", - "refreshToken": "mock-refresh-token-{{timestamp}}" - } - }, - "errorMessages": { - "validationFailed": "Заполните все обязательные поля", - "emailRequired": "Email обязателен", - "passwordRequired": "Пароль обязателен", - "termsRequired": "Необходимо принять условия использования", - "invalidCredentials": "Неверный email или пароль", - "refreshTokenRequired": "Refresh token обязателен", - "innValidation": "ИНН должен содержать 10 или 12 цифр" - }, - "successMessages": { - "logoutSuccess": "Успешный выход", - "emailVerified": "Email успешно подтвержден", - "passwordResetSent": "Письмо для восстановления пароля отправлено", - "passwordResetSuccess": "Пароль успешно изменен", - "logoUploaded": "Логотип успешно загружен", - "addedToFavorites": "Добавлено в избранное" - } -} diff --git a/server/routers/procurement/mocks/companies.json b/server/routers/procurement/mocks/companies.json deleted file mode 100644 index 8e3bffe..0000000 --- a/server/routers/procurement/mocks/companies.json +++ /dev/null @@ -1,430 +0,0 @@ -{ - "mockCompany": { - "id": "company-123", - "name": "ООО \"Тестовая Компания\"", - "inn": "7707083893", - "ogrn": "1027700132195", - "fullName": "Общество с ограниченной ответственностью \"Тестовая Компания\"", - "shortName": "ООО \"Тест\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "50-100", - "website": "https://test-company.ru", - "verified": true, - "rating": 4.5 - }, - "mockINNData": { - "7707083893": { - "name": "ПУБЛИЧНОЕ АКЦИОНЕРНОЕ ОБЩЕСТВО \"СБЕРБАНК РОССИИ\"", - "ogrn": "1027700132195", - "legal_form": "ПАО" - }, - "7730048036": { - "name": "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ \"КОМПАНИЯ\"", - "ogrn": "1047730048036", - "legal_form": "ООО" - } - }, - "mockCompanies": [ - { - "id": "company-1", - "inn": "7707083893", - "ogrn": "1027700132195", - "fullName": "Общество с ограниченной ответственностью \"СтройКомплект\"", - "shortName": "ООО \"СтройКомплект\"", - "legalForm": "ООО", - "industry": "Строительство", - "companySize": "100-250", - "website": "https://stroykomplekt.ru", - "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=SK", - "slogan": "Строим будущее вместе", - "rating": 4.8, - "verified": true, - "phone": "+7 (495) 123-45-67", - "email": "info@stroykomplekt.ru", - "legalAddress": "г. Москва, ул. Строительная, д. 15", - "foundedYear": 2010, - "employeeCount": "150 сотрудников" - }, - { - "id": "company-6", - "inn": "7707083894", - "ogrn": "1027700132196", - "fullName": "Акционерное общество \"Московский Строй\"", - "shortName": "АО \"Московский Строй\"", - "legalForm": "АО", - "industry": "Строительство", - "companySize": "500+", - "website": "https://moscow-stroy.ru", - "logo": "https://via.placeholder.com/100x100/1A365D/FFFFFF?text=MS", - "slogan": "Качество и надежность с 1995 года", - "rating": 4.9, - "verified": true, - "phone": "+7 (495) 987-65-43", - "email": "info@moscow-stroy.ru", - "legalAddress": "г. Москва, пр. Мира, д. 100", - "foundedYear": 1995, - "employeeCount": "800+ сотрудников" - }, - { - "id": "company-7", - "inn": "7707083895", - "ogrn": "1027700132197", - "fullName": "Общество с ограниченной ответственностью \"ДомСтрой\"", - "shortName": "ООО \"ДомСтрой\"", - "legalForm": "ООО", - "industry": "Строительство", - "companySize": "50-100", - "website": "https://domstroy.ru", - "logo": "https://via.placeholder.com/100x100/2D3748/FFFFFF?text=DS", - "slogan": "Строим дома мечты", - "rating": 4.3, - "verified": true, - "phone": "+7 (495) 555-12-34", - "email": "info@domstroy.ru", - "legalAddress": "г. Москва, ул. Жилстроительная, д. 25", - "foundedYear": 2015, - "employeeCount": "75 сотрудников" - }, - { - "id": "company-4", - "inn": "7730048038", - "ogrn": "1047730048038", - "fullName": "Общество с ограниченной ответственностью \"МеталлПром\"", - "shortName": "ООО \"МеталлПром\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "250-500", - "website": "https://metallprom.ru", - "logo": "https://via.placeholder.com/100x100/E53E3E/FFFFFF?text=MP", - "slogan": "Металл высшего качества", - "rating": 4.7, - "verified": true, - "phone": "+7 (495) 456-78-90", - "email": "info@metallprom.ru", - "legalAddress": "г. Москва, ул. Промышленная, д. 50", - "foundedYear": 2008, - "employeeCount": "300 сотрудников" - }, - { - "id": "company-8", - "inn": "7730048040", - "ogrn": "1047730048040", - "fullName": "Общество с ограниченной ответственностью \"СтальМет\"", - "shortName": "ООО \"СтальМет\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "100-250", - "website": "https://stalmet.ru", - "logo": "https://via.placeholder.com/100x100/9C4221/FFFFFF?text=SM", - "slogan": "Сталь для промышленности", - "rating": 4.6, - "verified": true, - "phone": "+7 (495) 777-88-99", - "email": "sales@stalmet.ru", - "legalAddress": "г. Москва, ул. Металлургическая, д. 30", - "foundedYear": 2012, - "employeeCount": "180 сотрудников" - }, - { - "id": "company-9", - "inn": "7730048041", - "ogrn": "1047730048041", - "fullName": "Общество с ограниченной ответственностью \"ПластМаш\"", - "shortName": "ООО \"ПластМаш\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "50-100", - "website": "https://plastmash.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=PM", - "slogan": "Пластиковые изделия для всех отраслей", - "rating": 4.4, - "verified": true, - "phone": "+7 (495) 333-44-55", - "email": "info@plastmash.ru", - "legalAddress": "г. Москва, ул. Пластиковая, д. 12", - "foundedYear": 2018, - "employeeCount": "80 сотрудников" - }, - { - "id": "company-2", - "inn": "7730048036", - "ogrn": "1047730048036", - "fullName": "Общество с ограниченной ответственностью \"ТехСнаб\"", - "shortName": "ООО \"ТехСнаб\"", - "legalForm": "ООО", - "industry": "Торговля", - "companySize": "50-100", - "website": "https://techsnab.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=TS", - "slogan": "Снабжение для профессионалов", - "rating": 4.5, - "verified": true, - "phone": "+7 (495) 234-56-78", - "email": "sales@techsnab.ru", - "legalAddress": "г. Москва, ул. Торговая, д. 8", - "foundedYear": 2010, - "employeeCount": "90 сотрудников" - }, - { - "id": "company-10", - "inn": "7730048042", - "ogrn": "1047730048042", - "fullName": "Общество с ограниченной ответственностью \"ОптТорг\"", - "shortName": "ООО \"ОптТорг\"", - "legalForm": "ООО", - "industry": "Торговля", - "companySize": "100-250", - "website": "https://opttorg.ru", - "logo": "https://via.placeholder.com/100x100/805AD5/FFFFFF?text=OT", - "slogan": "Оптовые поставки по всей России", - "rating": 4.2, - "verified": true, - "phone": "+7 (495) 111-22-33", - "email": "info@opttorg.ru", - "legalAddress": "г. Москва, ул. Оптовая, д. 45", - "foundedYear": 2005, - "employeeCount": "200 сотрудников" - }, - { - "id": "company-5", - "inn": "7730048039", - "ogrn": "1047730048039", - "fullName": "Общество с ограниченной ответственностью \"ЛогистикПлюс\"", - "shortName": "ООО \"ЛогистикПлюс\"", - "legalForm": "ООО", - "industry": "Логистика", - "companySize": "100-250", - "website": "https://logistikplus.ru", - "logo": "https://via.placeholder.com/100x100/805AD5/FFFFFF?text=LP", - "slogan": "Доставляем быстро и надежно", - "rating": 4.6, - "verified": true, - "phone": "+7 (495) 567-89-01", - "email": "info@logistikplus.ru", - "legalAddress": "г. Москва, ул. Логистическая, д. 20", - "foundedYear": 2013, - "employeeCount": "150 сотрудников" - }, - { - "id": "company-11", - "inn": "7730048043", - "ogrn": "1047730048043", - "fullName": "Общество с ограниченной ответственностью \"ТрансЛогист\"", - "shortName": "ООО \"ТрансЛогист\"", - "legalForm": "ООО", - "industry": "Логистика", - "companySize": "250-500", - "website": "https://translogist.ru", - "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=TL", - "slogan": "Транспортные решения для бизнеса", - "rating": 4.8, - "verified": true, - "phone": "+7 (495) 999-88-77", - "email": "info@translogist.ru", - "legalAddress": "г. Москва, ул. Транспортная, д. 60", - "foundedYear": 2007, - "employeeCount": "350 сотрудников" - }, - { - "id": "company-12", - "inn": "7730048044", - "ogrn": "1047730048044", - "fullName": "Общество с ограниченной ответственностью \"ТехСофт\"", - "shortName": "ООО \"ТехСофт\"", - "legalForm": "ООО", - "industry": "IT", - "companySize": "50-100", - "website": "https://techsoft.ru", - "logo": "https://via.placeholder.com/100x100/3182CE/FFFFFF?text=TS", - "slogan": "IT-решения для бизнеса", - "rating": 4.7, - "verified": true, - "phone": "+7 (495) 444-55-66", - "email": "info@techsoft.ru", - "legalAddress": "г. Москва, ул. Программистов, д. 10", - "foundedYear": 2016, - "employeeCount": "85 сотрудников" - }, - { - "id": "company-13", - "inn": "7730048045", - "ogrn": "1047730048045", - "fullName": "Общество с ограниченной ответственностью \"КиберТех\"", - "shortName": "ООО \"КиберТех\"", - "legalForm": "ООО", - "industry": "IT", - "companySize": "100-250", - "website": "https://cybertech.ru", - "logo": "https://via.placeholder.com/100x100/553C9A/FFFFFF?text=CT", - "slogan": "Кибербезопасность и автоматизация", - "rating": 4.9, - "verified": true, - "phone": "+7 (495) 666-77-88", - "email": "info@cybertech.ru", - "legalAddress": "г. Москва, ул. Кибернетическая, д. 5", - "foundedYear": 2014, - "employeeCount": "120 сотрудников" - }, - { - "id": "company-3", - "inn": "7730048037", - "ogrn": "1047730048037", - "fullName": "Индивидуальный предприниматель Сидоров Петр Иванович", - "shortName": "ИП Сидоров П.И.", - "legalForm": "ИП", - "industry": "Услуги", - "companySize": "1-10", - "website": "https://sidorov-service.ru", - "logo": "https://via.placeholder.com/100x100/D69E2E/FFFFFF?text=SI", - "slogan": "Качественные услуги для малого бизнеса", - "rating": 4.2, - "verified": false, - "phone": "+7 (495) 345-67-89", - "email": "info@sidorov-service.ru", - "legalAddress": "г. Москва, ул. Сервисная, д. 3", - "foundedYear": 2020, - "employeeCount": "5 сотрудников" - }, - { - "id": "company-14", - "inn": "7730048046", - "ogrn": "1047730048046", - "fullName": "Общество с ограниченной ответственностью \"КонсалтПро\"", - "shortName": "ООО \"КонсалтПро\"", - "legalForm": "ООО", - "industry": "Услуги", - "companySize": "10-50", - "website": "https://konsultpro.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=KP", - "slogan": "Консалтинг для роста бизнеса", - "rating": 4.5, - "verified": true, - "phone": "+7 (495) 222-33-44", - "email": "info@konsultpro.ru", - "legalAddress": "г. Москва, ул. Консультационная, д. 15", - "foundedYear": 2017, - "employeeCount": "25 сотрудников" - }, - { - "id": "company-15", - "inn": "7730048047", - "ogrn": "1047730048047", - "fullName": "Общество с ограниченной ответственностью \"ПищеПром\"", - "shortName": "ООО \"ПищеПром\"", - "legalForm": "ООО", - "industry": "Пищевая промышленность", - "companySize": "100-250", - "website": "https://pishcheprom.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=PP", - "slogan": "Качественные продукты питания", - "rating": 4.4, - "verified": true, - "phone": "+7 (495) 888-99-00", - "email": "info@pishcheprom.ru", - "legalAddress": "г. Москва, ул. Пищевая, д. 40", - "foundedYear": 2011, - "employeeCount": "180 сотрудников" - }, - { - "id": "company-16", - "inn": "7730048048", - "ogrn": "1047730048048", - "fullName": "Общество с ограниченной ответственностью \"ЭнергоСервис\"", - "shortName": "ООО \"ЭнергоСервис\"", - "legalForm": "ООО", - "industry": "Энергетика", - "companySize": "50-100", - "website": "https://energoservice.ru", - "logo": "https://via.placeholder.com/100x100/F6AD55/FFFFFF?text=ES", - "slogan": "Энергетические решения", - "rating": 4.6, - "verified": true, - "phone": "+7 (495) 555-66-77", - "email": "info@energoservice.ru", - "legalAddress": "г. Москва, ул. Энергетическая, д. 25", - "foundedYear": 2013, - "employeeCount": "70 сотрудников" - }, - { - "id": "company-17", - "inn": "7730048049", - "ogrn": "1047730048049", - "fullName": "Общество с ограниченной ответственностью \"МедТех\"", - "shortName": "ООО \"МедТех\"", - "legalForm": "ООО", - "industry": "Медицина", - "companySize": "100-250", - "website": "https://medtech.ru", - "logo": "https://via.placeholder.com/100x100/E53E3E/FFFFFF?text=MT", - "slogan": "Медицинские технологии будущего", - "rating": 4.8, - "verified": true, - "phone": "+7 (495) 777-00-11", - "email": "info@medtech.ru", - "legalAddress": "г. Москва, ул. Медицинская, д. 35", - "foundedYear": 2015, - "employeeCount": "200 сотрудников" - }, - { - "id": "company-18", - "inn": "7730048050", - "ogrn": "1047730048050", - "fullName": "Общество с ограниченной ответственностью \"ОбразЦентр\"", - "shortName": "ООО \"ОбразЦентр\"", - "legalForm": "ООО", - "industry": "Образование", - "companySize": "50-100", - "website": "https://obrazcentr.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=OC", - "slogan": "Образование и развитие персонала", - "rating": 4.3, - "verified": true, - "phone": "+7 (495) 333-00-22", - "email": "info@obrazcentr.ru", - "legalAddress": "г. Москва, ул. Образовательная, д. 18", - "foundedYear": 2018, - "employeeCount": "60 сотрудников" - }, - { - "id": "company-19", - "inn": "7730048051", - "ogrn": "1047730048051", - "fullName": "Общество с ограниченной ответственностью \"ФинКонсалт\"", - "shortName": "ООО \"ФинКонсалт\"", - "legalForm": "ООО", - "industry": "Финансы", - "companySize": "10-50", - "website": "https://finkonsalt.ru", - "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=FK", - "slogan": "Финансовое консультирование", - "rating": 4.7, - "verified": true, - "phone": "+7 (495) 444-00-33", - "email": "info@finkonsalt.ru", - "legalAddress": "г. Москва, ул. Финансовая, д. 12", - "foundedYear": 2016, - "employeeCount": "35 сотрудников" - }, - { - "id": "company-20", - "inn": "7730048052", - "ogrn": "1047730048052", - "fullName": "Общество с ограниченной ответственностью \"АгроТех\"", - "shortName": "ООО \"АгроТех\"", - "legalForm": "ООО", - "industry": "Сельское хозяйство", - "companySize": "100-250", - "website": "https://agrotech.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=AT", - "slogan": "Современные технологии в сельском хозяйстве", - "rating": 4.5, - "verified": true, - "phone": "+7 (495) 666-00-44", - "email": "info@agrotech.ru", - "legalAddress": "г. Москва, ул. Аграрная, д. 28", - "foundedYear": 2012, - "employeeCount": "160 сотрудников" - } - ] -} diff --git a/server/routers/procurement/mocks/products.json b/server/routers/procurement/mocks/products.json deleted file mode 100644 index 9b9b9a6..0000000 --- a/server/routers/procurement/mocks/products.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "mockProducts": [ - { - "id": "prod-1", - "name": "Металлические конструкции", - "description": "Производство и поставка металлических конструкций любой сложности для строительства", - "category": "Строительные материалы", - "type": "sell", - "companyId": "company-4", - "price": "от 50 000 руб/тонна", - "createdAt": "{{date-10-days}}", - "updatedAt": "{{date-2-days}}" - }, - { - "id": "prod-2", - "name": "Стальные балки и профили", - "description": "Высококачественные стальные балки и профили для промышленного строительства", - "category": "Металлопрокат", - "type": "sell", - "companyId": "company-8", - "price": "от 45 000 руб/тонна", - "createdAt": "{{date-8-days}}", - "updatedAt": "{{date-1-day}}" - }, - { - "id": "prod-3", - "name": "Пластиковые изделия", - "description": "Производство пластиковых изделий для различных отраслей промышленности", - "category": "Пластик", - "type": "sell", - "companyId": "company-9", - "price": "от 200 руб/кг", - "createdAt": "{{date-15-days}}", - "updatedAt": "{{date-3-days}}" - }, - { - "id": "prod-4", - "name": "Строительные материалы", - "description": "Полный спектр строительных материалов для жилого и коммерческого строительства", - "category": "Строительные материалы", - "type": "sell", - "companyId": "company-1", - "price": "по запросу", - "createdAt": "{{date-20-days}}", - "updatedAt": "{{date-5-days}}" - }, - { - "id": "prod-5", - "name": "IT-решения для бизнеса", - "description": "Разработка программного обеспечения и IT-консалтинг для предприятий", - "category": "IT-услуги", - "type": "sell", - "companyId": "company-12", - "price": "от 100 000 руб/проект", - "createdAt": "{{date-12-days}}", - "updatedAt": "{{date-2-days}}" - }, - { - "id": "prod-6", - "name": "Логистические услуги", - "description": "Комплексные логистические услуги по всей России и СНГ", - "category": "Логистика", - "type": "sell", - "companyId": "company-5", - "price": "от 15 руб/км", - "createdAt": "{{date-18-days}}", - "updatedAt": "{{date-4-days}}" - }, - { - "id": "prod-7", - "name": "Пищевая продукция", - "description": "Производство качественных продуктов питания для HoReCa и розничной торговли", - "category": "Пищевая продукция", - "type": "sell", - "companyId": "company-15", - "price": "по прайс-листу", - "createdAt": "{{date-25-days}}", - "updatedAt": "{{date-7-days}}" - }, - { - "id": "prod-8", - "name": "Медицинское оборудование", - "description": "Поставка современного медицинского оборудования и расходных материалов", - "category": "Медицинское оборудование", - "type": "sell", - "companyId": "company-17", - "price": "по запросу", - "createdAt": "{{date-30-days}}", - "updatedAt": "{{date-10-days}}" - }, - { - "id": "prod-9", - "name": "Запчасти для спецтехники", - "description": "Ищем надежного поставщика запчастей для строительной техники Caterpillar, Komatsu", - "category": "Запчасти", - "type": "buy", - "companyId": "company-2", - "budget": "до 500 000 руб", - "createdAt": "{{date-5-days}}", - "updatedAt": "{{date-1-day}}" - }, - { - "id": "prod-10", - "name": "Сырье для производства", - "description": "Требуется качественное сырье для производства пластиковых изделий", - "category": "Сырье", - "type": "buy", - "companyId": "company-9", - "budget": "до 1 000 000 руб", - "createdAt": "{{date-7-days}}", - "updatedAt": "{{date-2-days}}" - }, - { - "id": "prod-11", - "name": "IT-оборудование", - "description": "Закупка серверного оборудования и сетевого оборудования для офиса", - "category": "IT-оборудование", - "type": "buy", - "companyId": "company-13", - "budget": "до 2 000 000 руб", - "createdAt": "{{date-3-days}}", - "updatedAt": "{{date-1-day}}" - }, - { - "id": "prod-12", - "name": "Консалтинговые услуги", - "description": "Требуется консультация по оптимизации бизнес-процессов", - "category": "Консалтинг", - "type": "buy", - "companyId": "company-14", - "budget": "до 300 000 руб", - "createdAt": "{{date-4-days}}", - "updatedAt": "{{date-1-day}}" - }, - { - "id": "prod-13", - "name": "Образовательные программы", - "description": "Поиск поставщика корпоративного обучения для сотрудников", - "category": "Образование", - "type": "buy", - "companyId": "company-18", - "budget": "до 200 000 руб", - "createdAt": "{{date-6-days}}", - "updatedAt": "{{date-2-days}}" - }, - { - "id": "prod-14", - "name": "Финансовые услуги", - "description": "Требуется консультация по инвестиционному планированию", - "category": "Финансовые услуги", - "type": "buy", - "companyId": "company-19", - "budget": "до 150 000 руб", - "createdAt": "{{date-2-days}}", - "updatedAt": "{{date-1-day}}" - } - ] -} diff --git a/server/routers/procurement/mocks/search.json b/server/routers/procurement/mocks/search.json deleted file mode 100644 index e9081c8..0000000 --- a/server/routers/procurement/mocks/search.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "suggestions": [ - "Строительные материалы", - "Металлоконструкции", - "Логистические услуги", - "Промышленное оборудование", - "Запчасти для спецтехники", - "IT-решения", - "Консалтинговые услуги", - "Пищевая продукция", - "Энергетическое оборудование", - "Медицинские технологии", - "Образовательные услуги", - "Финансовые услуги", - "Сельскохозяйственная техника", - "Торговое оборудование", - "Производственные услуги" - ], - "searchHistory": [ - { - "query": "строительные материалы", - "timestamp": "{{date-1-day}}" - }, - { - "query": "металлоконструкции", - "timestamp": "{{date-2-days}}" - }, - { - "query": "логистические услуги", - "timestamp": "{{date-3-days}}" - }, - { - "query": "IT-решения", - "timestamp": "{{date-5-days}}" - }, - { - "query": "консалтинг", - "timestamp": "{{date-7-days}}" - }, - { - "query": "пищевая продукция", - "timestamp": "{{date-10-days}}" - }, - { - "query": "медицинское оборудование", - "timestamp": "{{date-12-days}}" - }, - { - "query": "образовательные услуги", - "timestamp": "{{date-15-days}}" - }, - { - "query": "финансовые услуги", - "timestamp": "{{date-18-days}}" - }, - { - "query": "сельскохозяйственная техника", - "timestamp": "{{date-20-days}}" - } - ], - "savedSearches": [ - { - "id": "saved-1", - "name": "Строительные компании", - "params": { - "industries": ["Строительство"], - "minRating": 4.5 - }, - "createdAt": "{{date-7-days}}" - }, - { - "id": "saved-2", - "name": "Поставщики металла", - "params": { - "query": "металл", - "industries": ["Производство"] - }, - "createdAt": "{{date-14-days}}" - }, - { - "id": "saved-3", - "name": "IT-компании", - "params": { - "industries": ["IT"], - "minRating": 4.0 - }, - "createdAt": "{{date-21-days}}" - }, - { - "id": "saved-4", - "name": "Логистические услуги", - "params": { - "industries": ["Логистика"], - "companySize": ["100-250", "250-500"] - }, - "createdAt": "{{date-28-days}}" - }, - { - "id": "saved-5", - "name": "Консалтинговые услуги", - "params": { - "industries": ["Услуги"], - "minRating": 4.3 - }, - "createdAt": "{{date-35-days}}" - } - ], - "recommendationReasons": { - "Строительство": "Отличная репутация в строительной сфере", - "Производство": "Высокое качество производимой продукции", - "Логистика": "Надежные логистические решения", - "Торговля": "Широкий ассортимент и быстрые поставки", - "IT": "Инновационные IT-решения", - "Услуги": "Профессиональные консалтинговые услуги", - "Пищевая промышленность": "Качественная пищевая продукция", - "Энергетика": "Энергоэффективные решения", - "Медицина": "Современные медицинские технологии", - "Образование": "Эффективные образовательные программы", - "Финансы": "Надежные финансовые услуги", - "Сельское хозяйство": "Современные агротехнологии" - } -} diff --git a/server/routers/procurement/mocks/user.json b/server/routers/procurement/mocks/user.json deleted file mode 100644 index 1d12fb0..0000000 --- a/server/routers/procurement/mocks/user.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "mockUser": { - "id": "user-123", - "email": "test@company.com", - "firstName": "Иван", - "lastName": "Петров", - "position": "Генеральный директор" - }, - "mockTokens": { - "accessToken": "mock-access-token-{{timestamp}}", - "refreshToken": "mock-refresh-token-{{timestamp}}" - } -} diff --git a/server/routers/procurement/models/BuyDocument.js b/server/routers/procurement/models/BuyDocument.js new file mode 100644 index 0000000..34d2661 --- /dev/null +++ b/server/routers/procurement/models/BuyDocument.js @@ -0,0 +1,43 @@ +const mongoose = require('mongoose'); + +const buyDocumentSchema = new mongoose.Schema({ + id: { + type: String, + required: true, + unique: true, + index: true + }, + ownerCompanyId: { + type: String, + required: true, + index: true + }, + name: { + type: String, + required: true + }, + type: { + type: String, + required: true + }, + size: { + type: Number, + required: true + }, + filePath: { + type: String, + required: true + }, + acceptedBy: { + type: [String], + default: [] + }, + createdAt: { + type: Date, + default: Date.now, + index: true + } +}); + +module.exports = mongoose.model('BuyDocument', buyDocumentSchema); + diff --git a/server/routers/procurement/models/BuyProduct.js b/server/routers/procurement/models/BuyProduct.js index 5396ebd..6828b12 100644 --- a/server/routers/procurement/models/BuyProduct.js +++ b/server/routers/procurement/models/BuyProduct.js @@ -30,6 +30,7 @@ const buyProductSchema = new mongoose.Schema({ url: String, type: String, size: Number, + storagePath: String, uploadedAt: { type: Date, default: Date.now diff --git a/server/routers/procurement/models/Experience.js b/server/routers/procurement/models/Experience.js new file mode 100644 index 0000000..09dd018 --- /dev/null +++ b/server/routers/procurement/models/Experience.js @@ -0,0 +1,46 @@ +const mongoose = require('mongoose'); + +const experienceSchema = new mongoose.Schema({ + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true, + index: true + }, + confirmed: { + type: Boolean, + default: false + }, + customer: { + type: String, + required: true + }, + subject: { + type: String, + required: true + }, + volume: { + type: String + }, + contact: { + type: String + }, + comment: { + type: String + }, + createdAt: { + type: Date, + default: Date.now, + index: true + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +// Индексы для оптимизации поиска +experienceSchema.index({ companyId: 1, createdAt: -1 }); + +module.exports = mongoose.model('Experience', experienceSchema); + diff --git a/server/routers/procurement/models/Request.js b/server/routers/procurement/models/Request.js index 6a14fab..88f921d 100644 --- a/server/routers/procurement/models/Request.js +++ b/server/routers/procurement/models/Request.js @@ -11,6 +11,12 @@ const requestSchema = new mongoose.Schema({ required: true, index: true }, + subject: { + type: String, + required: false, + trim: true, + default: '' + }, text: { type: String, required: true @@ -21,6 +27,7 @@ const requestSchema = new mongoose.Schema({ url: String, type: String, size: Number, + storagePath: String, uploadedAt: { type: Date, default: Date.now @@ -39,6 +46,18 @@ const requestSchema = new mongoose.Schema({ type: String, default: null }, + responseFiles: [{ + id: String, + name: String, + url: String, + type: String, + size: Number, + storagePath: String, + uploadedAt: { + type: Date, + default: Date.now + } + }], respondedAt: { type: Date, default: null @@ -58,5 +77,6 @@ const requestSchema = new mongoose.Schema({ requestSchema.index({ senderCompanyId: 1, createdAt: -1 }); requestSchema.index({ recipientCompanyId: 1, createdAt: -1 }); requestSchema.index({ senderCompanyId: 1, recipientCompanyId: 1 }); +requestSchema.index({ subject: 1, createdAt: -1 }); module.exports = mongoose.model('Request', requestSchema); diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index 035d0b0..c9a2bba 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -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; diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index 53dadb5..d23ea01 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -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) diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js index 2aabc93..9ee74fe 100644 --- a/server/routers/procurement/routes/buyProducts.js +++ b/server/routers/procurement/routes/buyProducts.js @@ -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); diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js index 0c70e21..5fd84e9 100644 --- a/server/routers/procurement/routes/companies.js +++ b/server/routers/procurement/routes/companies.js @@ -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({ diff --git a/server/routers/procurement/routes/experience.js b/server/routers/procurement/routes/experience.js index fa224d4..dcea942 100644 --- a/server/routers/procurement/routes/experience.js +++ b/server/routers/procurement/routes/experience.js @@ -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) { diff --git a/server/routers/procurement/routes/home.js b/server/routers/procurement/routes/home.js index eabc4f4..82c87d2 100644 --- a/server/routers/procurement/routes/home.js +++ b/server/routers/procurement/routes/home.js @@ -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 }); } }); diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js index b60a055..7573d1f 100644 --- a/server/routers/procurement/routes/messages.js +++ b/server/routers/procurement/routes/messages.js @@ -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) { diff --git a/server/routers/procurement/routes/products.js b/server/routers/procurement/routes/products.js index 44490d4..9a09aaf 100644 --- a/server/routers/procurement/routes/products.js +++ b/server/routers/procurement/routes/products.js @@ -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); diff --git a/server/routers/procurement/routes/requests.js b/server/routers/procurement/routes/requests.js index a7ab999..7e62b15 100644 --- a/server/routers/procurement/routes/requests.js +++ b/server/routers/procurement/routes/requests.js @@ -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); diff --git a/server/routers/procurement/routes/reviews.js b/server/routers/procurement/routes/reviews.js index 40bcd7d..820c9f8 100644 --- a/server/routers/procurement/routes/reviews.js +++ b/server/routers/procurement/routes/reviews.js @@ -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), diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index b983e7f..84e38e6 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -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) { diff --git a/server/routers/procurement/scripts/init-database.js b/server/routers/procurement/scripts/init-database.js deleted file mode 100644 index afae63b..0000000 --- a/server/routers/procurement/scripts/init-database.js +++ /dev/null @@ -1,74 +0,0 @@ -const mongoose = require('mongoose'); -const { migrateCompanies } = require('./migrate-companies'); -require('dotenv').config({ path: '../../.env' }); - -const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -// Migration history model -const migrationSchema = new mongoose.Schema({ - name: { type: String, unique: true, required: true }, - executedAt: { type: Date, default: Date.now }, - status: { type: String, enum: ['completed', 'failed'], default: 'completed' }, - message: String -}, { collection: 'migrations' }); - -const Migration = mongoose.model('Migration', migrationSchema); - -async function initializeDatabase() { - try { - console.log('[Init] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - console.log('[Init] Connected to MongoDB\n'); - - // Check if migrations already ran - const migrateCompaniesRan = await Migration.findOne({ name: 'migrate-companies' }); - - if (!migrateCompaniesRan) { - console.log('[Init] Running migrate-companies migration...'); - try { - await migrateCompanies(); - - // Record successful migration - await Migration.create({ - name: 'migrate-companies', - status: 'completed', - message: 'Company data migration completed successfully' - }); - - console.log('[Init] ✅ migrate-companies recorded in database\n'); - } catch (err) { - // Record failed migration - await Migration.create({ - name: 'migrate-companies', - status: 'failed', - message: err.message - }); - console.error('[Init] ❌ migrate-companies failed:', err.message); - } - } else { - console.log('[Init] ℹ️ migrate-companies already executed:', migrateCompaniesRan.executedAt); - console.log('[Init] Skipping migration...\n'); - } - - await mongoose.connection.close(); - console.log('[Init] Database initialization complete\n'); - } catch (err) { - console.error('[Init] ❌ Error during database initialization:', err.message); - process.exit(1); - } -} - -module.exports = initializeDatabase; - -// Run directly if called as script -if (require.main === module) { - initializeDatabase().catch(err => { - console.error('Initialization failed:', err); - process.exit(1); - }); -} diff --git a/server/routers/procurement/scripts/migrate-companies.js b/server/routers/procurement/scripts/migrate-companies.js deleted file mode 100644 index 44fb465..0000000 --- a/server/routers/procurement/scripts/migrate-companies.js +++ /dev/null @@ -1,124 +0,0 @@ -const mongoose = require('mongoose'); -const Company = require('../models/Company'); -require('dotenv').config({ path: '../../.env' }); - -const industryMap = { - 'it': 'IT', - 'finance': 'Финансы', - 'manufacturing': 'Производство', - 'construction': 'Строительство', - 'retail': 'Розничная торговля', - 'wholesale': 'Оптовая торговля', - 'logistics': 'Логистика', - 'healthcare': 'Здравоохранение', - 'education': 'Образование', - 'consulting': 'Консалтинг', - 'marketing': 'Маркетинг', - 'realestate': 'Недвижимость', - 'food': 'Пищевая промышленность', - 'agriculture': 'Сельское хозяйство', - 'energy': 'Энергетика', - 'telecom': 'Телекоммуникации', - 'media': 'Медиа', - 'tourism': 'Туризм', - 'legal': 'Юридические услуги', - 'other': 'Другое' -}; - -const validIndustries = Object.values(industryMap); - -const industryAliases = { - 'Торговля': 'Розничная торговля', - 'торговля': 'Розничная торговля', - 'Trade': 'Розничная торговля' -}; - -async function migrateCompanies() { - try { - const allCompanies = await Company.find().exec(); - console.log(`[Migration] Found ${allCompanies.length} companies to process`); - - let fixedCount = 0; - let errorCount = 0; - - for (const company of allCompanies) { - let needsUpdate = false; - let updates = {}; - - // Check and fix industry field - if (company.industry) { - if (Array.isArray(company.industry)) { - console.log(`[FIX] ${company.fullName}: industry is array, converting to string`); - updates.industry = company.industry[0] || 'Другое'; - needsUpdate = true; - } else if (!validIndustries.includes(company.industry)) { - const mapped = industryAliases[company.industry]; - if (mapped) { - console.log(`[FIX] ${company.fullName}: "${company.industry}" → "${mapped}"`); - updates.industry = mapped; - needsUpdate = true; - } else { - console.log(`[WARN] ${company.fullName}: unknown industry "${company.industry}"`); - } - } - } - - // Check and fix companySize field - if (company.companySize && Array.isArray(company.companySize)) { - console.log(`[FIX] ${company.fullName}: companySize is array, converting to string`); - updates.companySize = company.companySize[0] || ''; - needsUpdate = true; - } - - if (needsUpdate) { - try { - await Company.updateOne({ _id: company._id }, { $set: updates }); - fixedCount++; - console.log(` ✅ Updated`); - } catch (err) { - console.error(` ❌ Error: ${err.message}`); - errorCount++; - } - } - } - - console.log('\n[Migration] === MIGRATION SUMMARY ==='); - console.log(`[Migration] Total companies: ${allCompanies.length}`); - console.log(`[Migration] Fixed: ${fixedCount}`); - console.log(`[Migration] Errors: ${errorCount}`); - - if (fixedCount === 0 && errorCount === 0) { - console.log('[Migration] ✅ No migration needed - all data is valid!'); - } else if (errorCount === 0) { - console.log('[Migration] ✅ Migration completed successfully!'); - } else { - console.log('[Migration] ⚠️ Migration completed with errors.'); - } - } catch (err) { - console.error('[Migration] ❌ Error:', err.message); - throw err; - } -} - -module.exports = { - migrateCompanies: migrateCompanies -}; - -// Run directly if called as script -if (require.main === module) { - const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; - - mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }).then(async () => { - console.log('[Migration] Connected to MongoDB\n'); - await migrateCompanies(); - await mongoose.connection.close(); - }).catch(err => { - console.error('[Migration] ❌ Error:', err.message); - process.exit(1); - }); -} diff --git a/server/routers/procurement/scripts/migrate-messages.js b/server/routers/procurement/scripts/migrate-messages.js index dba2b66..d342f44 100644 --- a/server/routers/procurement/scripts/migrate-messages.js +++ b/server/routers/procurement/scripts/migrate-messages.js @@ -6,17 +6,14 @@ const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procureme async function migrateMessages() { try { - // Check if connection exists, if not connect - if (mongoose.connection.readyState === 0) { - console.log('[Migration] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - console.log('[Migration] Connected to MongoDB'); - } + console.log('[Migration] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('[Migration] Connected to MongoDB'); // Найти все сообщения const allMessages = await Message.find().exec(); @@ -84,18 +81,13 @@ async function migrateMessages() { console.log('[Migration] ✅ Migration completed!'); console.log('[Migration] Fixed:', fixedCount, 'messages'); console.log('[Migration] Errors:', errorCount); + + await mongoose.connection.close(); + console.log('[Migration] Disconnected from MongoDB'); } catch (err) { console.error('[Migration] ❌ Error:', err.message); - throw err; + process.exit(1); } } -module.exports = { migrateMessages }; - -// Run directly if called as script -if (require.main === module) { - migrateMessages().catch(err => { - console.error('Migration failed:', err); - process.exit(1); - }); -} +migrateMessages(); diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 211b8a6..40694c9 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -1,32 +1,62 @@ const mongoose = require('mongoose'); +const path = require('path'); require('dotenv').config(); // Импорт моделей -const User = require('../models/User'); -const Company = require('../models/Company'); +const User = require(path.join(__dirname, '..', 'models', 'User')); +const Company = require(path.join(__dirname, '..', 'models', 'Company')); + +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 () => { + try { + console.log('\n📡 Подключение к MongoDB (PRIMARY)...'); + await mongoose.connect(primaryUri, { useNewUrlParser: true, useUnifiedTopology: true }); + console.log('✅ Подключено к PRIMARY MongoDB'); + } catch (primaryError) { + console.error('❌ Ошибка PRIMARY подключения:', primaryError.message); + + const requiresFallback = + primaryError.code === 18 || primaryError.code === 13 || String(primaryError.message || '').includes('auth'); + + if (!requiresFallback) { + throw primaryError; + } + + console.log('\n📡 Подключение к MongoDB (FALLBACK)...'); + await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true }); + console.log('✅ Подключено к FALLBACK MongoDB'); + } +}; const recreateTestUser = async () => { try { - console.log('[Migration] Processing test user creation...'); + await connectWithFallback(); + + const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796'); + const presetUserEmail = 'admin@test-company.ru'; // Удалить старого тестового пользователя - console.log('[Migration] Removing old test user...'); - const oldUser = await User.findOne({ email: 'admin@test-company.ru' }); + console.log('🗑️ Удаление старого тестового пользователя...'); + const oldUser = await User.findOne({ email: presetUserEmail }); if (oldUser) { // Удалить связанную компанию if (oldUser.companyId) { await Company.findByIdAndDelete(oldUser.companyId); - console.log('[Migration] ✓ Old company removed'); + console.log(' ✓ Старая компания удалена'); } await User.findByIdAndDelete(oldUser._id); - console.log('[Migration] ✓ Old user removed'); + console.log(' ✓ Старый пользователь удален'); } else { - console.log('[Migration] ℹ️ Old user not found'); + console.log(' ℹ️ Старый пользователь не найден'); } // Создать новую компанию с правильной кодировкой UTF-8 - console.log('[Migration] Creating test company...'); + console.log('\n🏢 Создание тестовой компании...'); const company = await Company.create({ + _id: presetCompanyId, fullName: 'ООО "Тестовая Компания"', inn: '1234567890', ogrn: '1234567890123', @@ -40,12 +70,12 @@ const recreateTestUser = async () => { reviewsCount: 10, dealsCount: 25, }); - console.log('[Migration] ✓ Company created:', company.fullName); + console.log(' ✓ Компания создана:', company.fullName); // Создать нового пользователя с правильной кодировкой UTF-8 - console.log('[Migration] Creating test user...'); + console.log('\n👤 Создание тестового пользователя...'); const user = await User.create({ - email: 'admin@test-company.ru', + email: presetUserEmail, password: 'SecurePass123!', firstName: 'Иван', lastName: 'Иванов', @@ -53,10 +83,24 @@ const recreateTestUser = async () => { phone: '+7 (999) 123-45-67', companyId: company._id, }); - console.log('[Migration] ✓ User created:', user.firstName, user.lastName); + console.log(' ✓ Пользователь создан:', user.firstName, user.lastName); + + // Проверка что данные сохранены правильно + console.log('\n✅ Проверка данных:'); + console.log(' Email:', user.email); + console.log(' Имя:', user.firstName); + console.log(' Фамилия:', user.lastName); + console.log(' Компания:', company.fullName); + console.log(' Должность:', user.position); + + console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8'); + console.log('\n📋 Данные для входа:'); + console.log(' Email: admin@test-company.ru'); + console.log(' Пароль: SecurePass123!'); + console.log(''); // Обновить существующие mock компании - console.log('[Migration] Updating existing companies...'); + console.log('\n🔄 Обновление существующих mock компаний...'); const updates = [ { inn: '7707083894', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, { inn: '7707083895', updates: { companySize: '500+', partnerGeography: ['moscow', 'russia_all'] } }, @@ -67,33 +111,18 @@ const recreateTestUser = async () => { for (const item of updates) { await Company.updateOne({ inn: item.inn }, { $set: item.updates }); - console.log(`[Migration] ✓ Company updated: INN ${item.inn}`); + console.log(` ✓ Компания обновлена: INN ${item.inn}`); } - console.log('[Migration] ✅ Test user migration completed!'); + await mongoose.connection.close(); + process.exit(0); } catch (error) { - console.error('[Migration] ❌ Error:', error.message); - throw error; + console.error('\n❌ Ошибка:', error.message); + console.error(error); + process.exit(1); } }; -module.exports = { recreateTestUser }; - -// Run directly if called as script -if (require.main === module) { - const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; - - mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - }).then(async () => { - console.log('[Migration] Connected to MongoDB\n'); - await recreateTestUser(); - await mongoose.connection.close(); - process.exit(0); - }).catch(err => { - console.error('[Migration] ❌ Error:', err.message); - process.exit(1); - }); -} +// Запуск +recreateTestUser(); diff --git a/server/routers/procurement/scripts/run-migrations.js b/server/routers/procurement/scripts/run-migrations.js deleted file mode 100644 index 1c59fbc..0000000 --- a/server/routers/procurement/scripts/run-migrations.js +++ /dev/null @@ -1,117 +0,0 @@ -const mongoose = require('mongoose'); -const { migrateCompanies } = require('./migrate-companies'); -const { migrateMessages } = require('./migrate-messages'); -const { recreateTestUser } = require('./recreate-test-user'); -require('dotenv').config(); - -const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -// Migration history model -const migrationSchema = new mongoose.Schema({ - name: { type: String, unique: true, required: true }, - executedAt: { type: Date, default: Date.now }, - status: { type: String, enum: ['completed', 'failed'], default: 'completed' }, - message: String -}, { collection: 'migrations' }); - -const Migration = mongoose.model('Migration', migrationSchema); - -const migrations = [ - { name: 'migrate-companies', fn: migrateCompanies }, - { name: 'migrate-messages', fn: migrateMessages }, - { name: 'recreate-test-user', fn: recreateTestUser } -]; - -async function runMigrations(shouldCloseConnection = false) { - let mongooseConnected = false; - - try { - console.log('\n' + '='.repeat(60)); - console.log('🚀 Starting Database Migrations'); - console.log('='.repeat(60) + '\n'); - - // Only connect if not already connected - if (mongoose.connection.readyState === 0) { - console.log('[Migrations] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - mongooseConnected = true; - console.log('[Migrations] ✅ Connected to MongoDB\n'); - } else { - console.log('[Migrations] ✅ Using existing MongoDB connection\n'); - } - - for (const migration of migrations) { - console.log(`[${migration.name}] Starting...`); - - try { - // Check if already executed - const existing = await Migration.findOne({ name: migration.name }); - - if (existing) { - console.log(`[${migration.name}] ℹ️ Already executed at: ${existing.executedAt.toISOString()}`); - console.log(`[${migration.name}] Status: ${existing.status}`); - if (existing.message) console.log(`[${migration.name}] Message: ${existing.message}`); - console.log(''); - continue; - } - - // Run migration - await migration.fn(); - - // Record successful migration - await Migration.create({ - name: migration.name, - status: 'completed', - message: `${migration.name} executed successfully` - }); - - console.log(`[${migration.name}] ✅ Completed and recorded\n`); - } catch (error) { - console.error(`[${migration.name}] ❌ Error: ${error.message}\n`); - - // Record failed migration - try { - await Migration.create({ - name: migration.name, - status: 'failed', - message: error.message - }); - } catch (recordErr) { - // Ignore if we can't record the failure - } - } - } - - console.log('='.repeat(60)); - console.log('✅ All migrations processed'); - console.log('='.repeat(60) + '\n'); - - } catch (error) { - console.error('\n❌ Fatal migration error:', error.message); - console.error(error); - if (shouldCloseConnection) { - process.exit(1); - } - } finally { - // Only close connection if we created it and requested to close - if (mongooseConnected && shouldCloseConnection) { - await mongoose.connection.close(); - console.log('[Migrations] Disconnected from MongoDB\n'); - } - } -} - -module.exports = { runMigrations, Migration }; - -// Run directly if called as script -if (require.main === module) { - runMigrations(true).catch(err => { - console.error('Migration failed:', err); - process.exit(1); - }); -} diff --git a/server/routers/procurement/scripts/test-logging.js b/server/routers/procurement/scripts/test-logging.js new file mode 100644 index 0000000..e914f23 --- /dev/null +++ b/server/routers/procurement/scripts/test-logging.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +/** + * Скрипт для тестирования логирования + * + * Использование: + * node stubs/scripts/test-logging.js # Логи скрыты (DEV не установлена) + * DEV=true node stubs/scripts/test-logging.js # Логи видны + */ + +// Функция логирования из маршрутов +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + +console.log(''); +console.log('='.repeat(60)); +console.log('TEST: Логирование с переменной окружения DEV'); +console.log('='.repeat(60)); +console.log(''); + +console.log('Значение DEV:', process.env.DEV || '(не установлена)'); +console.log(''); + +// Тестируем различные логи +log('[Auth] Token verified - userId: 68fe2ccda3526c303ca06799 companyId: 68fe2ccda3526c303ca06796'); +log('[Auth] Generating token for userId:', '68fe2ccda3526c303ca06799'); +log('[BuyProducts] Found', 0, 'products for company 68fe2ccda3526c303ca06796'); +log('[Products] GET Fetching products for companyId:', '68fe2ccda3526c303ca06799'); +log('[Products] Found', 1, 'products'); +log('[Reviews] Returned', 0, 'reviews for company 68fe2ccda3526c303ca06796'); +log('[Messages] Fetching threads for companyId:', '68fe2ccda3526c303ca06796'); +log('[Messages] Found', 4, 'messages for company'); +log('[Messages] Returned', 3, 'unique threads'); +log('[Search] Request params:', { query: '', page: 1 }); + +console.log(''); +console.log('='.repeat(60)); +console.log('РЕЗУЛЬТАТ:'); +console.log('='.repeat(60)); + +if (process.env.DEV === 'true') { + console.log('✅ DEV=true - логи ВИДНЫ выше'); +} else { + console.log('❌ DEV не установлена или != "true" - логи СКРЫТЫ'); + console.log(''); + console.log('Для включения логов запустите:'); + console.log(' export DEV=true && npm start (Linux/Mac)'); + console.log(' $env:DEV = "true"; npm start (PowerShell)'); + console.log(' set DEV=true && npm start (CMD)'); +} + +console.log(''); +console.log('='.repeat(60)); +console.log(''); diff --git a/server/routers/procurement/scripts/validate-companies.js b/server/routers/procurement/scripts/validate-companies.js deleted file mode 100644 index cbf1147..0000000 --- a/server/routers/procurement/scripts/validate-companies.js +++ /dev/null @@ -1,93 +0,0 @@ -const mongoose = require('mongoose'); -const Company = require('../models/Company'); -require('dotenv').config({ path: '../../.env' }); - -const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -const industryMap = { - 'it': 'IT', - 'finance': 'Финансы', - 'manufacturing': 'Производство', - 'construction': 'Строительство', - 'retail': 'Розничная торговля', - 'wholesale': 'Оптовая торговля', - 'logistics': 'Логистика', - 'healthcare': 'Здравоохранение', - 'education': 'Образование', - 'consulting': 'Консалтинг', - 'marketing': 'Маркетинг', - 'realestate': 'Недвижимость', - 'food': 'Пищевая промышленность', - 'agriculture': 'Сельское хозяйство', - 'energy': 'Энергетика', - 'telecom': 'Телекоммуникации', - 'media': 'Медиа', - 'tourism': 'Туризм', - 'legal': 'Юридические услуги', - 'other': 'Другое' -}; - -async function validateCompanies() { - try { - console.log('[Validation] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - console.log('[Validation] Connected to MongoDB\n'); - - const allCompanies = await Company.find().exec(); - console.log(`Found ${allCompanies.length} total companies\n`); - - console.log('=== COMPANY DATA VALIDATION REPORT ===\n'); - - let issuesFound = 0; - let validCompanies = 0; - - for (const company of allCompanies) { - console.log(`📋 Company: ${company.fullName}`); - console.log(` ID: ${company._id}`); - console.log(` Industry: ${company.industry} (type: ${typeof company.industry})`); - console.log(` Company Size: ${company.companySize}`); - - let hasIssues = false; - - if (company.industry) { - if (Array.isArray(company.industry)) { - console.log(` ⚠️ WARNING: industry is array!`); - issuesFound++; - hasIssues = true; - } else if (!Object.values(industryMap).includes(company.industry)) { - console.log(` ⚠️ industry value unknown: "${company.industry}"`); - issuesFound++; - hasIssues = true; - } else { - console.log(` ✅ industry OK`); - } - } - - if (!hasIssues) validCompanies++; - console.log(''); - } - - console.log('\n=== SUMMARY ==='); - console.log(`Total: ${allCompanies.length}`); - console.log(`Valid: ${validCompanies}`); - console.log(`Issues: ${issuesFound}`); - - if (issuesFound === 0) { - console.log('\n✅ All data OK. No migration needed.'); - } else { - console.log('\n⚠️ Migration recommended.'); - } - - await mongoose.connection.close(); - } catch (err) { - console.error('❌ Error:', err.message); - process.exit(1); - } -} - -validateCompanies();