document.head.insertAdjacentHTML('beforeend','')
PouchDB = require 'app/utils/pouch'
module.exports =
name: 'AdminProducts'
render: (new Function '_ctx', '_cache', renderFns['app/pages/Admin/Products/index.pug'])()
data: ->
return {
products: []
categories: []
showProductModal: false
showImportModal: false
selectedFile: null
importing: false
importProgress: 0
importResults: null
currentProduct: {
_id: ''
type: 'product'
name: ''
sku: ''
price: 0
oldPrice: 0
category: ''
description: ''
active: true
domains: []
attributes: {}
images: []
createdAt: ''
updatedAt: ''
}
searchQuery: ''
selectedCategory: ''
bulkActions: []
selectedProducts: []
}
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
return products
availableDomains: ->
@$root.currentDomainSettings?.domains or [@$root.currentDomain]
mounted: ->
@loadProducts()
@loadCategories()
methods:
loadProducts: ->
debug.log '📥 Загрузка товаров...'
PouchDB.queryView('admin', 'products', { include_docs: true })
.then (result) =>
@products = result.rows.map (row) -> row.doc
debug.log "✅ Загружено #{@products.length} товаров"
.catch (error) =>
debug.log '❌ Ошибка загрузки товаров:', error
@showNotification 'Ошибка загрузки товаров', 'error'
loadCategories: ->
debug.log '📥 Загрузка категорий...'
PouchDB.queryView('admin', 'categories', { include_docs: true })
.then (result) =>
@categories = result.rows.map (row) -> row.doc
debug.log "✅ Загружено #{@categories.length} категорий"
.catch (error) =>
debug.log '❌ Ошибка загрузки категорий:', error
createCategory: (categoryName) ->
categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9а-яё]/g, '-').replace(/-+/g, '-')
categoryData = {
_id: "category:#{categorySlug}"
type: 'category'
name: categoryName
slug: categorySlug
active: true
order: @categories.length
domains: @availableDomains
createdAt: new Date().toISOString()
updatedAt: new Date().toISOString()
}
PouchDB.saveToRemote(categoryData)
.then (result) =>
debug.log "✅ Создана категория: #{categoryName}"
@loadCategories()
return categorySlug
.catch (error) =>
debug.log '❌ Ошибка создания категории:', error
throw error
transformProductData: (csvData, index) ->
debug.log "🔄 Преобразование данных товара #{index + 1}: #{csvData['Артикул*']}"
sku = csvData['Артикул*']?.toString().trim()
return null unless sku
# Определяем категорию
categoryName = csvData['Тип*']?.trim() or 'Другое'
debug.log "🔍 Поиск категории: #{categoryName}"
# Ищем существующую категорию
existingCategory = @categories.find (cat) -> cat.name == categoryName
if existingCategory
categorySlug = existingCategory.slug
debug.log "✅ Использована существующая категория: #{categoryName}"
else
debug.log "🆕 Создание новой категории: #{categoryName}"
categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9а-яё]/g, '-').replace(/-+/g, '-')
# Категория будет создана позже в процессе импорта
# Базовые данные товара
productData = {
_id: "product:#{sku}"
type: 'product'
name: csvData['Название товара']?.trim() or "Товар #{sku}"
sku: sku
price: parseFloat(csvData['Цена, руб.*']?.replace(/\s/g, '')?.replace(',', '.') or 0)
oldPrice: parseFloat(csvData['Цена до скидки, руб.']?.replace(/\s/g, '')?.replace(',', '.') or 0)
category: categorySlug
brand: csvData['Бренд*']?.trim()
description: csvData['Аннотация']?.trim() or ''
active: true
domains: @availableDomains
attributes: {}
images: []
createdAt: new Date().toISOString()
updatedAt: new Date().toISOString()
}
# Дополнительные атрибуты
additionalAttributes = {}
for key, value of csvData
if value and not key in ['Артикул*', 'Название товара', 'Цена, руб.*', 'Цена до скидки, руб.', 'Тип*', 'Бренд*', 'Аннотация', 'Rich-контент JSON', 'Ссылка на главное фото', 'Ссылки на дополнительные фото']
additionalAttributes[key] = value
productData.attributes = additionalAttributes
# Rich-контент
if csvData['Rich-контент JSON']
try
richContent = JSON.parse(csvData['Rich-контент JSON'])
productData.richContent = richContent
catch error
debug.log '⚠️ Ошибка парсинга Rich-контента:', error
debug.log "✅ Данные товара полностью преобразованы: #{sku}"
return productData
downloadAndStoreImage: (imageUrl, docId, filename) ->
return new Promise (resolve, reject) =>
debug.log "🔄 Начало загрузки изображения: #{imageUrl}"
# Проверяем валидность URL
unless imageUrl and imageUrl.startsWith('http')
debug.log '⚠️ Невалидный URL изображения:', imageUrl
return resolve(null)
# Создаем уникальное имя файла
fileExtension = imageUrl.split('.').pop()?.split('?')[0] or 'jpg'
uniqueFilename = "#{filename}.#{fileExtension}"
debug.log "📁 Документ: #{docId}, Файл: #{uniqueFilename}"
# Используем fetch вместо XMLHttpRequest для лучшей обработки ошибок
fetch(imageUrl)
.then (response) =>
unless response.ok
throw new Error("HTTP #{response.status}: #{response.statusText}")
return response.blob()
.then (blob) =>
debug.log "✅ Blob получен, размер: #{blob.size} байт"
if blob.size == 0
throw new Error('Пустой blob')
# Читаем blob как ArrayBuffer
reader = new FileReader()
reader.onload = (event) =>
try
arrayBuffer = event.target.result
debug.log "✅ ArrayBuffer успешно прочитан, размер: #{arrayBuffer.byteLength} байт"
# Сохраняем attachment в PouchDB
PouchDB.localDb.putAttachment(
docId,
uniqueFilename,
@currentProduct._rev,
blob,
blob.type
)
.then (result) =>
debug.log "✅ Attachment сохранен: #{uniqueFilename}"
resolve({
filename: uniqueFilename
contentType: blob.type
size: blob.size
})
.catch (attachmentError) =>
debug.log "❌ Ошибка сохранения attachment:", attachmentError
reject(attachmentError)
catch readError
debug.log "❌ Ошибка чтения blob:", readError
reject(readError)
reader.onerror = (error) =>
debug.log "❌ Ошибка FileReader:", error
reject(error)
reader.readAsArrayBuffer(blob)
.catch (fetchError) =>
debug.log "❌ Ошибка загрузки изображения:", fetchError
reject(fetchError)
processProductImages: (productData, csvData) ->
debug.log "🖼️ Начало обработки изображений для товара: #{productData.sku}"
imagePromises = []
# Основное изображение
mainImageUrl = csvData['Ссылка на главное фото']?.trim()
if mainImageUrl
imagePromises.push(
@downloadAndStoreImage(mainImageUrl, productData._id, 'main')
.then (imageInfo) =>
if imageInfo
productData.mainImage = imageInfo.filename
return imageInfo
return null
.catch (error) =>
debug.log "⚠️ Не удалось загрузить основное изображение:", error
return null
)
# Дополнительные изображения
additionalImages = csvData['Ссылки на дополнительные фото']
if additionalImages
# Разделяем строку по переносам и фильтруем пустые значения
imageUrls = additionalImages.split('\n')
.map((url) -> url.trim())
.filter((url) -> url and url.startsWith('http'))
.slice(0, 5) # Ограничиваем 5 изображениями
imageUrls.forEach (imageUrl, index) =>
imagePromises.push(
@downloadAndStoreImage(imageUrl, productData._id, "additional-#{index + 1}")
.then (imageInfo) =>
if imageInfo
return imageInfo.filename
return null
.catch (error) =>
debug.log "⚠️ Не удалось загрузить дополнительное изображение:", error
return null
)
return Promise.allSettled(imagePromises)
.then (results) =>
# Фильтруем успешно загруженные изображения
successfulResults = results.filter (r) -> r.status == 'fulfilled' and r.value
additionalFilenames = successfulResults.slice(1).map (r) -> r.value?.filename
productData.additionalImages = additionalFilenames.filter (filename) -> filename
debug.log "✅ Обработано изображений: #{successfulResults.length}"
return productData
.catch (error) =>
debug.log "❌ Ошибка обработки изображений:", error
return productData
saveProduct: (productData) ->
debug.log "💾 Попытка сохранения товара: #{productData.sku}"
return new Promise (resolve, reject) =>
# Сначала пытаемся получить существующий документ для получения _rev
PouchDB.getDocument(productData._id)
.then (existingDoc) =>
debug.log "🔄 Обновление существующего товара: #{productData.sku}"
productData._rev = existingDoc._rev
productData.updatedAt = new Date().toISOString()
# Сохраняем в удаленную БД
PouchDB.saveToRemote(productData)
.then (result) =>
debug.log "✅ Товар сохранен, получение обновленной версии: #{productData.sku}"
# Получаем обновленный документ
PouchDB.getDocument(productData._id)
.then (updatedDoc) =>
debug.log "✅ Документ получен с актуальным _rev: #{updatedDoc._rev?.substring(0, 10)}..."
resolve(updatedDoc)
.catch (getError) =>
debug.log "⚠️ Не удалось получить обновленный документ:", getError
resolve(result)
.catch (saveError) =>
debug.log "❌ Ошибка сохранения товара:", saveError
reject(saveError)
.catch (getError) =>
if getError.status == 404
debug.log "🆕 Создание нового товара: #{productData.sku}"
productData.createdAt = new Date().toISOString()
productData.updatedAt = productData.createdAt
PouchDB.saveToRemote(productData)
.then (result) =>
debug.log "✅ Товар сохранен в БД: #{productData.sku}"
resolve(result)
.catch (saveError) =>
debug.log "❌ Ошибка создания товара:", saveError
reject(saveError)
else
debug.log "❌ Ошибка при получении документа:", getError
reject(getError)
readFile: (file) ->
return new Promise (resolve, reject) =>
reader = new FileReader()
reader.onload = (event) -> resolve(event.target.result)
reader.onerror = (error) -> reject(error)
reader.readAsText(file, 'UTF-8')
importProducts: ->
unless @selectedFile
@showNotification 'Выберите файл для импорта', 'error'
return
@importing = true
@importProgress = 0
@importResults = null
debug.log '📦 Начало импорта товаров...'
@readFile(@selectedFile)
.then (text) =>
# Парсим CSV
results = Papa.parse(text, {
header: true
delimiter: ';'
skipEmptyLines: true
encoding: 'UTF-8'
})
# Фильтруем валидные строки
validProducts = results.data.filter (row, index) =>
row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*']
debug.log "📊 Найдено валидных товаров: #{validProducts.length}"
if validProducts.length == 0
throw new Error('Не найдено валидных товаров для импорта')
# Создаем массив для обещаний
importPromises = []
processedCount = 0
errors = []
# Обрабатываем каждый товар
validProducts.forEach (product, index) =>
promise = =>
debug.log "🔧 Обработка товара #{index + 1}/#{validProducts.length}: #{product['Название товара']?.substring(0, 50)}..."
try
# Преобразуем данные CSV в объект товара
productData = @transformProductData(product, index)
return Promise.resolve(null) unless productData
# Обрабатываем категорию
categoryName = product['Тип*']?.trim() or 'Другое'
existingCategory = @categories.find (cat) -> cat.name == categoryName
if not existingCategory
debug.log "🏷️ Создание категории: #{categoryName}"
return @createCategory(categoryName)
.then (categorySlug) =>
productData.category = categorySlug
# Перезагружаем категории
@loadCategories()
return productData
.catch (categoryError) =>
debug.log "⚠️ Не удалось создать категорию, используется 'Другое'"
productData.category = 'drugoe'
return productData
else
return Promise.resolve(productData)
catch transformError
debug.log "❌ Ошибка преобразования товара:", transformError
errors.push("Товар #{index + 1}: #{transformError.message}")
return Promise.resolve(null)
.then (productData) =>
return null unless productData
# Обрабатываем изображения
return @processProductImages(productData, product)
.then (productWithImages) =>
# Сохраняем товар
return @saveProduct(productWithImages)
.then (savedProduct) =>
processedCount++
@importProgress = Math.round((processedCount / validProducts.length) * 100)
debug.log "✅ Обработан товар #{processedCount}/#{validProducts.length}: #{savedProduct.sku}"
return savedProduct
.catch (saveError) =>
errorMsg = "Товар #{index + 1} (#{productData.sku}): #{saveError.message}"
debug.log "❌ Ошибка обработки товара #{index + 1}:", saveError
errors.push(errorMsg)
return null
importPromises.push(promise())
# Ожидаем завершения всех операций
return Promise.allSettled(importPromises)
.then (results) =>
successfulImports = results.filter((r) -> r.status == 'fulfilled' and r.value).length
failedImports = results.filter((r) -> r.status == 'rejected').length
@importResults = {
success: true
processed: validProducts.length
successful: successfulImports
failed: failedImports
errors: errors
}
debug.log "🎉 Импорт завершен: #{successfulImports} успешно, #{failedImports} с ошибками"
if successfulImports > 0
@showNotification "Импортировано #{successfulImports} товаров"
@loadProducts() # Перезагружаем список
else
@showNotification 'Не удалось импортировать ни одного товара', 'error'
.catch (error) =>
debug.log '❌ Ошибка импорта:', error
@importResults = {
success: false
error: error.message
processed: 0
successful: 0
failed: 0
errors: [error.message]
}
@showNotification "Ошибка импорта: #{error.message}", 'error'
.finally =>
@importing = false
@selectedFile = null
editProduct: (product) ->
@currentProduct = Object.assign({}, product)
@showProductModal = true
deleteProduct: (product) ->
if confirm("Удалить товар \"#{product.name}\"?")
PouchDB.localDb.remove(product)
.then =>
PouchDB.saveToRemote(product) # Удаляем из удаленной БД
.then =>
@showNotification 'Товар удален'
@loadProducts()
.catch (error) =>
debug.log '❌ Ошибка удаления товара из удаленной БД:', error
@showNotification 'Ошибка удаления товара', 'error'
.catch (error) =>
debug.log '❌ Ошибка удаления товара:', error
@showNotification 'Ошибка удаления товара', 'error'
saveProductForm: ->
unless @currentProduct.name and @currentProduct.sku and @currentProduct.price
@showNotification 'Заполните обязательные поля', 'error'
return
productData = Object.assign({}, @currentProduct)
if productData._id
# Обновление существующего товара
productData.updatedAt = new Date().toISOString()
else
# Создание нового товара
productData._id = "product:#{productData.sku}"
productData.type = 'product'
productData.createdAt = new Date().toISOString()
productData.updatedAt = productData.createdAt
@saveProduct(productData)
.then (result) =>
@showProductModal = false
@resetCurrentProduct()
@showNotification 'Товар сохранен'
@loadProducts()
.catch (error) =>
debug.log '❌ Ошибка сохранения товара:', error
@showNotification 'Ошибка сохранения товара', 'error'
resetCurrentProduct: ->
@currentProduct = {
_id: ''
type: 'product'
name: ''
sku: ''
price: 0
oldPrice: 0
category: ''
description: ''
active: true
domains: @availableDomains
attributes: {}
images: []
createdAt: ''
updatedAt: ''
}
showNotification: (message, type = 'success') ->
@$root.showNotification(message, type)
handleFileSelect: (event) ->
@selectedFile = event.target.files[0]
debug.log "📁 Выбран файл: #{@selectedFile?.name}"
toggleAllProducts: (event) ->
if event.target.checked
@selectedProducts = @filteredProducts.map (product) -> product._id
else
@selectedProducts = []