| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- # 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'])()
- }
|