document.head.insertAdjacentHTML('beforeend','')
PouchDB = require 'app/utils/pouch'
Papa = require 'papaparse'
module.exports =
name: 'AdminProducts'
render: (new Function '_ctx', '_cache', renderFns['app/pages/Admin/Products/index.pug'])()
data: ->
return {
products: []
categories: []
searchQuery: ''
selectedCategory: ''
selectedStatus: ''
showProductModal: false
showImportModal: false
showCategoriesModal: false
showCategoryModal: false
showMassActionsModal: false
showCategoryAssignModal: false
showMassCategoryAssign: false
showMassPriceModal: false
editingProduct: null
editingCategory: null
selectedFile: null
selectedCategoriesFile: null
importing: false
importingCategories: false
importResults: null
categoriesImportResults: null
availableDomains: []
categoriesActiveTab: 'list'
# Mass actions data
selectedProducts: []
selectAll: false
massCategory: ''
massAllCategory: ''
removeExistingCategories: false
massRemoveAllCategories: false
priceChangeType: 'fixed'
priceChangeValue: null
applyToOldPrice: false
productForm: {
name: ''
sku: ''
category: ''
price: 0
oldPrice: 0
brand: ''
description: ''
image: ''
active: true
domains: []
}
categoryForm: {
name: ''
slug: ''
description: ''
parentCategory: ''
sortOrder: 0
image: ''
icon: ''
active: true
domains: []
}
}
computed:
filteredProducts: ->
products = @products
if @searchQuery
query = @searchQuery.toLowerCase()
products = products.filter (product) =>
product.name?.toLowerCase().includes(query) ||
product.sku?.toLowerCase().includes(query)
if @selectedCategory
products = products.filter (product) =>
product.category == @selectedCategory
if @selectedStatus == 'active'
products = products.filter (product) => product.active
else if @selectedStatus == 'inactive'
products = products.filter (product) => !product.active
return products
methods:
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 })
.then (result) =>
@categories = result.rows.map (row) -> row.doc
.catch (error) =>
debug.log 'Ошибка загрузки категорий:', error
loadDomains: ->
PouchDB.queryView('admin', 'domain_settings', { include_docs: true })
.then (result) =>
@availableDomains = result.rows.map (row) -> row.doc
.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()
# Обработка дополнительных изображений
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
else
@selectedProducts = []
isProductSelected: (productId) ->
@selectedProducts.includes(productId)
clearSelection: ->
@selectedProducts = []
@selectAll = false
activateSelected: ->
if @selectedProducts.length == 0
@showNotification 'Выберите товары для активации', 'error'
return
promises = @selectedProducts.map (productId) =>
product = @products.find (p) -> p._id == productId
if product && !product.active
updatedProduct = {
...product
active: true
updatedAt: new Date().toISOString()
}
PouchDB.saveToRemote(updatedProduct)
Promise.all(promises)
.then (results) =>
@loadProducts()
@clearSelection()
@showNotification "Активировано #{results.length} товаров"
.catch (error) =>
debug.log 'Ошибка активации товаров:', error
@showNotification 'Ошибка активации товаров', 'error'
deactivateSelected: ->
if @selectedProducts.length == 0
@showNotification 'Выберите товары для деактивации', 'error'
return
promises = @selectedProducts.map (productId) =>
product = @products.find (p) -> p._id == productId
if product && product.active
updatedProduct = {
...product
active: false
updatedAt: new Date().toISOString()
}
PouchDB.saveToRemote(updatedProduct)
Promise.all(promises)
.then (results) =>
@loadProducts()
@clearSelection()
@showNotification "Деактивировано #{results.length} товаров"
.catch (error) =>
debug.log 'Ошибка деактивации товаров:', error
@showNotification 'Ошибка деактивации товаров', 'error'
deleteSelected: ->
if @selectedProducts.length == 0
@showNotification 'Выберите товары для удаления', 'error'
return
if !confirm("Вы уверены, что хотите удалить #{@selectedProducts.length} товаров?")
return
promises = @selectedProducts.map (productId) =>
PouchDB.getDocument(productId)
.then (doc) ->
PouchDB.saveToRemote({ ...doc, _deleted: true })
Promise.all(promises)
.then (results) =>
@loadProducts()
@clearSelection()
@showNotification "Удалено #{results.length} товаров"
.catch (error) =>
debug.log 'Ошибка удаления товаров:', error
@showNotification 'Ошибка удаления товаров', 'error'
showNotification: (message, type = 'success') ->
@$root.showNotification?(message, type) || debug.log("#{type}: #{message}")
mounted: ->
@loadProducts()
@loadCategories()
@loadDomains()