# app/pages/Admin/Import/index.coffee ImportService = require 'app/services/ImportService' ProductService = require 'app/services/ProductService' CategoryService = require 'app/services/CategoryService' FileUpload = require 'app/components/Admin/FileUpload/index.coffee' DataTable = require 'app/components/Admin/DataTable/index.coffee' if globalThis.stylFns and globalThis.stylFns['app/pages/Admin/Import/index.styl'] styleElement = document.createElement('style') styleElement.type = 'text/css' styleElement.textContent = globalThis.stylFns['app/pages/Admin/Import/index.styl'] document.head.appendChild(styleElement) module.exports = { components: { 'file-upload': FileUpload 'data-table': DataTable } data: -> { currentStep: 1 selectedFile: null csvFields: [] csvData: [] fieldMapping: {} previewData: [] previewLoading: false importing: false importProgress: 0 processedItems: 0 totalItems: 0 importStats: null importErrors: [] importComplete: false newCategories: [] servicesInitialized: false initializingServices: false } computed: previewColumns: -> [ { key: 'name', title: 'Название' } { key: 'sku', title: 'Артикул' } { key: 'price', title: 'Цена' } { key: 'brand', title: 'Бренд' } { key: 'category', title: 'Категория' } ] methods: onFileSelect: (files) -> @selectedFile = files[0] @parseCSVFile() parseCSVFile: -> return unless @selectedFile reader = new FileReader() reader.onload = (e) => try log '📊 Начало парсинга CSV файла' # Используем Papa Parse с настройками для русского CSV results = Papa.parse(e.target.result, { header: true delimiter: ';' skipEmptyLines: true encoding: 'UTF-8' quoteChar: '"' escapeChar: '"' dynamicTyping: false # Оставляем все как строки }) if results.errors.length > 0 log '⚠️ Предупреждения при парсинге CSV: ' + JSON.stringify(results.errors) @csvData = results.data.filter (row) -> # Фильтруем пустые строки и строки без обязательных полей row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*'] @csvFields = Object.keys(@csvData[0] || {}) log '📊 CSV файл распарсен успешно:' log 'Количество записей: ' + @csvData.length log 'Поля CSV: ' + @csvFields.join(', ') # Выводим первую запись для отладки if @csvData.length > 0 log 'Пример первой записи: ' + JSON.stringify(@csvData[0], null, 2) @initializeFieldMapping() catch error log '❌ Ошибка парсинга CSV: ' + error.message console.error('Ошибка парсинга:', error) @$emit('show-notification', 'Ошибка чтения CSV файла: ' + error.message, 'error') reader.onerror = (error) => log '❌ Ошибка чтения файла: ' + error @$emit('show-notification', 'Ошибка чтения файла', 'error') reader.readAsText(@selectedFile, 'UTF-8') initializeFieldMapping: -> @fieldMapping = {} # Автоматическое сопоставление полей по ключевым словам mappingRules = [ { pattern: 'артикул', field: 'sku' } { pattern: 'название', field: 'name' } { pattern: 'цена, руб', field: 'price' } { pattern: 'цена до скидки', field: 'oldPrice' } { pattern: 'бренд', field: 'brand' } { pattern: 'тип', field: 'category' } { pattern: 'описание', field: 'description' } { pattern: 'аннотация', field: 'description' } { pattern: 'ссылка на главное фото', field: 'mainImage' } { pattern: 'ссылки на дополнительные фото', field: 'additionalImages' } ] @csvFields.forEach (csvField) => lowerField = csvField.toLowerCase() matched = false for rule in mappingRules if lowerField.includes(rule.pattern) @fieldMapping[csvField] = rule.field matched = true break if not matched and csvField != '№' @fieldMapping[csvField] = '' # Не импортировать log '🔄 Автоматическое сопоставление полей: ' + JSON.stringify(@fieldMapping) nextStep: -> @currentStep++ prevStep: -> @currentStep-- validateMapping: -> requiredFields = ['name', 'sku', 'price'] mappedFields = Object.values(@fieldMapping) missingFields = requiredFields.filter (field) -> not mappedFields.includes(field) if missingFields.length > 0 @$emit('show-notification', 'Заполните обязательные поля: ' + missingFields.join(', '), 'error') return @generatePreview() @nextStep() generatePreview: -> @previewLoading = true try # Обрабатываем только первые 50 записей для предпросмотра @previewData = @csvData.slice(0, 50).map (row, index) => @transformRowToProduct(row, index) # Сбор уникальных категорий categorySet = new Set() @previewData.forEach (item) -> if item.category categorySet.add(item.category) @newCategories = Array.from(categorySet) @previewLoading = false log '👀 Предпросмотр сгенерирован: ' + @previewData.length + ' товаров' log '📂 Новые категории: ' + @newCategories.join(', ') catch error log '❌ Ошибка генерации предпросмотра: ' + error console.error('Ошибка генерации предпросмотра:', error) @$emit('show-notification', 'Ошибка обработки данных: ' + error.message, 'error') @previewLoading = false transformRowToProduct: (row, index) -> # Получаем значения по маппингу sku = row[@getMappedField('sku')] || '' name = row[@getMappedField('name')] || '' price = @parsePrice(row[@getMappedField('price')]) oldPrice = @parsePrice(row[@getMappedField('oldPrice')]) brand = row[@getMappedField('brand')] || '' category = row[@getMappedField('category')] || '' description = row[@getMappedField('description')] || '' mainImage = row[@getMappedField('mainImage')] || '' additionalImages = row[@getMappedField('additionalImages')] || '' # Создаем базовый объект товара product = { _id: 'product:' + (sku || 'temp_' + index) name: name sku: sku price: price oldPrice: oldPrice brand: brand category: category description: description domains: [window.location.hostname] type: 'product' active: true inStock: true images: [] attributes: {} createdAt: new Date().toISOString() updatedAt: new Date().toISOString() } # Обработка изображений if mainImage product.images.push({ url: mainImage.trim() type: 'main' order: 0 }) if additionalImages # Разбиваем строку с дополнительными изображениями по переносам imageUrls = additionalImages.split('\n').filter (url) -> url.trim() imageUrls.slice(0, 10).forEach (imgUrl, imgIndex) -> if imgUrl.trim() product.images.push({ url: imgUrl.trim() type: 'additional' order: imgIndex + 1 }) # Обработка дополнительных полей как атрибутов @csvFields.forEach (field) => if not @fieldMapping[field] and row[field] and field != '№' product.attributes[field] = row[field].toString().trim() # Обработка Rich-контента если есть if row['Rich-контент JSON'] try richContent = JSON.parse(row['Rich-контент JSON']) product.richContent = @jsonToMarkdown(richContent) catch error log '⚠️ Ошибка парсинга Rich-контента в строке ' + index + ': ' + error log '🔄 Трансформирован товар ' + index + ': ' + product.name return product parsePrice: (priceStr) -> return null unless priceStr try # Удаляем пробелы (например, "1 056,00" -> "1056,00") # Заменяем запятые на точки и удаляем все нецифровые символы кроме точки cleaned = priceStr.toString() .replace(/\s/g, '') # Удаляем пробелы .replace(',', '.') # Заменяем запятые на точки .replace(/[^\d.]/g, '') # Удаляем все кроме цифр и точек price = parseFloat(cleaned) return if isNaN(price) then null else price catch return null jsonToMarkdown: (richContent) -> markdown = '' if richContent and richContent.content richContent.content.forEach (block) -> if block.widgetName == 'raTextBlock' and block.text and block.text.items block.text.items.forEach (item) -> if item.type == 'text' and item.content markdown += item.content + '\n\n' else if item.type == 'br' markdown += '\n' return markdown.trim() getMappedField: (targetField) -> for csvField, mappedField of @fieldMapping return csvField if mappedField == targetField return '' initializeServices: -> return Promise.resolve() if @servicesInitialized if @initializingServices # Ждем завершения предыдущей инициализации return new Promise (resolve) => checkInterval = setInterval (=> if @servicesInitialized clearInterval(checkInterval) resolve() ), 100 @initializingServices = true log '🔄 Инициализация сервисов перед импортом...' try # Инициализируем все необходимые сервисы await ProductService.init() log '✅ ProductService инициализирован' await CategoryService.init() log '✅ CategoryService инициализирован' #await ImportService.init() log '✅ ImportService инициализирован' @servicesInitialized = true @initializingServices = false log '🎉 Все сервисы инициализированы' return Promise.resolve() catch error @initializingServices = false log '❌ Ошибка инициализации сервисов: ' + error throw error startImport: -> try # Сначала инициализируем сервисы @currentStep = 4 @importing = true @importProgress = 0 @processedItems = 0 @totalItems = @csvData.length @importErrors = [] @importStats = { success: 0 errors: 0 newCategories: 0 } log '🔄 Инициализация сервисов перед импортом...' await @initializeServices() log '🚀 Начало импорта ' + @totalItems + ' товаров' @processImportBatch() catch error log '❌ Ошибка инициализации перед импортом: ' + error @importing = false @$emit('show-notification', 'Ошибка инициализации: ' + error.message, 'error') processImportBatch: (batchSize = 10, batchIndex = 0) -> batches = [] for i in [0...@csvData.length] by batchSize batches.push(@csvData.slice(i, i + batchSize)) processBatch = (currentBatchIndex) => batch = batches[currentBatchIndex] return unless batch try log '🔄 Обработка пакета ' + (currentBatchIndex + 1) + ' из ' + batches.length # Трансформация данных products = batch.map (row, index) => globalIndex = currentBatchIndex * batchSize + index product = @transformRowToProduct(row, globalIndex) log '📦 Трансформирован товар: ' + product.name + ' (SKU: ' + product.sku + ')' return product # Создание категорий await @createMissingCategories(products) # Сохранение товаров log '💾 Сохранение пакета товаров в базу...' results = await ProductService.bulkSaveProducts(products) # Логируем результат сохранения log '✅ Результат сохранения пакета:' log 'Успешно: ' + results.success.length log 'Ошибок: ' + results.errors.length if results.errors.length > 0 results.errors.forEach (error) -> log '❌ Ошибка сохранения товара: ' + error.error # Обновление прогресса @processedItems += batch.length @importProgress = Math.round((@processedItems / @totalItems) * 100) @importStats.success += results.success.length @importStats.errors += results.errors.length # Сохранение ошибок results.errors.forEach (error) => @importErrors.push({ row: currentBatchIndex * batchSize + error.index message: error.error }) log '📊 Прогресс импорта: ' + @importProgress + '% (' + @processedItems + ' из ' + @totalItems + ')' # Следующий пакет или завершение if currentBatchIndex < batches.length - 1 setTimeout (=> @processImportBatch(batchSize, currentBatchIndex + 1) ), 500 else @finishImport() catch error log '❌ Ошибка обработки пакета ' + currentBatchIndex + ': ' + error console.error('Ошибка пакета:', error) @importErrors.push({ row: currentBatchIndex * batchSize message: 'Ошибка пакета: ' + error.message }) @finishImport() processBatch(batchIndex) createMissingCategories: (products) -> # Собираем уникальные категории из товаров categories = [] products.forEach (product) -> if product.category and product.category.trim() and not categories.includes(product.category) categories.push(product.category.trim()) log '📂 Категории для создания: ' + categories.join(', ') createdCount = 0 pouchService = require 'app/utils/pouch' for categoryName in categories try # Создаем простой slug slug = @simpleSlugify(categoryName) categoryId = 'category:' + slug log '🔍 Проверка категории: ' + categoryName # Проверка 1: По ID (slug) categoryExists = false try existingCategory = await pouchService.getDocument(categoryId) categoryExists = true log '⚠️ Категория существует по ID: ' + categoryName catch error if error.status != 404 log '❌ Ошибка проверки по ID: ' + error.message # Проверка 2: По имени (если не нашли по ID) if not categoryExists try # Ищем категории с таким же именем categoriesResult = await pouchService.allDocs({ startkey: 'category:' endkey: 'category:\ufff0' include_docs: true }) sameNameCategory = categoriesResult.rows.find (row) -> row.doc.name.toLowerCase().trim() == categoryName.toLowerCase().trim() if sameNameCategory categoryExists = true log '⚠️ Категория существует по имени: ' + categoryName + ' (ID: ' + sameNameCategory.doc._id + ')' catch error log '❌ Ошибка проверки по имени: ' + error.message # Если категория существует, пропускаем создание if categoryExists continue # Создаем новую категорию log '🆕 Создание категории: ' + categoryName + ' (ID: ' + categoryId + ')' categoryDoc = { _id: categoryId type: 'category' name: categoryName slug: slug domains: [window.location.hostname] active: true order: 0 description: '' createdAt: new Date().toISOString() updatedAt: new Date().toISOString() } result = await pouchService.saveDocument(categoryDoc) if result and result.ok createdCount++ @importStats.newCategories++ log '✅ Создана категория: ' + categoryName else log '❌ Ошибка сохранения категории' catch error if error.status == 409 log '⚠️ Категория уже существует (конфликт): ' + categoryName else log '❌ Ошибка создания категории ' + categoryName + ': ' + error.message log '📊 Создано категорий: ' + createdCount return createdCount simpleSlugify: (text) -> return 'cat-' + Date.now() unless text # Простейшая транслитерация text = text.toString().trim().toLowerCase() slug = text.replace(/\s+/g, '-').replace(/[^a-z0-9\-]/g, '') if slug.length == 0 slug = 'cat-' + Date.now() return slug slugify: (text) -> # Базовая проверка return 'category-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5) unless text text = text.toString().trim() return 'category-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5) if text.length == 0 # Простая транслитерация кириллицы translitMap = { 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya' } # Транслитерация slug = '' for char in text.toLowerCase() if translitMap[char] slug += translitMap[char] else if char.match(/[a-z0-9]/) slug += char else if char == ' ' or char == '-' slug += '-' # Очистка slug slug = slug .replace(/\-\-+/g, '-') .replace(/^-+/, '') .replace(/-+$/, '') # Если slug пустой, создаем уникальный if not slug or slug.length == 0 slug = 'category-' + Date.now() + '-' + Math.random().toString(36).substr(2, 5) log '🔧 Создан slug: ' + text + ' -> ' + slug return slug finishImport: -> @importing = false @importComplete = true log '🎉 Импорт завершен!' log '📊 Итоговая статистика:' log 'Успешно: ' + @importStats.success log 'Ошибок: ' + @importStats.errors log 'Новых категорий: ' + @importStats.newCategories if @importStats.errors == 0 message = 'Импорт завершен успешно! Обработано ' + @importStats.success + ' товаров' @$emit('show-notification', message, 'success') else message = 'Импорт завершен с ошибками. Успешно: ' + @importStats.success + ', Ошибок: ' + @importStats.errors @$emit('show-notification', message, 'warning') cancelImport: -> @importing = false @importComplete = true @$emit('show-notification', 'Импорт отменен', 'info') resetImport: -> @currentStep = 1 @selectedFile = null @csvFields = [] @csvData = [] @fieldMapping = {} @previewData = [] @importStats = null @importErrors = [] @importComplete = false mounted: -> log '📥 Компонент импорта загружен' # Предварительная инициализация сервисов при загрузке компонента @initializeServices().catch (error) -> log '⚠️ Предварительная инициализация сервисов не удалась: ' + error render: (new Function '_ctx', '_cache', globalThis.renderFns['app/pages/Admin/Import/index.pug'])() }