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'
importProgress: 0
processedCount: 0
totalCount: 0
uploadingImages: false
newImageUrl: ''
newAdditionalImageUrl: ''
newAttributeKey: ''
newAttributeValue: ''
newTag: ''
# Mass actions data
selectedProducts: []
selectAll: false
massCategory: ''
massAllCategory: ''
removeExistingCategories: false
massRemoveAllCategories: false
priceChangeType: 'fixed'
priceChangeValue: null
applyToOldPrice: false
productForm:
_id: ''
name: ''
sku: ''
category: ''
price: 0
oldPrice: 0
brand: ''
description: ''
image: ''
additionalImages: []
active: true
domains: []
attributes: {}
tags: []
categoryForm:
_id: ''
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) or
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
isEditing: ->
@editingProduct != null
attributesList: ->
list = []
for key, value of @productForm.attributes
list.push
key: key
value: value
return list
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 or 'Без категории'
getCategoryProductCount: (categoryId) ->
@products.filter((product) -> product.category == categoryId).length
# Управление категориями
showCategoriesManager: ->
@showCategoriesModal = true
createCategory: ->
@editingCategory = null
@resetCategoryForm()
@showCategoryModal = true
editCategory: (category) ->
@editingCategory = category
@categoryForm = Object.assign {},
_id: category._id
name: category.name or ''
slug: category.slug or ''
description: category.description or ''
parentCategory: category.parentCategory or ''
sortOrder: category.sortOrder or 0
image: category.image or ''
icon: category.icon or ''
active: category.active != false
domains: category.domains or [window.location.hostname]
@showCategoryModal = true
saveCategory: ->
if !@categoryForm.name
@showNotification 'Введите название категории', 'error'
return
categoryData = Object.assign {}, @categoryForm
delete categoryData._id
if !categoryData.slug
categoryData.slug = @generateSlug(categoryData.name)
if @editingCategory
# Обновление существующей категории
categoryData._id = @editingCategory._id
categoryData._rev = @editingCategory._rev
categoryData.updatedAt = new Date().toISOString()
else
# Создание новой категории
categoryData._id = "category:#{Date.now()}"
categoryData.type = 'category'
categoryData.createdAt = new Date().toISOString()
categoryData.updatedAt = categoryData.createdAt
PouchDB.saveToRemote(categoryData)
.then (result) =>
@showCategoryModal = false
@resetCategoryForm()
@loadCategories()
@showNotification "Категория #{if @editingCategory then 'обновлена' else 'создана'}"
.catch (error) =>
debug.log 'Ошибка сохранения категории:', error
@showNotification 'Ошибка сохранения категории', 'error'
deleteCategory: (category) ->
if !confirm("Удалить категорию \"#{category.name}\"? Товары в этой категории не будут удалены.")
return
PouchDB.getDocument(category._id)
.then (doc) ->
PouchDB.saveToRemote(Object.assign {}, doc, { _deleted: true })
.then (result) =>
@loadCategories()
@showNotification 'Категория удалена'
.catch (error) =>
debug.log 'Ошибка удаления категории:', error
@showNotification 'Ошибка удаления категории', 'error'
resetCategoryForm: ->
@categoryForm =
_id: ''
name: ''
slug: ''
description: ''
parentCategory: ''
sortOrder: 0
image: ''
icon: ''
active: true
domains: [window.location.hostname]
# Редактирование и создание товаров
createProduct: ->
@editingProduct = null
@resetProductForm()
@showProductModal = true
editProduct: (product) ->
@editingProduct = product
@productForm = Object.assign {},
_id: product._id
name: product.name or ''
sku: product.sku or ''
category: product.category or ''
price: product.price or 0
oldPrice: product.oldPrice or 0
brand: product.brand or ''
description: product.description or ''
image: product.image or ''
additionalImages: product.additionalImages or []
active: product.active != false
domains: product.domains or [window.location.hostname]
attributes: product.attributes or {}
tags: product.tags or []
@showProductModal = true
saveProduct: ->
if !@productForm.name or !@productForm.sku or !@productForm.price
@showNotification 'Заполните обязательные поля: название, артикул и цена', 'error'
return
productData = Object.assign {}, @productForm
delete productData._id
if @isEditing
# Обновление существующего товара
productData._id = @editingProduct._id
productData._rev = @editingProduct._rev
productData.updatedAt = new Date().toISOString()
else
# Создание нового товара
productData._id = "product:#{@productForm.sku}"
productData.type = 'product'
productData.createdAt = new Date().toISOString()
productData.updatedAt = productData.createdAt
PouchDB.saveToRemote(productData)
.then (result) =>
@showProductModal = false
@resetProductForm()
@loadProducts()
@showNotification "Товар #{if @isEditing then 'обновлен' else 'создан'}"
.catch (error) =>
debug.log 'Ошибка сохранения товара:', error
@showNotification 'Ошибка сохранения товара', 'error'
resetProductForm: ->
@productForm =
_id: ''
name: ''
sku: ''
category: ''
price: 0
oldPrice: 0
brand: ''
description: ''
image: ''
additionalImages: []
active: true
domains: [window.location.hostname]
attributes: {}
tags: []
# Управление атрибутами товара
addAttribute: ->
if !@newAttributeKey
@showNotification 'Введите название атрибута', 'error'
return
if @productForm.attributes[@newAttributeKey]
@showNotification 'Атрибут с таким названием уже существует', 'error'
return
@productForm.attributes[@newAttributeKey] = @newAttributeValue or ''
@newAttributeKey = ''
@newAttributeValue = ''
@showNotification 'Атрибут добавлен'
removeAttribute: (key) ->
delete @productForm.attributes[key]
@showNotification 'Атрибут удален'
updateAttribute: (key, value) ->
@productForm.attributes[key] = value
# Управление тегами
addTag: ->
if !@newTag
@showNotification 'Введите тег', 'error'
return
if @productForm.tags.includes(@newTag)
@showNotification 'Такой тег уже существует', 'error'
return
@productForm.tags.push(@newTag)
@newTag = ''
@showNotification 'Тег добавлен'
removeTag: (index) ->
@productForm.tags.splice(index, 1)
@showNotification 'Тег удален'
# Загрузка изображений для редактирования
uploadMainImage: ->
if !@newImageUrl
@showNotification 'Введите URL изображения', 'error'
return
@uploadingImages = true
productId = if @isEditing then @editingProduct._id else "product:#{@productForm.sku}"
@downloadAndStoreImage(@newImageUrl, productId, 'main.jpg')
.then (attachmentInfo) =>
@productForm.image = "/d/braer_color_shop/#{productId}/main.jpg"
@newImageUrl = ''
@showNotification 'Основное изображение загружено'
.catch (error) =>
debug.log 'Ошибка загрузки основного изображения:', error
@showNotification 'Ошибка загрузки изображения', 'error'
.finally =>
@uploadingImages = false
uploadAdditionalImage: ->
if !@newAdditionalImageUrl
@showNotification 'Введите URL изображения', 'error'
return
@uploadingImages = true
productId = if @isEditing then @editingProduct._id else "product:#{@productForm.sku}"
index = @productForm.additionalImages.length
@downloadAndStoreImage(@newAdditionalImageUrl, productId, "additional-#{index}.jpg")
.then (attachmentInfo) =>
imagePath = "/d/braer_color_shop/#{productId}/additional-#{index}.jpg"
@productForm.additionalImages.push(imagePath)
@newAdditionalImageUrl = ''
@showNotification 'Дополнительное изображение загружено'
.catch (error) =>
debug.log 'Ошибка загрузки дополнительного изображения:', error
@showNotification 'Ошибка загрузки изображения', 'error'
.finally =>
@uploadingImages = false
removeAdditionalImage: (index) ->
@productForm.additionalImages.splice(index, 1)
@showNotification 'Изображение удалено'
# ИСПРАВЛЕННЫЙ метод загрузки и сохранения изображения как attachment
downloadAndStoreImage: (imageUrl, docId, filename) ->
return new Promise (resolve, reject) =>
debug.log "🔄 Начало загрузки изображения: #{imageUrl}"
debug.log "📁 Документ: #{docId}, Файл: #{filename}"
try
# Создаем XMLHttpRequest для загрузки изображения
xhr = new XMLHttpRequest()
xhr.open('GET', imageUrl, true)
xhr.responseType = 'blob'
xhr.onload = =>
debug.log "📡 Статус ответа XHR: #{xhr.status}"
if xhr.status == 200
blob = xhr.response
debug.log "✅ Blob получен:",
type: blob.type
size: blob.size
isBlob: blob instanceof Blob
# Читаем blob как ArrayBuffer
reader = new FileReader()
reader.onloadstart = ->
debug.log "📖 Начало чтения blob как ArrayBuffer"
reader.onload = (e) =>
debug.log "✅ ArrayBuffer успешно прочитан"
arrayBuffer = e.target.result
debug.log "📊 ArrayBuffer:",
byteLength: arrayBuffer.byteLength
isArrayBuffer: arrayBuffer instanceof ArrayBuffer
# Получаем текущий документ для правильного _rev
debug.log "🔍 Получение документа #{docId} для _rev"
PouchDB.getDocument(docId)
.then (doc) =>
debug.log "✅ Документ получен:",
_id: doc._id
_rev: doc._rev?.substring(0, 10) + '...'
# Сохраняем как attachment в PouchDB
debug.log "💾 Сохранение attachment..."
return PouchDB.putAttachment(docId, filename, doc._rev, arrayBuffer, blob.type)
.then (result) =>
debug.log "✅ Attachment успешно сохранен:", result
resolve
filename: filename
contentType: blob.type
size: blob.size
url: "/d/braer_color_shop/#{docId}/#{filename}"
.catch (error) =>
debug.log "❌ Ошибка при работе с документом:", error
if error.status == 404
debug.log "📄 Документ не найден, создаем временный"
# Документа нет - создаем временный для attachment
tempDoc =
_id: docId
type: 'product'
name: 'Temp'
sku: 'temp'
price: 0
active: false
createdAt: new Date().toISOString()
updatedAt: new Date().toISOString()
PouchDB.saveToRemote(tempDoc)
.then =>
debug.log "✅ Временный документ создан"
PouchDB.putAttachment(docId, filename, tempDoc._rev, arrayBuffer, blob.type)
.then (result) =>
debug.log "✅ Attachment сохранен во временный документ:", result
resolve
filename: filename
contentType: blob.type
size: blob.size
url: "/d/braer_color_shop/#{docId}/#{filename}"
.catch (err) ->
debug.log "❌ Ошибка сохранения attachment во временный документ:", err
reject(err)
else
debug.log "❌ Другая ошибка при получении документа:", error
reject(error)
reader.onerror = (error) =>
debug.log "❌ Ошибка чтения blob:", error
reject(new Error("Ошибка чтения blob: #{error}"))
reader.onabort = ->
debug.log "⚠️ Чтение blob прервано"
debug.log "🔁 Чтение blob как ArrayBuffer..."
reader.readAsArrayBuffer(blob)
else
errorMsg = "Ошибка загрузки изображения: #{xhr.status}"
debug.log "❌ #{errorMsg}"
reject(new Error(errorMsg))
xhr.onerror = =>
errorMsg = 'Ошибка сети при загрузке изображения'
debug.log "❌ #{errorMsg}"
reject(new Error(errorMsg))
xhr.ontimeout = =>
errorMsg = 'Таймаут загрузки изображения'
debug.log "❌ #{errorMsg}"
reject(new Error(errorMsg))
xhr.onabort = =>
debug.log "⚠️ Загрузка изображения прервана"
debug.log "🚀 Отправка XHR запроса..."
xhr.send()
catch error
debug.log "💥 Критическая ошибка в downloadAndStoreImage:", error
reject(error)
# Импорт товаров из CSV
onFileSelect: (event) ->
@selectedFile = event.target.files[0]
@importResults = null
@importProgress = 0
@processedCount = 0
importProducts: ->
if !@selectedFile
@showNotification 'Выберите файл для импорта', 'error'
return
@importing = true
@importResults = null
@importProgress = 0
@processedCount = 0
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 and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*']
@totalCount = products.length
debug.log "📊 Найдено товаров для импорта: #{@totalCount}"
# Обрабатываем товары последовательно
@processProductsSequentially(products)
.then (results) =>
successCount = results.filter((r) -> r.success).length
errorCount = results.filter((r) -> !r.success).length
@importResults =
success: true
processed: successCount
errors: results.filter((r) -> !r.success).map((r) -> r.error)
total: @totalCount
@importing = false
@loadProducts()
@loadCategories()
@showNotification "Импортировано #{successCount} товаров (#{errorCount} ошибок)"
.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) ->
return new Promise (resolve, reject) =>
results = []
currentIndex = 0
processNextProduct = =>
if currentIndex >= products.length
debug.log "✅ Все товары обработаны. Успешно: #{results.filter((r) -> r.success).length}, Ошибок: #{results.filter((r) -> !r.success).length}"
resolve(results)
return
product = products[currentIndex]
currentIndex++
debug.log "🔧 Обработка товара #{currentIndex}/#{products.length}: #{product['Название товара']?.substring(0, 50)}..."
@transformProductData(product, currentIndex)
.then (productData) =>
debug.log "✅ Данные товара преобразованы: #{productData.sku}"
# Сохраняем каждый товар в отдельный документ
return @saveProductToDB(productData)
.then (savedProduct) =>
debug.log "✅ Товар сохранен в БД: #{savedProduct.sku}"
# Затем обрабатываем изображения для сохраненного товара
return @processProductImages(product, savedProduct)
.then (finalProduct) =>
@processedCount = currentIndex
@importProgress = Math.round((currentIndex / products.length) * 100)
results.push(success: true, product: finalProduct)
debug.log "✅ Товар полностью обработан: #{finalProduct.sku}"
processNextProduct()
.catch (error) =>
debug.log "❌ Ошибка обработки товара #{currentIndex}:", error
@processedCount = currentIndex
@importProgress = Math.round((currentIndex / products.length) * 100)
results.push(success: false, error: error.message, product: product)
# Продолжаем обработку следующих товаров даже при ошибке
debug.log "➡️ Продолжение обработки следующих товаров..."
processNextProduct()
debug.log "🚀 Запуск последовательной обработки #{products.length} товаров"
processNextProduct()
# Сохранение товара в БД с правильной обработкой ревизий
saveProductToDB: (productData) ->
return new Promise (resolve, reject) =>
debug.log "💾 Попытка сохранения товара: #{productData.sku}"
# Сначала пытаемся получить существующий документ
PouchDB.getDocument(productData._id)
.then (existingDoc) =>
# Документ существует - обновляем
debug.log "🔄 Обновление существующего товара: #{productData.sku}"
# Сохраняем только данные, без _rev (PouchDB сам обработает)
updatedData = Object.assign {}, productData
delete updatedData._rev
updatedData.updatedAt = new Date().toISOString()
return PouchDB.saveToRemote(updatedData)
.catch (error) =>
if error.status == 404
# Документ не существует - создаем новый
debug.log "🆕 Создание нового товара: #{productData.sku}"
productData.createdAt = new Date().toISOString()
productData.updatedAt = productData.createdAt
return PouchDB.saveToRemote(productData)
else
throw error
.then (result) =>
# Получаем обновленный документ с правильным _rev
debug.log "✅ Товар сохранен, получение обновленной версии: #{productData.sku}"
return PouchDB.getDocument(productData._id)
.then (savedDoc) =>
debug.log "✅ Документ получен с актуальным _rev: #{savedDoc._rev?.substring(0, 10)}..."
resolve(savedDoc)
.catch (error) =>
debug.log "❌ Ошибка сохранения товара #{productData.sku}:", error
reject(error)
# Обработка изображений после сохранения основного документа
processProductImages: (product, savedProduct) ->
debug.log "🖼️ Начало обработки изображений для товара: #{savedProduct.sku}"
promises = []
# Обработка основного изображения
if product['Ссылка на главное фото*']
imageUrl = product['Ссылка на главное фото*'].trim()
if imageUrl
debug.log "📸 Загрузка основного изображения: #{imageUrl}"
promises.push @downloadAndStoreImage(imageUrl, savedProduct._id, 'main.jpg')
.then (attachmentInfo) =>
debug.log "✅ Основное изображение загружено, обновление товара"
savedProduct.image = "/d/braer_color_shop/#{savedProduct._id}/main.jpg"
return PouchDB.saveToRemote(savedProduct)
.catch (error) =>
debug.log "❌ Ошибка загрузки основного изображения:", error
return savedProduct
else
debug.log "⏭️ Основное изображение отсутствует"
# Обработка дополнительных изображений
if product['Ссылки на дополнительные фото']
additionalImages = product['Ссылки на дополнительные фото']
if typeof additionalImages == 'string'
imageUrls = additionalImages.split('\n').filter((url) -> url.trim())
else
imageUrls = []
debug.log "🖼️ Найдено дополнительных изображений: #{imageUrls.length}"
if imageUrls.length > 0
savedProduct.additionalImages = []
for imageUrl, i in imageUrls.slice(0, 3)
do (imageUrl, i) =>
debug.log "📸 Загрузка дополнительного изображения #{i}: #{imageUrl}"
promise = @downloadAndStoreImage(imageUrl.trim(), savedProduct._id, "additional-#{i}.jpg")
.then (attachmentInfo) =>
imagePath = "/d/braer_color_shop/#{savedProduct._id}/additional-#{i}.jpg"
savedProduct.additionalImages.push(imagePath)
debug.log "✅ Дополнительное изображение #{i} загружено"
return savedProduct
.catch (error) =>
debug.log "❌ Ошибка загрузки дополнительного изображения #{i}:", error
return savedProduct
promises.push(promise)
if promises.length == 0
debug.log "⏭️ Нет изображений для загрузки"
return Promise.resolve(savedProduct)
debug.log "⏳ Ожидание загрузки #{promises.length} изображений..."
return Promise.all(promises)
.then =>
debug.log "✅ Все изображения загружены, обновление документа"
# Обновляем документ с информацией об изображениях
return PouchDB.saveToRemote(savedProduct)
.then (result) =>
debug.log "✅ Документ обновлен с информацией об изображениях"
return PouchDB.getDocument(savedProduct._id)
.catch (error) =>
debug.log "❌ Ошибка обновления товара с изображениями:", error
return savedProduct
# Полное преобразование данных товара с правильной структурой
transformProductData: (product, index) ->
return new Promise (resolve, reject) =>
try
# Генерируем ID на основе артикула для постоянства
sku = product['Артикул*']?.trim() or "SKU-#{Date.now()}-#{index}"
productId = "product:#{sku}"
debug.log "🔄 Преобразование данных товара #{index}: #{sku}"
# Базовые поля согласно design документам
productData =
_id: productId
type: 'product'
name: product['Название товара']?.trim() or 'Без названия'
sku: sku
price: @parsePrice(product['Цена, руб.*'])
active: true
createdAt: new Date().toISOString()
updatedAt: new Date().toISOString()
domains: [window.location.hostname]
additionalImages: []
attributes: {}
tags: []
# Обработка всех полей CSV в единую структуру attributes
@processAllCSVFields(product, productData)
# Обработка категории
@processCategory(product, productData, index)
.then =>
# Обработка Rich-контента
@processRichContent(product, productData)
debug.log "✅ Данные товара полностью преобразованы: #{productData.sku}"
resolve(productData)
.catch (error) =>
debug.log "⚠️ Ошибка обработки товара, возвращаем частичные данные:", error
# Возвращаем товар даже с ошибками обработки
resolve(productData)
catch error
debug.log "❌ Критическая ошибка преобразования данных товара:", error
reject(error)
# Парсинг цены
parsePrice: (priceString) ->
return 0 if !priceString
try
# Удаляем пробелы и заменяем запятые на точки
cleanPrice = priceString.toString().replace(/\s/g, '').replace(',', '.')
price = parseFloat(cleanPrice)
return if isNaN(price) then 0 else Math.round(price * 100) / 100
catch
return 0
# Обработка всех полей CSV в единую структуру attributes
processAllCSVFields: (product, productData) ->
# Базовые поля
if product['Бренд*']?.trim()
productData.brand = product['Бренд*']?.trim()
if product['Аннотация']?.trim()
productData.description = product['Аннотация']?.trim()
# Цены
if product['Цена до скидки, руб.']
productData.oldPrice = @parsePrice(product['Цена до скидки, руб.'])
# Основные характеристики с оригинальными названиями
@setAttribute productData, 'Вес товара, г', product
@setAttribute productData, 'Объем, л', product
@setAttribute productData, 'Страна-изготовитель', product
@setAttribute productData, 'Гарантия', product
@setAttribute productData, 'Цвет товара', product
@setAttribute productData, 'Название цвета', product
@setAttribute productData, 'Класс опасности товара*', product
@setAttribute productData, 'Степень блеска покрытия', product
@setAttribute productData, 'Работы', product
@setAttribute productData, 'Количество товара в УЕИ', product
# Технические характеристики
@setAttribute productData, 'Расход, л/м2', product
@setAttribute productData, 'Время высыхания, часов', product
@setAttribute productData, 'Вид краски', product
@setAttribute productData, 'Основа краски', product
@setAttribute productData, 'Способ нанесения', product
@setAttribute productData, 'Область применения состава', product
@setAttribute productData, 'Назначение грунтовки', product
@setAttribute productData, 'Рекомендуемое количество слоев', product
@setAttribute productData, 'Расход, кг/м2', product
@setAttribute productData, 'Количество компонентов', product
@setAttribute productData, 'Особенности ЛКМ', product
@setAttribute productData, 'Макс. температура эксплуатации, С°', product
@setAttribute productData, 'Материал основания', product
@setAttribute productData, 'Основа грунтовки', product
@setAttribute productData, 'Форма выпуска средства', product
@setAttribute productData, 'Назначение', product
@setAttribute productData, 'Тип помещения', product
@setAttribute productData, 'Вид выпуска товара', product
@setAttribute productData, 'Тип растворителя', product
@setAttribute productData, 'Эффект краски', product
@setAttribute productData, 'Марка эмали', product
@setAttribute productData, 'Базис', product
@setAttribute productData, 'Помещение', product
# Флаги и булевы значения (объединены с attributes)
@setBooleanAttribute productData, 'Рассрочка', product
@setBooleanAttribute productData, 'Баллы за отзывы', product
@setBooleanAttribute productData, 'Возможность колеровки', product
@setBooleanAttribute productData, 'Аэрозоль', product
@setBooleanAttribute productData, 'Можно мыть', product
# Мета-данные
if product['#Хештеги']
tags = product['#Хештеги']?.split('#').filter((tag) -> tag.trim()).map((tag) -> tag.trim())
productData.tags = tags or []
# Вспомогательный метод для установки атрибута
setAttribute: (productData, fieldName, product) ->
if product[fieldName]?.trim()
# Сохраняем оригинальное название поля
productData.attributes[fieldName] = product[fieldName]?.trim()
# Вспомогательный метод для установки булевых атрибутов
setBooleanAttribute: (productData, fieldName, product) ->
if product[fieldName]
value = product[fieldName]?.toLowerCase()
productData.attributes[fieldName] = value == 'да'
# Обработка категории с проверкой дубликатов
processCategory: (product, productData, index) ->
return Promise.resolve() if !product['Тип*']
categoryName = product['Тип*'].trim()
return Promise.resolve() if !categoryName
debug.log "🔍 Поиск категории: #{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
debug.log "🆕 Создание новой категории: #{categoryName}"
return PouchDB.saveToRemote(newCategory)
.then (result) =>
debug.log "✅ Создана новая категория: #{categoryName}"
@categories.push(newCategory)
return Promise.resolve()
.catch (error) ->
debug.log "❌ Ошибка создания категории #{categoryName}:", error
# Продолжаем без категории
return Promise.resolve()
# Обработка Rich-контента JSON и преобразование в Markdown
processRichContent: (product, productData) ->
# Сначала пробуем Rich-контент JSON
if product['Rich-контент JSON'] and product['Rich-контент JSON'].trim()
try
richContent = JSON.parse(product['Rich-контент JSON'])
markdownDescription = @richContentToMarkdown(richContent)
# Если получили Markdown, используем его как описание
if markdownDescription and markdownDescription != 'Описание товара'
productData.description = markdownDescription
productData.attributes['Rich-контент JSON'] = product['Rich-контент JSON']
return
catch error
debug.log "❌ Ошибка парсинга Rich-контента:", error
# Если Rich-контент невалиден или отсутствует, используем аннотацию
if product['Аннотация'] and product['Аннотация'].trim() and !productData.description
productData.description = product['Аннотация'].trim()
# Преобразование Rich-контента JSON в Markdown
richContentToMarkdown: (richContent) ->
return '' if !richContent
try
markdownParts = []
if richContent.content and Array.isArray(richContent.content)
for item in richContent.content
markdownParts.push(@processContentItem(item))
result = markdownParts.filter((part) -> part).join('\n\n')
return result or 'Описание товара'
catch error
debug.log "❌ Ошибка преобразования Rich-контента в Markdown:", error
return ''
# Обработка отдельного элемента контента
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 ''
# Обработка текстового блока
processTextBlock: (item) ->
textParts = []
# Заголовок
if item.title and item.title.items
titleText = @processTextItems(item.title.items)
if titleText
level = item.title.size or 'size3'
hashes = @getHeadingLevel(level)
textParts.push("#{hashes} #{titleText}")
# Основной текст
if item.text and item.text.items
bodyText = @processTextItems(item.text.items)
if bodyText
textParts.push(bodyText)
return textParts.join('\n\n')
# Обработка заголовка
processHeader: (item) ->
if item.text and item.text.items
headerText = @processTextItems(item.text.items)
if headerText
level = item.size or 'size2'
hashes = @getHeadingLevel(level)
return "#{hashes} #{headerText}"
return ''
# Обработка изображения
processImage: (item) ->
if item.url
altText = item.alt or 'Изображение товара'
return ""
return ''
# Обработка списка
processList: (item) ->
return '' if !item.items or !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 or !Array.isArray(items)
textParts = []
for textItem in items
if textItem.type == 'text' and 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)
toggleProductSelection: (productId) ->
if @isProductSelected(productId)
@selectedProducts = @selectedProducts.filter (id) -> id != productId
else
@selectedProducts.push(productId)
clearSelection: ->
@selectedProducts = []
@selectAll = false
toggleProductStatus: (product) ->
updatedProduct = Object.assign {}, product,
active: !product.active
updatedAt: new Date().toISOString()
PouchDB.saveToRemote(updatedProduct)
.then (result) =>
@loadProducts()
@showNotification "Товар #{if product.active then 'деактивирован' else 'активирован'}"
.catch (error) =>
debug.log 'Ошибка изменения статуса товара:', error
@showNotification 'Ошибка изменения статуса товара', 'error'
activateSelected: ->
if @selectedProducts.length == 0
@showNotification 'Выберите товары для активации', 'error'
return
promises = @selectedProducts.map (productId) =>
product = @products.find (p) -> p._id == productId
if product and !product.active
updatedProduct = Object.assign {}, 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 and product.active
updatedProduct = Object.assign {}, 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(Object.assign {}, doc, { _deleted: true })
Promise.all(promises)
.then (results) =>
@loadProducts()
@clearSelection()
@showNotification "Удалено #{results.length} товаров"
.catch (error) =>
debug.log 'Ошибка удаления товаров:', error
@showNotification 'Ошибка удаления товаров', 'error'
showNotification: (message, type = 'success') ->
if @$root.showNotification?
@$root.showNotification(message, type)
else
debug.log("#{type}: #{message}")
mounted: ->
@loadProducts()
@loadCategories()
@loadDomains()