|
@@ -0,0 +1,597 @@
|
|
|
|
|
+# 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'])()
|
|
|
|
|
+}
|