|
|
@@ -19,6 +19,10 @@ module.exports =
|
|
|
showImportModal: false
|
|
|
showCategoriesModal: false
|
|
|
showCategoryModal: false
|
|
|
+ showMassActionsModal: false
|
|
|
+ showCategoryAssignModal: false
|
|
|
+ showMassCategoryAssign: false
|
|
|
+ showMassPriceModal: false
|
|
|
editingProduct: null
|
|
|
editingCategory: null
|
|
|
selectedFile: null
|
|
|
@@ -30,6 +34,17 @@ module.exports =
|
|
|
availableDomains: []
|
|
|
categoriesActiveTab: 'list'
|
|
|
|
|
|
+ # Mass actions data
|
|
|
+ selectedProducts: []
|
|
|
+ selectAll: false
|
|
|
+ massCategory: ''
|
|
|
+ massAllCategory: ''
|
|
|
+ removeExistingCategories: false
|
|
|
+ massRemoveAllCategories: false
|
|
|
+ priceChangeType: 'fixed'
|
|
|
+ priceChangeValue: null
|
|
|
+ applyToOldPrice: false
|
|
|
+
|
|
|
productForm: {
|
|
|
name: ''
|
|
|
sku: ''
|
|
|
@@ -60,19 +75,16 @@ module.exports =
|
|
|
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'
|
|
|
@@ -81,236 +93,355 @@ module.exports =
|
|
|
return products
|
|
|
|
|
|
methods:
|
|
|
- loadProducts: ->
|
|
|
- PouchDB.queryView('admin', 'products', { include_docs: true })
|
|
|
- .then (result) =>
|
|
|
- @products = result.rows.map (row) -> row.doc
|
|
|
- .catch (error) =>
|
|
|
- console.error 'Ошибка загрузки товаров:', error
|
|
|
- @showNotification 'Ошибка загрузки товаров', 'error'
|
|
|
+ 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) =>
|
|
|
- console.error 'Ошибка загрузки категорий:', error
|
|
|
+ debug.log 'Ошибка загрузки категорий:', error
|
|
|
|
|
|
loadDomains: ->
|
|
|
PouchDB.queryView('admin', 'domain_settings', { include_docs: true })
|
|
|
.then (result) =>
|
|
|
@availableDomains = result.rows.map (row) -> row.doc
|
|
|
.catch (error) =>
|
|
|
- console.error 'Ошибка загрузки доменов:', error
|
|
|
+ debug.log 'Ошибка загрузки доменов:', error
|
|
|
|
|
|
- getCategoryName: (categoryId) ->
|
|
|
- category = @categories.find (cat) -> cat._id == categoryId
|
|
|
- category?.name || 'Без категории'
|
|
|
|
|
|
- getCategoryProductCount: (categoryId) ->
|
|
|
- @products.filter((product) -> product.category == categoryId).length
|
|
|
+ # Mass actions methods
|
|
|
+ toggleSelectAll: ->
|
|
|
+ if @selectAll
|
|
|
+ @selectedProducts = @filteredProducts.map (product) -> product._id
|
|
|
+ else
|
|
|
+ @selectedProducts = []
|
|
|
|
|
|
- # Управление товарами
|
|
|
- editProduct: (product) ->
|
|
|
- @editingProduct = product
|
|
|
- @productForm = {
|
|
|
- name: product.name || ''
|
|
|
- sku: product.sku || ''
|
|
|
- category: product.category || ''
|
|
|
- price: product.price || 0
|
|
|
- oldPrice: product.oldPrice || 0
|
|
|
- brand: product.brand || ''
|
|
|
- description: product.description || ''
|
|
|
- image: product.image || ''
|
|
|
- active: product.active != false
|
|
|
- domains: product.domains || []
|
|
|
- }
|
|
|
- @showProductModal = true
|
|
|
+ isProductSelected: (productId) ->
|
|
|
+ @selectedProducts.includes(productId)
|
|
|
+
|
|
|
+ clearSelection: ->
|
|
|
+ @selectedProducts = []
|
|
|
+ @selectAll = false
|
|
|
|
|
|
- saveProduct: ->
|
|
|
- if !@productForm.name || !@productForm.sku || !@productForm.price
|
|
|
- @showNotification 'Заполните обязательные поля (Название, Артикул, Цена)', 'error'
|
|
|
+ activateSelected: ->
|
|
|
+ if @selectedProducts.length == 0
|
|
|
+ @showNotification 'Выберите товары для активации', 'error'
|
|
|
return
|
|
|
|
|
|
- productData = {
|
|
|
- type: 'product'
|
|
|
- ...@productForm
|
|
|
- updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
-
|
|
|
- if @editingProduct
|
|
|
- productData._id = @editingProduct._id
|
|
|
- productData._rev = @editingProduct._rev
|
|
|
- productData.createdAt = @editingProduct.createdAt
|
|
|
- else
|
|
|
- productData._id = "product:#{Date.now()}"
|
|
|
- productData.createdAt = new Date().toISOString()
|
|
|
-
|
|
|
- PouchDB.saveToRemote(productData)
|
|
|
- .then (result) =>
|
|
|
- @showProductModal = false
|
|
|
- @resetProductForm()
|
|
|
+ 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()
|
|
|
- @showNotification 'Товар успешно сохранен'
|
|
|
+ @clearSelection()
|
|
|
+ @showNotification "Активировано #{results.length} товаров"
|
|
|
.catch (error) =>
|
|
|
- console.error 'Ошибка сохранения товара:', error
|
|
|
- @showNotification 'Ошибка сохранения товара', 'error'
|
|
|
+ debug.log 'Ошибка активации товаров:', error
|
|
|
+ @showNotification 'Ошибка активации товаров', 'error'
|
|
|
|
|
|
- removeProductImage: ->
|
|
|
- @productForm.image = ''
|
|
|
-
|
|
|
- onProductImageUpload: (event) ->
|
|
|
- file = event.target.files[0]
|
|
|
- if file
|
|
|
- reader = new FileReader()
|
|
|
- reader.onload = (e) =>
|
|
|
- @productForm.image = e.target.result
|
|
|
- reader.readAsDataURL(file)
|
|
|
+ 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)
|
|
|
|
|
|
- # Управление категориями
|
|
|
- editCategory: (category) ->
|
|
|
- @editingCategory = category
|
|
|
- @categoryForm = {
|
|
|
- name: category.name || ''
|
|
|
- slug: category.slug || ''
|
|
|
- description: category.description || ''
|
|
|
- parentCategory: category.parentCategory || ''
|
|
|
- sortOrder: category.sortOrder || 0
|
|
|
- image: category.image || ''
|
|
|
- icon: category.icon || ''
|
|
|
- active: category.active != false
|
|
|
- domains: category.domains || []
|
|
|
- }
|
|
|
- @showCategoryModal = true
|
|
|
+ Promise.all(promises)
|
|
|
+ .then (results) =>
|
|
|
+ @loadProducts()
|
|
|
+ @clearSelection()
|
|
|
+ @showNotification "Деактивировано #{results.length} товаров"
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log 'Ошибка деактивации товаров:', error
|
|
|
+ @showNotification 'Ошибка деактивации товаров', 'error'
|
|
|
|
|
|
- saveCategory: ->
|
|
|
- if !@categoryForm.name || !@categoryForm.slug
|
|
|
- @showNotification 'Заполните обязательные поля (Название, URL slug)', 'error'
|
|
|
+ deleteSelected: ->
|
|
|
+ if @selectedProducts.length == 0
|
|
|
+ @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()
|
|
|
+ if !confirm("Вы уверены, что хотите удалить #{@selectedProducts.length} товаров?")
|
|
|
+ return
|
|
|
|
|
|
- PouchDB.saveToRemote(categoryData)
|
|
|
- .then (result) =>
|
|
|
- @showCategoryModal = false
|
|
|
- @resetCategoryForm()
|
|
|
- @loadCategories()
|
|
|
- @showNotification 'Категория успешно сохранена'
|
|
|
+ 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) =>
|
|
|
- console.error 'Ошибка сохранения категории:', error
|
|
|
- @showNotification 'Ошибка сохранения категории', 'error'
|
|
|
+ debug.log 'Ошибка удаления товаров:', error
|
|
|
+ @showNotification 'Ошибка удаления товаров', 'error'
|
|
|
|
|
|
- removeCategoryImage: ->
|
|
|
- @categoryForm.image = ''
|
|
|
+ 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)
|
|
|
|
|
|
- removeCategoryIcon: ->
|
|
|
- @categoryForm.icon = ''
|
|
|
+ Promise.all(promises)
|
|
|
+ .then (results) =>
|
|
|
+ @loadProducts()
|
|
|
+ @clearSelection()
|
|
|
+ @showCategoryAssignModal = false
|
|
|
+ @massCategory = ''
|
|
|
+ @removeExistingCategories = false
|
|
|
+ @showNotification "Категория назначена для #{results.length} товаров"
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log 'Ошибка назначения категории:', error
|
|
|
+ @showNotification 'Ошибка назначения категории', 'error'
|
|
|
|
|
|
- onCategoryImageUpload: (event) ->
|
|
|
- file = event.target.files[0]
|
|
|
- if file
|
|
|
- reader = new FileReader()
|
|
|
- reader.onload = (e) =>
|
|
|
- @categoryForm.image = e.target.result
|
|
|
- reader.readAsDataURL(file)
|
|
|
+ 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)
|
|
|
|
|
|
- onCategoryIconUpload: (event) ->
|
|
|
- file = event.target.files[0]
|
|
|
- if file
|
|
|
- reader = new FileReader()
|
|
|
- reader.onload = (e) =>
|
|
|
- @categoryForm.icon = e.target.result
|
|
|
- reader.readAsDataURL(file)
|
|
|
+ Promise.all(promises)
|
|
|
+ .then (results) =>
|
|
|
+ @loadProducts()
|
|
|
+ @showMassCategoryAssign = false
|
|
|
+ @massAllCategory = ''
|
|
|
+ @massRemoveAllCategories = false
|
|
|
+ @showNotification "Категория назначена для всех товаров"
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log 'Ошибка назначения категории:', error
|
|
|
+ @showNotification 'Ошибка назначения категории', 'error'
|
|
|
|
|
|
- deleteCategory: (categoryId) ->
|
|
|
- if confirm('Вы уверены, что хотите удалить эту категорию?')
|
|
|
- PouchDB.getDocument(categoryId)
|
|
|
- .then (doc) ->
|
|
|
- PouchDB.saveToRemote({ ...doc, _deleted: true })
|
|
|
- .then (result) =>
|
|
|
- @loadCategories()
|
|
|
- @showNotification 'Категория удалена'
|
|
|
- .catch (error) =>
|
|
|
- console.error 'Ошибка удаления категории:', 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()
|
|
|
|
|
|
- # Импорт товаров
|
|
|
- onFileSelect: (event) ->
|
|
|
- @selectedFile = event.target.files[0]
|
|
|
- @importResults = null
|
|
|
+ Promise.all(promises)
|
|
|
+ .then (results) =>
|
|
|
+ @loadProducts()
|
|
|
+ @showNotification "Статус всех товаров изменен"
|
|
|
+ .catch (error) =>
|
|
|
+ debug.log 'Ошибка изменения статуса:', error
|
|
|
+ @showNotification 'Ошибка изменения статуса', 'error'
|
|
|
|
|
|
- importProducts: ->
|
|
|
- if !@selectedFile
|
|
|
- @showNotification 'Выберите файл для импорта', 'error'
|
|
|
+ massRemoveCategories: ->
|
|
|
+ if !confirm("Удалить категории у всех товаров?")
|
|
|
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'
|
|
|
+ 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)
|
|
|
|
|
|
- products = results.data.filter (row) =>
|
|
|
- row && row['Артикул*'] && row['Название товара'] && row['Цена, руб.*']
|
|
|
+ when 'percent'
|
|
|
+ if @applyToOldPrice && updatedProduct.oldPrice
|
|
|
+ updatedProduct.oldPrice = updatedProduct.oldPrice * (1 + parseFloat(@priceChangeValue) / 100)
|
|
|
+ else
|
|
|
+ updatedProduct.price = updatedProduct.price * (1 + parseFloat(@priceChangeValue) / 100)
|
|
|
|
|
|
- couchProducts = products.map (product, index) =>
|
|
|
- @transformProductData(product, index)
|
|
|
+ when 'increase'
|
|
|
+ if @applyToOldPrice && updatedProduct.oldPrice
|
|
|
+ updatedProduct.oldPrice = updatedProduct.oldPrice + parseFloat(@priceChangeValue)
|
|
|
+ else
|
|
|
+ updatedProduct.price = updatedProduct.price + parseFloat(@priceChangeValue)
|
|
|
|
|
|
- # Пакетное сохранение
|
|
|
- PouchDB.bulkDocs(couchProducts)
|
|
|
- .then (result) =>
|
|
|
- @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'
|
|
|
+ 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))
|
|
|
|
|
|
- catch error
|
|
|
- @importResults = {
|
|
|
- success: false,
|
|
|
- error: error.message,
|
|
|
- processed: 0,
|
|
|
- errors: [error.message]
|
|
|
- }
|
|
|
- @importing = false
|
|
|
- @showNotification "Ошибка обработки файла: #{error.message}", 'error'
|
|
|
+ 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)
|
|
|
|
|
|
- reader.readAsText(@selectedFile, 'UTF-8')
|
|
|
+ @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'
|
|
|
@@ -322,17 +453,19 @@ module.exports =
|
|
|
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
|
|
|
@@ -345,11 +478,18 @@ module.exports =
|
|
|
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(',', '.'))
|
|
|
|
|
|
@@ -362,7 +502,6 @@ module.exports =
|
|
|
if product['Тип*']
|
|
|
productData.productType = product['Тип*']
|
|
|
|
|
|
- # Rich content преобразование
|
|
|
if product['Rich-контент JSON']
|
|
|
try
|
|
|
richContent = JSON.parse(product['Rich-контент JSON'])
|
|
|
@@ -372,189 +511,50 @@ module.exports =
|
|
|
else
|
|
|
productData.description = product['Аннотация'] || ''
|
|
|
|
|
|
- # Домены
|
|
|
productData.domains = @availableDomains?.map((d) -> d.domain) || []
|
|
|
|
|
|
return productData
|
|
|
|
|
|
- # Импорт категорий
|
|
|
- onCategoriesFileSelect: (event) ->
|
|
|
- @selectedCategoriesFile = event.target.files[0]
|
|
|
- @categoriesImportResults = null
|
|
|
-
|
|
|
- importCategories: ->
|
|
|
- if !@selectedCategoriesFile
|
|
|
- @showNotification 'Выберите файл категорий для импорта', 'error'
|
|
|
+ # Updated category creation method with duplicate prevention
|
|
|
+ saveCategory: ->
|
|
|
+ if !@categoryForm.name || !@categoryForm.slug
|
|
|
+ @showNotification 'Заполните обязательные поля (Название, URL slug)', 'error'
|
|
|
return
|
|
|
|
|
|
- @importingCategories = true
|
|
|
- @categoriesImportResults = null
|
|
|
+ # Check for duplicate category name
|
|
|
+ duplicateCategory = @categories.find (cat) =>
|
|
|
+ cat.name?.toLowerCase() == @categoryForm.name.toLowerCase() &&
|
|
|
+ (!@editingCategory || cat._id != @editingCategory._id)
|
|
|
|
|
|
- reader = new FileReader()
|
|
|
- reader.onload = (e) =>
|
|
|
- try
|
|
|
- results = Papa.parse e.target.result, {
|
|
|
- header: true
|
|
|
- delimiter: ','
|
|
|
- skipEmptyLines: true
|
|
|
- encoding: 'UTF-8'
|
|
|
- }
|
|
|
-
|
|
|
- categories = results.data.filter (row) =>
|
|
|
- row && row.name && row.slug
|
|
|
-
|
|
|
- couchCategories = categories.map (category, index) =>
|
|
|
- @transformCategoryData(category, index)
|
|
|
-
|
|
|
- # Пакетное сохранение категорий
|
|
|
- PouchDB.bulkDocs(couchCategories)
|
|
|
- .then (result) =>
|
|
|
- @categoriesImportResults = {
|
|
|
- success: true,
|
|
|
- processed: couchCategories.length,
|
|
|
- errors: []
|
|
|
- }
|
|
|
- @importingCategories = false
|
|
|
- @loadCategories()
|
|
|
- @showNotification "Импортировано #{couchCategories.length} категорий"
|
|
|
- .catch (error) =>
|
|
|
- @categoriesImportResults = {
|
|
|
- success: false,
|
|
|
- error: error.message,
|
|
|
- processed: 0,
|
|
|
- errors: [error.message]
|
|
|
- }
|
|
|
- @importingCategories = false
|
|
|
- @showNotification "Ошибка импорта категорий: #{error.message}", 'error'
|
|
|
-
|
|
|
- catch error
|
|
|
- @categoriesImportResults = {
|
|
|
- success: false,
|
|
|
- error: error.message,
|
|
|
- processed: 0,
|
|
|
- errors: [error.message]
|
|
|
- }
|
|
|
- @importingCategories = false
|
|
|
- @showNotification "Ошибка обработки файла категорий: #{error.message}", 'error'
|
|
|
+ if duplicateCategory
|
|
|
+ @showNotification 'Категория с таким названием уже существует', 'error'
|
|
|
+ return
|
|
|
|
|
|
- reader.readAsText(@selectedCategoriesFile, 'UTF-8')
|
|
|
-
|
|
|
- transformCategoryData: (category, index) ->
|
|
|
categoryData = {
|
|
|
- _id: "category:import-#{Date.now()}-#{index}"
|
|
|
type: 'category'
|
|
|
- name: category.name
|
|
|
- slug: category.slug
|
|
|
- description: category.description || ''
|
|
|
- parentCategory: category.parentCategory || ''
|
|
|
- sortOrder: parseInt(category.sortOrder) || @categories.length + index
|
|
|
- active: category.active != 'false'
|
|
|
- createdAt: new Date().toISOString()
|
|
|
+ ...@categoryForm
|
|
|
updatedAt: new Date().toISOString()
|
|
|
- domains: @availableDomains?.map((d) -> d.domain) || []
|
|
|
}
|
|
|
|
|
|
- if category.image
|
|
|
- categoryData.image = category.image
|
|
|
-
|
|
|
- if category.icon
|
|
|
- categoryData.icon = category.icon
|
|
|
-
|
|
|
- return categoryData
|
|
|
-
|
|
|
- # Вспомогательные методы
|
|
|
- generateSlug: (text) ->
|
|
|
- text.toLowerCase()
|
|
|
- .replace(/\s+/g, '-')
|
|
|
- .replace(/[^\w\-]+/g, '')
|
|
|
- .replace(/\-\-+/g, '-')
|
|
|
- .replace(/^-+/, '')
|
|
|
- .replace(/-+$/, '')
|
|
|
-
|
|
|
- richContentToMarkdown: (richContent) ->
|
|
|
- # Простое преобразование rich content в markdown
|
|
|
- return JSON.stringify(richContent) # Временная реализация
|
|
|
-
|
|
|
- toggleProductStatus: (product) ->
|
|
|
- updatedProduct = {
|
|
|
- ...product
|
|
|
- active: !product.active
|
|
|
- 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(updatedProduct)
|
|
|
+ PouchDB.saveToRemote(categoryData)
|
|
|
.then (result) =>
|
|
|
- @loadProducts()
|
|
|
- @showNotification 'Статус товара обновлен'
|
|
|
+ @showCategoryModal = false
|
|
|
+ @resetCategoryForm()
|
|
|
+ @loadCategories()
|
|
|
+ @showNotification 'Категория успешно сохранена'
|
|
|
.catch (error) =>
|
|
|
- console.error 'Ошибка обновления статуса:', error
|
|
|
- @showNotification 'Ошибка обновления статуса', 'error'
|
|
|
-
|
|
|
- deleteProduct: (productId) ->
|
|
|
- if confirm('Вы уверены, что хотите удалить этот товар?')
|
|
|
- PouchDB.getDocument(productId)
|
|
|
- .then (doc) ->
|
|
|
- PouchDB.saveToRemote({ ...doc, _deleted: true })
|
|
|
- .then (result) =>
|
|
|
- @loadProducts()
|
|
|
- @showNotification 'Товар удален'
|
|
|
- .catch (error) =>
|
|
|
- console.error 'Ошибка удаления товара:', error
|
|
|
- @showNotification 'Ошибка удаления товара', 'error'
|
|
|
-
|
|
|
- resetProductForm: ->
|
|
|
- @editingProduct = null
|
|
|
- @productForm = {
|
|
|
- name: ''
|
|
|
- sku: ''
|
|
|
- category: ''
|
|
|
- price: 0
|
|
|
- oldPrice: 0
|
|
|
- brand: ''
|
|
|
- description: ''
|
|
|
- image: ''
|
|
|
- active: true
|
|
|
- domains: []
|
|
|
- }
|
|
|
-
|
|
|
- resetCategoryForm: ->
|
|
|
- @editingCategory = null
|
|
|
- @categoryForm = {
|
|
|
- name: ''
|
|
|
- slug: ''
|
|
|
- description: ''
|
|
|
- parentCategory: ''
|
|
|
- sortOrder: 0
|
|
|
- image: ''
|
|
|
- icon: ''
|
|
|
- active: true
|
|
|
- domains: []
|
|
|
- }
|
|
|
-
|
|
|
- getCategoriesTabClass: (tabId) ->
|
|
|
- baseClass = 'admin-products__categories-tab'
|
|
|
- isActive = @categoriesActiveTab == tabId
|
|
|
-
|
|
|
- if isActive
|
|
|
- return "#{baseClass} admin-products__categories-tab--active"
|
|
|
- else
|
|
|
- return baseClass
|
|
|
-
|
|
|
- formatPrice: (price) ->
|
|
|
- return '0 ₽' if !price
|
|
|
- new Intl.NumberFormat('ru-RU', {
|
|
|
- style: 'currency'
|
|
|
- currency: 'RUB'
|
|
|
- minimumFractionDigits: 0
|
|
|
- }).format(price)
|
|
|
-
|
|
|
- getStatusClass: (isActive) ->
|
|
|
- baseClass = 'admin-products__status'
|
|
|
- if isActive
|
|
|
- return "#{baseClass} admin-products__status--active"
|
|
|
- else
|
|
|
- return "#{baseClass} admin-products__status--inactive"
|
|
|
+ debug.log 'Ошибка сохранения категории:', error
|
|
|
+ @showNotification 'Ошибка сохранения категории', 'error'
|
|
|
|
|
|
+ # Rest of the methods remain the same...
|
|
|
showNotification: (message, type = 'success') ->
|
|
|
@$root.showNotification?(message, type) || debug.log("#{type}: #{message}")
|
|
|
|