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:
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'
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
# Mass actions methods
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'
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}")
mounted: ->
@loadProducts()
@loadCategories()
@loadDomains()