|
|
@@ -93,34 +93,13 @@ module.exports =
|
|
|
return products
|
|
|
|
|
|
methods:
|
|
|
- getStatusClass: (isActive) ->
|
|
|
- baseClass = 'admin-products__status'
|
|
|
- if isActive
|
|
|
- return "#{baseClass} admin-products__status--active"
|
|
|
- else
|
|
|
- return "#{baseClass} admin-products__status--inactive"
|
|
|
-
|
|
|
- formatPrice: (price) ->
|
|
|
- return '0 ₽' if !price
|
|
|
- new Intl.NumberFormat('ru-RU', {
|
|
|
- style: 'currency'
|
|
|
- currency: 'RUB'
|
|
|
- minimumFractionDigits: 0
|
|
|
- }).format(price)
|
|
|
-
|
|
|
- getCategoryName: (categoryId) ->
|
|
|
- category = @categories.find (cat) -> cat._id == categoryId
|
|
|
- category?.name || 'Без категории'
|
|
|
-
|
|
|
- getCategoryProductCount: (categoryId) ->
|
|
|
- @products.filter((product) -> product.category == categoryId).length
|
|
|
- loadProducts: ->
|
|
|
- PouchDB.queryView('admin', 'products', { include_docs: true })
|
|
|
- .then (result) =>
|
|
|
- @products = result.rows.map (row) -> row.doc
|
|
|
- .catch (error) =>
|
|
|
- debug.log 'Ошибка загрузки товаров:', error
|
|
|
- @showNotification 'Ошибка загрузки товаров', 'error'
|
|
|
+ loadProducts: ->
|
|
|
+ PouchDB.queryView('admin', 'products', { include_docs: true })
|
|
|
+ .then (result) =>
|
|
|
+ @products = result.rows.map (row) -> row.doc
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log 'Ошибка загрузки товаров:', error
|
|
|
+ @showNotification 'Ошибка загрузки товаров', 'error'
|
|
|
|
|
|
loadCategories: ->
|
|
|
PouchDB.queryView('admin', 'categories', { include_docs: true })
|
|
|
@@ -136,8 +115,471 @@ module.exports =
|
|
|
.catch (error) =>
|
|
|
debug.log 'Ошибка загрузки доменов:', error
|
|
|
|
|
|
+ getCategoryName: (categoryId) ->
|
|
|
+ category = @categories.find (cat) -> cat._id == categoryId
|
|
|
+ category?.name || 'Без категории'
|
|
|
+
|
|
|
+ getCategoryProductCount: (categoryId) ->
|
|
|
+ @products.filter((product) -> product.category == categoryId).length
|
|
|
+
|
|
|
+ # Улучшенный импорт товаров с полной обработкой CSV
|
|
|
+ onFileSelect: (event) ->
|
|
|
+ @selectedFile = event.target.files[0]
|
|
|
+ @importResults = null
|
|
|
+
|
|
|
+ importProducts: ->
|
|
|
+ if !@selectedFile
|
|
|
+ @showNotification 'Выберите файл для импорта', 'error'
|
|
|
+ return
|
|
|
+
|
|
|
+ @importing = true
|
|
|
+ @importResults = null
|
|
|
+
|
|
|
+ reader = new FileReader()
|
|
|
+ reader.onload = (e) =>
|
|
|
+ try
|
|
|
+ results = Papa.parse e.target.result, {
|
|
|
+ header: true
|
|
|
+ delimiter: ';'
|
|
|
+ skipEmptyLines: true
|
|
|
+ encoding: 'UTF-8'
|
|
|
+ }
|
|
|
+
|
|
|
+ products = results.data.filter (row) =>
|
|
|
+ row && row['Артикул*'] && row['Название товара'] && row['Цена, руб.*']
|
|
|
+
|
|
|
+ # Обрабатываем товары последовательно для загрузки изображений
|
|
|
+ @processProductsSequentially(products)
|
|
|
+ .then (couchProducts) =>
|
|
|
+ @importResults = {
|
|
|
+ success: true,
|
|
|
+ processed: couchProducts.length,
|
|
|
+ errors: []
|
|
|
+ }
|
|
|
+ @importing = false
|
|
|
+ @loadProducts()
|
|
|
+ @loadCategories()
|
|
|
+ @showNotification "Импортировано #{couchProducts.length} товаров"
|
|
|
+ .catch (error) =>
|
|
|
+ @importResults = {
|
|
|
+ success: false,
|
|
|
+ error: error.message,
|
|
|
+ processed: 0,
|
|
|
+ errors: [error.message]
|
|
|
+ }
|
|
|
+ @importing = false
|
|
|
+ @showNotification "Ошибка импорта: #{error.message}", 'error'
|
|
|
+
|
|
|
+ catch error
|
|
|
+ @importResults = {
|
|
|
+ success: false,
|
|
|
+ error: error.message,
|
|
|
+ processed: 0,
|
|
|
+ errors: [error.message]
|
|
|
+ }
|
|
|
+ @importing = false
|
|
|
+ @showNotification "Ошибка обработки файла: #{error.message}", 'error'
|
|
|
+
|
|
|
+ reader.readAsText(@selectedFile, 'UTF-8')
|
|
|
+
|
|
|
+ # Последовательная обработка товаров с загрузкой изображений
|
|
|
+ processProductsSequentially: (products) ->
|
|
|
+ couchProducts = []
|
|
|
+ currentIndex = 0
|
|
|
+
|
|
|
+ processNextProduct = =>
|
|
|
+ if currentIndex >= products.length
|
|
|
+ return Promise.resolve(couchProducts)
|
|
|
+
|
|
|
+ product = products[currentIndex]
|
|
|
+ currentIndex++
|
|
|
+
|
|
|
+ @transformProductData(product, currentIndex)
|
|
|
+ .then (productData) =>
|
|
|
+ couchProducts.push(productData)
|
|
|
+ return processNextProduct()
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log "Ошибка обработки товара #{currentIndex}:", error
|
|
|
+ # Продолжаем обработку следующих товаров даже при ошибке
|
|
|
+ return processNextProduct()
|
|
|
+
|
|
|
+ return processNextProduct()
|
|
|
+
|
|
|
+ # Полное преобразование данных товара с загрузкой изображений
|
|
|
+ transformProductData: (product, index) ->
|
|
|
+ return new Promise (resolve, reject) =>
|
|
|
+ try
|
|
|
+ # Базовые поля
|
|
|
+ productData = {
|
|
|
+ _id: "product:#{Date.now()}-#{index}"
|
|
|
+ type: 'product'
|
|
|
+ name: product['Название товара']?.trim() || 'Без названия'
|
|
|
+ sku: product['Артикул*']?.trim() || "SKU-#{Date.now()}-#{index}"
|
|
|
+ price: @parsePrice(product['Цена, руб.*'])
|
|
|
+ active: true
|
|
|
+ createdAt: new Date().toISOString()
|
|
|
+ updatedAt: new Date().toISOString()
|
|
|
+ domains: [window.location.hostname] # Текущий домен по умолчанию
|
|
|
+ }
|
|
|
+
|
|
|
+ # Обработка всех полей CSV
|
|
|
+ @processAllCSVFields(product, productData)
|
|
|
+
|
|
|
+ # Обработка категории
|
|
|
+ @processCategory(product, productData, index)
|
|
|
+ .then =>
|
|
|
+ # Обработка основного изображения
|
|
|
+ return @processMainImage(product, productData)
|
|
|
+ .then =>
|
|
|
+ # Обработка дополнительных изображений
|
|
|
+ return @processAdditionalImages(product, productData)
|
|
|
+ .then =>
|
|
|
+ # Обработка Rich-контента
|
|
|
+ @processRichContent(product, productData)
|
|
|
+
|
|
|
+ resolve(productData)
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log "Ошибка обработки товара:", error
|
|
|
+ # Возвращаем товар даже с ошибками обработки
|
|
|
+ resolve(productData)
|
|
|
+
|
|
|
+ catch error
|
|
|
+ reject(error)
|
|
|
+
|
|
|
+ # Парсинг цены
|
|
|
+ parsePrice: (priceString) ->
|
|
|
+ return 0 if !priceString
|
|
|
+ try
|
|
|
+ # Удаляем пробелы и заменяем запятые на точки
|
|
|
+ cleanPrice = priceString.toString().replace(/\s/g, '').replace(',', '.')
|
|
|
+ price = parseFloat(cleanPrice)
|
|
|
+ return isNaN(price) ? 0 : price
|
|
|
+ catch
|
|
|
+ return 0
|
|
|
+
|
|
|
+ # Обработка всех полей CSV
|
|
|
+ processAllCSVFields: (product, productData) ->
|
|
|
+ # Базовые поля
|
|
|
+ productData.brand = product['Бренд*']?.trim() || ''
|
|
|
+ productData.productType = product['Тип*']?.trim() || ''
|
|
|
+ productData.weight = product['Вес товара, г']?.trim() || ''
|
|
|
+ productData.volume = product['Объем, л']?.trim() || ''
|
|
|
+ productData.country = product['Страна-изготовитель']?.trim() || ''
|
|
|
+ productData.warranty = product['Гарантия']?.trim() || ''
|
|
|
+ productData.color = product['Цвет товара']?.trim() || ''
|
|
|
+
|
|
|
+ # Цены
|
|
|
+ if product['Цена до скидки, руб.']
|
|
|
+ productData.oldPrice = @parsePrice(product['Цена до скидки, руб.'])
|
|
|
+
|
|
|
+ # Характеристики
|
|
|
+ productData.attributes = {}
|
|
|
+
|
|
|
+ # Технические характеристики
|
|
|
+ if product['Расход, л/м2']
|
|
|
+ productData.attributes.consumption = product['Расход, л/м2']
|
|
|
+ if product['Время высыхания, часов']
|
|
|
+ productData.attributes.dryingTime = product['Время высыхания, часов']
|
|
|
+ if product['Вид краски']
|
|
|
+ productData.attributes.paintType = product['Вид краски']
|
|
|
+ if product['Основа краски']
|
|
|
+ productData.attributes.paintBase = product['Основа краски']
|
|
|
+ if product['Способ нанесения']
|
|
|
+ productData.attributes.applicationMethod = product['Способ нанесения']
|
|
|
+ if product['Область применения состава']
|
|
|
+ productData.attributes.applicationArea = product['Область применения состава']
|
|
|
+
|
|
|
+ # Мета-данные
|
|
|
+ productData.tags = []
|
|
|
+ if product['#Хештеги']
|
|
|
+ tags = product['#Хештеги']?.split('#').filter((tag) -> tag.trim()).map((tag) -> tag.trim())
|
|
|
+ productData.tags = tags || []
|
|
|
+
|
|
|
+ # Статусы и флаги
|
|
|
+ productData.features = {
|
|
|
+ installment: product['Рассрочка']?.toLowerCase() == 'да'
|
|
|
+ reviewPoints: product['Баллы за отзывы']?.toLowerCase() == 'да'
|
|
|
+ canBeTinted: product['Возможность колеровки']?.toLowerCase() == 'да'
|
|
|
+ aerosol: product['Аэрозоль']?.toLowerCase() == 'да'
|
|
|
+ }
|
|
|
+
|
|
|
+ # Обработка категории с проверкой дубликатов
|
|
|
+ processCategory: (product, productData, index) ->
|
|
|
+ return Promise.resolve() if !product['Тип*']
|
|
|
+
|
|
|
+ categoryName = product['Тип*'].trim()
|
|
|
+ return Promise.resolve() if !categoryName
|
|
|
+
|
|
|
+ # Поиск существующей категории (регистронезависимо)
|
|
|
+ existingCategory = @categories.find (cat) ->
|
|
|
+ cat.name?.toLowerCase() == categoryName.toLowerCase()
|
|
|
+
|
|
|
+ if existingCategory
|
|
|
+ productData.category = existingCategory._id
|
|
|
+ debug.log "Использована существующая категория: #{categoryName}"
|
|
|
+ return Promise.resolve()
|
|
|
+ else
|
|
|
+ # Создание новой категории
|
|
|
+ categoryId = "category:#{Date.now()}-#{index}"
|
|
|
+ newCategory = {
|
|
|
+ _id: categoryId
|
|
|
+ type: 'category'
|
|
|
+ name: categoryName
|
|
|
+ slug: @generateSlug(categoryName)
|
|
|
+ sortOrder: @categories.length
|
|
|
+ active: true
|
|
|
+ createdAt: new Date().toISOString()
|
|
|
+ updatedAt: new Date().toISOString()
|
|
|
+ domains: [window.location.hostname]
|
|
|
+ }
|
|
|
+
|
|
|
+ productData.category = categoryId
|
|
|
+
|
|
|
+ return PouchDB.saveToRemote(newCategory)
|
|
|
+ .then (result) =>
|
|
|
+ debug.log "Создана новая категория: #{categoryName}"
|
|
|
+ @categories.push(newCategory)
|
|
|
+ return Promise.resolve()
|
|
|
+ .catch (error) ->
|
|
|
+ debug.log "Ошибка создания категории #{categoryName}:", error
|
|
|
+ # Продолжаем без категории
|
|
|
+ return Promise.resolve()
|
|
|
+
|
|
|
+ # Обработка основного изображения
|
|
|
+ processMainImage: (product, productData) ->
|
|
|
+ return Promise.resolve() if !product['Ссылка на главное фото*']
|
|
|
+
|
|
|
+ imageUrl = product['Ссылка на главное фото*'].trim()
|
|
|
+ return Promise.resolve() if !imageUrl
|
|
|
+
|
|
|
+ return @downloadAndStoreImage(imageUrl, productData._id, 'main.jpg')
|
|
|
+ .then (attachmentInfo) =>
|
|
|
+ productData.image = "/d/braer_color_shop/#{productData._id}/main.jpg"
|
|
|
+ productData.attachments = productData.attachments || {}
|
|
|
+ productData.attachments.main = attachmentInfo
|
|
|
+ return Promise.resolve()
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log "Ошибка загрузки основного изображения:", error
|
|
|
+ # Продолжаем без изображения
|
|
|
+ return Promise.resolve()
|
|
|
|
|
|
- # Mass actions methods
|
|
|
+ # Обработка дополнительных изображений
|
|
|
+ processAdditionalImages: (product, productData) ->
|
|
|
+ return Promise.resolve() if !product['Ссылки на дополнительные фото']
|
|
|
+
|
|
|
+ additionalImages = product['Ссылки на дополнительные фото']
|
|
|
+ if typeof additionalImages == 'string'
|
|
|
+ # Разделяем ссылки по переносам строк
|
|
|
+ imageUrls = additionalImages.split('\n').filter((url) -> url.trim())
|
|
|
+ else
|
|
|
+ imageUrls = []
|
|
|
+
|
|
|
+ return Promise.resolve() if imageUrls.length == 0
|
|
|
+
|
|
|
+ # Обрабатываем первые 5 изображений чтобы не перегружать
|
|
|
+ imagePromises = []
|
|
|
+ productData.additionalImages = []
|
|
|
+ productData.attachments = productData.attachments || {}
|
|
|
+
|
|
|
+ for imageUrl, i in imageUrls.slice(0, 5)
|
|
|
+ do (imageUrl, i) =>
|
|
|
+ promise = @downloadAndStoreImage(imageUrl.trim(), productData._id, "additional-#{i}.jpg")
|
|
|
+ .then (attachmentInfo) =>
|
|
|
+ imagePath = "/d/braer_color_shop/#{productData._id}/additional-#{i}.jpg"
|
|
|
+ productData.additionalImages.push(imagePath)
|
|
|
+ productData.attachments["additional-#{i}"] = attachmentInfo
|
|
|
+ return Promise.resolve()
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log "Ошибка загрузки дополнительного изображения #{i}:", error
|
|
|
+ return Promise.resolve()
|
|
|
+
|
|
|
+ imagePromises.push(promise)
|
|
|
+
|
|
|
+ return Promise.all(imagePromises)
|
|
|
+
|
|
|
+ # Загрузка и сохранение изображения как attachment
|
|
|
+ downloadAndStoreImage: (imageUrl, docId, filename) ->
|
|
|
+ return new Promise (resolve, reject) =>
|
|
|
+ try
|
|
|
+ # Создаем XMLHttpRequest для загрузки изображения
|
|
|
+ xhr = new XMLHttpRequest()
|
|
|
+ xhr.open('GET', imageUrl, true)
|
|
|
+ xhr.responseType = 'blob'
|
|
|
+
|
|
|
+ xhr.onload = =>
|
|
|
+ if xhr.status == 200
|
|
|
+ blob = xhr.response
|
|
|
+
|
|
|
+ # Читаем blob как ArrayBuffer
|
|
|
+ reader = new FileReader()
|
|
|
+ reader.onload = (e) =>
|
|
|
+ arrayBuffer = e.target.result
|
|
|
+
|
|
|
+ # Сохраняем как attachment в PouchDB
|
|
|
+ PouchDB.putAttachment(docId, filename, arrayBuffer, blob.type)
|
|
|
+ .then (result) =>
|
|
|
+ resolve({
|
|
|
+ filename: filename
|
|
|
+ contentType: blob.type
|
|
|
+ size: blob.size
|
|
|
+ url: "/d/braer_color_shop/#{docId}/#{filename}"
|
|
|
+ })
|
|
|
+ .catch (error) =>
|
|
|
+ reject(error)
|
|
|
+
|
|
|
+ reader.readAsArrayBuffer(blob)
|
|
|
+ else
|
|
|
+ reject(new Error(`Ошибка загрузки изображения: ${xhr.status}`))
|
|
|
+
|
|
|
+ xhr.onerror = =>
|
|
|
+ reject(new Error('Ошибка сети при загрузке изображения'))
|
|
|
+
|
|
|
+ xhr.send()
|
|
|
+
|
|
|
+ catch error
|
|
|
+ reject(error)
|
|
|
+
|
|
|
+ # Обработка Rich-контента JSON и преобразование в Markdown
|
|
|
+ processRichContent: (product, productData) ->
|
|
|
+ # Сначала пробуем Rich-контент JSON
|
|
|
+ if product['Rich-контент JSON'] && product['Rich-контент JSON'].trim()
|
|
|
+ try
|
|
|
+ richContent = JSON.parse(product['Rich-контент JSON'])
|
|
|
+ productData.description = @richContentToMarkdown(richContent)
|
|
|
+ productData.originalRichContent = richContent # Сохраняем оригинал
|
|
|
+ return
|
|
|
+ catch error
|
|
|
+ debug.log "Ошибка парсинга Rich-контента:", error
|
|
|
+
|
|
|
+ # Если Rich-контент невалиден или отсутствует, используем аннотацию
|
|
|
+ if product['Аннотация'] && product['Аннотация'].trim()
|
|
|
+ productData.description = product['Аннотация'].trim()
|
|
|
+ else
|
|
|
+ productData.description = ''
|
|
|
+
|
|
|
+ # Преобразование Rich-контента JSON в Markdown
|
|
|
+ richContentToMarkdown: (richContent) ->
|
|
|
+ return '' if !richContent
|
|
|
+
|
|
|
+ try
|
|
|
+ markdownParts = []
|
|
|
+
|
|
|
+ if richContent.content && Array.isArray(richContent.content)
|
|
|
+ for item in richContent.content
|
|
|
+ markdownParts.push(@processContentItem(item))
|
|
|
+
|
|
|
+ result = markdownParts.filter((part) -> part).join('\n\n')
|
|
|
+ return result || 'Описание товара'
|
|
|
+ catch error
|
|
|
+ debug.log "Ошибка преобразования Rich-контента в Markdown:", error
|
|
|
+ return JSON.stringify(richContent)
|
|
|
+
|
|
|
+ # Обработка отдельного элемента контента
|
|
|
+ processContentItem: (item) ->
|
|
|
+ return '' if !item
|
|
|
+
|
|
|
+ switch item.widgetName
|
|
|
+ when 'raTextBlock'
|
|
|
+ return @processTextBlock(item)
|
|
|
+ when 'raHeader'
|
|
|
+ return @processHeader(item)
|
|
|
+ when 'raImage'
|
|
|
+ return @processImage(item)
|
|
|
+ when 'raList'
|
|
|
+ return @processList(item)
|
|
|
+ else
|
|
|
+ return JSON.stringify(item)
|
|
|
+
|
|
|
+ # Обработка текстового блока
|
|
|
+ processTextBlock: (item) ->
|
|
|
+ textParts = []
|
|
|
+
|
|
|
+ # Заголовок
|
|
|
+ if item.title && item.title.items
|
|
|
+ titleText = @processTextItems(item.title.items)
|
|
|
+ if titleText
|
|
|
+ level = item.title.size || 'size3'
|
|
|
+ hashes = @getHeadingLevel(level)
|
|
|
+ textParts.push("#{hashes} #{titleText}")
|
|
|
+
|
|
|
+ # Основной текст
|
|
|
+ if item.text && item.text.items
|
|
|
+ bodyText = @processTextItems(item.text.items)
|
|
|
+ if bodyText
|
|
|
+ textParts.push(bodyText)
|
|
|
+
|
|
|
+ return textParts.join('\n\n')
|
|
|
+
|
|
|
+ # Обработка заголовка
|
|
|
+ processHeader: (item) ->
|
|
|
+ if item.text && item.text.items
|
|
|
+ headerText = @processTextItems(item.text.items)
|
|
|
+ if headerText
|
|
|
+ level = item.size || 'size2'
|
|
|
+ hashes = @getHeadingLevel(level)
|
|
|
+ return "#{hashes} #{headerText}"
|
|
|
+ return ''
|
|
|
+
|
|
|
+ # Обработка изображения
|
|
|
+ processImage: (item) ->
|
|
|
+ if item.url
|
|
|
+ altText = item.alt || 'Изображение товара'
|
|
|
+ return ""
|
|
|
+ return ''
|
|
|
+
|
|
|
+ # Обработка списка
|
|
|
+ processList: (item) ->
|
|
|
+ return '' if !item.items || !Array.isArray(item.items)
|
|
|
+
|
|
|
+ listItems = []
|
|
|
+ for listItem in item.items
|
|
|
+ if listItem.content
|
|
|
+ listItems.push("- #{listItem.content}")
|
|
|
+
|
|
|
+ return listItems.join('\n')
|
|
|
+
|
|
|
+ # Обработка текстовых элементов
|
|
|
+ processTextItems: (items) ->
|
|
|
+ return '' if !items || !Array.isArray(items)
|
|
|
+
|
|
|
+ textParts = []
|
|
|
+ for textItem in items
|
|
|
+ if textItem.type == 'text' && textItem.content
|
|
|
+ content = textItem.content
|
|
|
+
|
|
|
+ # Обработка форматирования
|
|
|
+ if textItem.formatting
|
|
|
+ if textItem.formatting.bold
|
|
|
+ content = "**#{content}**"
|
|
|
+ if textItem.formatting.italic
|
|
|
+ content = "*#{content}*"
|
|
|
+
|
|
|
+ textParts.push(content)
|
|
|
+
|
|
|
+ else if textItem.type == 'br'
|
|
|
+ textParts.push('\n')
|
|
|
+
|
|
|
+ return textParts.join('')
|
|
|
+
|
|
|
+ # Получение уровня заголовка Markdown
|
|
|
+ getHeadingLevel: (size) ->
|
|
|
+ switch size
|
|
|
+ when 'size1', 'size5' then '#' # H1
|
|
|
+ when 'size2', 'size4' then '##' # H2
|
|
|
+ when 'size3' then '###' # H3
|
|
|
+ else '##' # H2 по умолчанию
|
|
|
+
|
|
|
+ # Генерация slug
|
|
|
+ generateSlug: (text) ->
|
|
|
+ return '' if !text
|
|
|
+ text.toLowerCase()
|
|
|
+ .replace(/\s+/g, '-')
|
|
|
+ .replace(/[^\w\-]+/g, '')
|
|
|
+ .replace(/\-\-+/g, '-')
|
|
|
+ .replace(/^-+/, '')
|
|
|
+ .replace(/-+$/, '')
|
|
|
+
|
|
|
+ # Массовые действия (остаются без изменений)
|
|
|
toggleSelectAll: ->
|
|
|
if @selectAll
|
|
|
@selectedProducts = @filteredProducts.map (product) -> product._id
|
|
|
@@ -221,340 +663,6 @@ module.exports =
|
|
|
debug.log 'Ошибка удаления товаров:', error
|
|
|
@showNotification 'Ошибка удаления товаров', 'error'
|
|
|
|
|
|
- assignCategoryToSelected: ->
|
|
|
- if @selectedProducts.length == 0 || !@massCategory
|
|
|
- @showNotification 'Выберите товары и категорию', 'error'
|
|
|
- return
|
|
|
-
|
|
|
- promises = @selectedProducts.map (productId) =>
|
|
|
- product = @products.find (p) -> p._id == productId
|
|
|
- if product
|
|
|
- updatedProduct = {
|
|
|
- ...product
|
|
|
- updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
-
|
|
|
- if @removeExistingCategories
|
|
|
- updatedProduct.category = @massCategory
|
|
|
- else
|
|
|
- # Если категория уже есть, не перезаписываем
|
|
|
- updatedProduct.category = @massCategory
|
|
|
-
|
|
|
- PouchDB.saveToRemote(updatedProduct)
|
|
|
-
|
|
|
- Promise.all(promises)
|
|
|
- .then (results) =>
|
|
|
- @loadProducts()
|
|
|
- @clearSelection()
|
|
|
- @showCategoryAssignModal = false
|
|
|
- @massCategory = ''
|
|
|
- @removeExistingCategories = false
|
|
|
- @showNotification "Категория назначена для #{results.length} товаров"
|
|
|
- .catch (error) =>
|
|
|
- debug.log 'Ошибка назначения категории:', error
|
|
|
- @showNotification 'Ошибка назначения категории', 'error'
|
|
|
-
|
|
|
- assignCategoryToAll: ->
|
|
|
- if !@massAllCategory
|
|
|
- @showNotification 'Выберите категорию', 'error'
|
|
|
- return
|
|
|
-
|
|
|
- promises = @products.map (product) =>
|
|
|
- updatedProduct = {
|
|
|
- ...product
|
|
|
- updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
-
|
|
|
- if @massRemoveAllCategories
|
|
|
- updatedProduct.category = @massAllCategory
|
|
|
- else if !updatedProduct.category
|
|
|
- updatedProduct.category = @massAllCategory
|
|
|
-
|
|
|
- PouchDB.saveToRemote(updatedProduct)
|
|
|
-
|
|
|
- Promise.all(promises)
|
|
|
- .then (results) =>
|
|
|
- @loadProducts()
|
|
|
- @showMassCategoryAssign = false
|
|
|
- @massAllCategory = ''
|
|
|
- @massRemoveAllCategories = false
|
|
|
- @showNotification "Категория назначена для всех товаров"
|
|
|
- .catch (error) =>
|
|
|
- debug.log 'Ошибка назначения категории:', error
|
|
|
- @showNotification 'Ошибка назначения категории', 'error'
|
|
|
-
|
|
|
- massChangeStatus: (status) ->
|
|
|
- promises = @products.map (product) =>
|
|
|
- if product.active != status
|
|
|
- updatedProduct = {
|
|
|
- ...product
|
|
|
- active: status
|
|
|
- updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
- PouchDB.saveToRemote(updatedProduct)
|
|
|
- else
|
|
|
- Promise.resolve()
|
|
|
-
|
|
|
- Promise.all(promises)
|
|
|
- .then (results) =>
|
|
|
- @loadProducts()
|
|
|
- @showNotification "Статус всех товаров изменен"
|
|
|
- .catch (error) =>
|
|
|
- debug.log 'Ошибка изменения статуса:', error
|
|
|
- @showNotification 'Ошибка изменения статуса', 'error'
|
|
|
-
|
|
|
- massRemoveCategories: ->
|
|
|
- if !confirm("Удалить категории у всех товаров?")
|
|
|
- return
|
|
|
-
|
|
|
- promises = @products.map (product) =>
|
|
|
- if product.category
|
|
|
- updatedProduct = {
|
|
|
- ...product
|
|
|
- category: ''
|
|
|
- updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
- PouchDB.saveToRemote(updatedProduct)
|
|
|
- else
|
|
|
- Promise.resolve()
|
|
|
-
|
|
|
- Promise.all(promises)
|
|
|
- .then (results) =>
|
|
|
- @loadProducts()
|
|
|
- @showNotification "Категории удалены у всех товаров"
|
|
|
- .catch (error) =>
|
|
|
- debug.log 'Ошибка удаления категорий:', error
|
|
|
- @showNotification 'Ошибка удаления категорий', 'error'
|
|
|
-
|
|
|
- applyMassPriceChange: ->
|
|
|
- if !@priceChangeValue
|
|
|
- @showNotification 'Введите значение изменения', 'error'
|
|
|
- return
|
|
|
-
|
|
|
- promises = @products.map (product) =>
|
|
|
- updatedProduct = { ...product, updatedAt: new Date().toISOString() }
|
|
|
-
|
|
|
- switch @priceChangeType
|
|
|
- when 'fixed'
|
|
|
- if @applyToOldPrice
|
|
|
- updatedProduct.oldPrice = parseFloat(@priceChangeValue)
|
|
|
- else
|
|
|
- updatedProduct.price = parseFloat(@priceChangeValue)
|
|
|
-
|
|
|
- when 'percent'
|
|
|
- if @applyToOldPrice && updatedProduct.oldPrice
|
|
|
- updatedProduct.oldPrice = updatedProduct.oldPrice * (1 + parseFloat(@priceChangeValue) / 100)
|
|
|
- else
|
|
|
- updatedProduct.price = updatedProduct.price * (1 + parseFloat(@priceChangeValue) / 100)
|
|
|
-
|
|
|
- when 'increase'
|
|
|
- if @applyToOldPrice && updatedProduct.oldPrice
|
|
|
- updatedProduct.oldPrice = updatedProduct.oldPrice + parseFloat(@priceChangeValue)
|
|
|
- else
|
|
|
- updatedProduct.price = updatedProduct.price + parseFloat(@priceChangeValue)
|
|
|
-
|
|
|
- when 'decrease'
|
|
|
- if @applyToOldPrice && updatedProduct.oldPrice
|
|
|
- updatedProduct.oldPrice = Math.max(0, updatedProduct.oldPrice - parseFloat(@priceChangeValue))
|
|
|
- else
|
|
|
- updatedProduct.price = Math.max(0, updatedProduct.price - parseFloat(@priceChangeValue))
|
|
|
-
|
|
|
- PouchDB.saveToRemote(updatedProduct)
|
|
|
-
|
|
|
- Promise.all(promises)
|
|
|
- .then (results) =>
|
|
|
- @loadProducts()
|
|
|
- @showMassPriceModal = false
|
|
|
- @priceChangeValue = null
|
|
|
- @showNotification "Цены успешно обновлены"
|
|
|
- .catch (error) =>
|
|
|
- debug.log 'Ошибка изменения цен:', error
|
|
|
- @showNotification 'Ошибка изменения цен', 'error'
|
|
|
-
|
|
|
- exportProducts: ->
|
|
|
- csvData = @products.map (product) =>
|
|
|
- categoryName = @getCategoryName(product.category)
|
|
|
- return {
|
|
|
- 'Название товара': product.name
|
|
|
- 'Артикул': product.sku
|
|
|
- 'Цена, руб.': product.price
|
|
|
- 'Старая цена, руб.': product.oldPrice || ''
|
|
|
- 'Категория': categoryName
|
|
|
- 'Бренд': product.brand || ''
|
|
|
- 'Статус': if product.active then 'Активен' else 'Неактивен'
|
|
|
- 'Описание': product.description || ''
|
|
|
- }
|
|
|
-
|
|
|
- csv = Papa.unparse(csvData, {
|
|
|
- delimiter: ';'
|
|
|
- encoding: 'UTF-8'
|
|
|
- })
|
|
|
-
|
|
|
- blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' })
|
|
|
- link = document.createElement('a')
|
|
|
- url = URL.createObjectURL(blob)
|
|
|
- link.setAttribute('href', url)
|
|
|
- link.setAttribute('download', 'products_export.csv')
|
|
|
- link.style.visibility = 'hidden'
|
|
|
- document.body.appendChild(link)
|
|
|
- link.click()
|
|
|
- document.body.removeChild(link)
|
|
|
-
|
|
|
- @showNotification 'Экспорт завершен'
|
|
|
-
|
|
|
- exportSelectedProducts: ->
|
|
|
- if @selectedProducts.length == 0
|
|
|
- @showNotification 'Выберите товары для экспорта', 'error'
|
|
|
- return
|
|
|
-
|
|
|
- selectedProductsData = @products.filter (product) =>
|
|
|
- @selectedProducts.includes(product._id)
|
|
|
-
|
|
|
- csvData = selectedProductsData.map (product) =>
|
|
|
- categoryName = @getCategoryName(product.category)
|
|
|
- return {
|
|
|
- 'Название товара': product.name
|
|
|
- 'Артикул': product.sku
|
|
|
- 'Цена, руб.': product.price
|
|
|
- 'Старая цена, руб.': product.oldPrice || ''
|
|
|
- 'Категория': categoryName
|
|
|
- 'Бренд': product.brand || ''
|
|
|
- 'Статус': if product.active then 'Активен' else 'Неактивен'
|
|
|
- 'Описание': product.description || ''
|
|
|
- }
|
|
|
-
|
|
|
- csv = Papa.unparse(csvData, {
|
|
|
- delimiter: ';'
|
|
|
- encoding: 'UTF-8'
|
|
|
- })
|
|
|
-
|
|
|
- blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' })
|
|
|
- link = document.createElement('a')
|
|
|
- url = URL.createObjectURL(blob)
|
|
|
- link.setAttribute('href', url)
|
|
|
- link.setAttribute('download', 'selected_products_export.csv')
|
|
|
- link.style.visibility = 'hidden'
|
|
|
- document.body.appendChild(link)
|
|
|
- link.click()
|
|
|
- document.body.removeChild(link)
|
|
|
-
|
|
|
- @showNotification 'Экспорт выбранных товаров завершен'
|
|
|
-
|
|
|
- # Updated category creation with duplicate check
|
|
|
- transformProductData: (product, index) ->
|
|
|
- productData = {
|
|
|
- _id: "product:#{Date.now()}-#{index}"
|
|
|
- type: 'product'
|
|
|
- name: product['Название товара']
|
|
|
- sku: product['Артикул*']
|
|
|
- price: parseFloat(product['Цена, руб.*'].replace(/\s/g, '').replace(',', '.')) || 0
|
|
|
- active: true
|
|
|
- createdAt: new Date().toISOString()
|
|
|
- updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
-
|
|
|
- # Improved category handling with duplicate check
|
|
|
- if product['Тип*']
|
|
|
- categoryName = product['Тип*'].trim()
|
|
|
-
|
|
|
- # Check for existing category by name (case insensitive)
|
|
|
- existingCategory = @categories.find (cat) ->
|
|
|
- cat.name?.toLowerCase() == categoryName.toLowerCase()
|
|
|
-
|
|
|
- if existingCategory
|
|
|
- productData.category = existingCategory._id
|
|
|
- debug.log "Использована существующая категория: #{categoryName}"
|
|
|
- else
|
|
|
- # Create new category only if it doesn't exist
|
|
|
- categoryId = "category:#{Date.now()}-#{index}"
|
|
|
- newCategory = {
|
|
|
- _id: categoryId
|
|
|
- type: 'category'
|
|
|
- name: categoryName
|
|
|
- slug: @generateSlug(categoryName)
|
|
|
- sortOrder: @categories.length
|
|
|
- active: true
|
|
|
- createdAt: new Date().toISOString()
|
|
|
- updatedAt: new Date().toISOString()
|
|
|
- domains: @availableDomains?.map((d) -> d.domain) || []
|
|
|
- }
|
|
|
-
|
|
|
- # Save new category and add to local categories array
|
|
|
- PouchDB.saveToRemote(newCategory)
|
|
|
- .then (result) =>
|
|
|
- debug.log "Создана новая категория: #{categoryName}"
|
|
|
- @categories.push(newCategory)
|
|
|
- .catch (error) ->
|
|
|
- debug.log "Ошибка создания категории #{categoryName}:", error
|
|
|
-
|
|
|
- productData.category = categoryId
|
|
|
-
|
|
|
- # Rest of the method remains the same...
|
|
|
- if product['Цена до скидки, руб.']
|
|
|
- productData.oldPrice = parseFloat(product['Цена до скидки, руб.'].replace(/\s/g, '').replace(',', '.'))
|
|
|
-
|
|
|
- if product['Ссылка на главное фото*']
|
|
|
- productData.image = product['Ссылка на главное фото*']
|
|
|
-
|
|
|
- if product['Бренд*']
|
|
|
- productData.brand = product['Бренд*']
|
|
|
-
|
|
|
- if product['Тип*']
|
|
|
- productData.productType = product['Тип*']
|
|
|
-
|
|
|
- if product['Rich-контент JSON']
|
|
|
- try
|
|
|
- richContent = JSON.parse(product['Rich-контент JSON'])
|
|
|
- productData.description = @richContentToMarkdown(richContent)
|
|
|
- catch
|
|
|
- productData.description = product['Аннотация'] || ''
|
|
|
- else
|
|
|
- productData.description = product['Аннотация'] || ''
|
|
|
-
|
|
|
- productData.domains = @availableDomains?.map((d) -> d.domain) || []
|
|
|
-
|
|
|
- return productData
|
|
|
-
|
|
|
- # Updated category creation method with duplicate prevention
|
|
|
- saveCategory: ->
|
|
|
- if !@categoryForm.name || !@categoryForm.slug
|
|
|
- @showNotification 'Заполните обязательные поля (Название, URL slug)', 'error'
|
|
|
- return
|
|
|
-
|
|
|
- # Check for duplicate category name
|
|
|
- duplicateCategory = @categories.find (cat) =>
|
|
|
- cat.name?.toLowerCase() == @categoryForm.name.toLowerCase() &&
|
|
|
- (!@editingCategory || cat._id != @editingCategory._id)
|
|
|
-
|
|
|
- if duplicateCategory
|
|
|
- @showNotification 'Категория с таким названием уже существует', 'error'
|
|
|
- return
|
|
|
-
|
|
|
- categoryData = {
|
|
|
- type: 'category'
|
|
|
- ...@categoryForm
|
|
|
- updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
-
|
|
|
- if @editingCategory
|
|
|
- categoryData._id = @editingCategory._id
|
|
|
- categoryData._rev = @editingCategory._rev
|
|
|
- categoryData.createdAt = @editingCategory.createdAt
|
|
|
- else
|
|
|
- categoryData._id = "category:#{Date.now()}"
|
|
|
- categoryData.createdAt = new Date().toISOString()
|
|
|
-
|
|
|
- PouchDB.saveToRemote(categoryData)
|
|
|
- .then (result) =>
|
|
|
- @showCategoryModal = false
|
|
|
- @resetCategoryForm()
|
|
|
- @loadCategories()
|
|
|
- @showNotification 'Категория успешно сохранена'
|
|
|
- .catch (error) =>
|
|
|
- debug.log 'Ошибка сохранения категории:', error
|
|
|
- @showNotification 'Ошибка сохранения категории', 'error'
|
|
|
-
|
|
|
- # Rest of the methods remain the same...
|
|
|
showNotification: (message, type = 'success') ->
|
|
|
@$root.showNotification?(message, type) || debug.log("#{type}: #{message}")
|
|
|
|