|
|
@@ -9,67 +9,63 @@ module.exports =
|
|
|
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: []
|
|
|
- }
|
|
|
- }
|
|
|
+ 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: ->
|
|
|
@@ -78,7 +74,7 @@ module.exports =
|
|
|
if @searchQuery
|
|
|
query = @searchQuery.toLowerCase()
|
|
|
products = products.filter (product) =>
|
|
|
- product.name?.toLowerCase().includes(query) ||
|
|
|
+ product.name?.toLowerCase().includes(query) or
|
|
|
product.sku?.toLowerCase().includes(query)
|
|
|
|
|
|
if @selectedCategory
|
|
|
@@ -117,7 +113,7 @@ module.exports =
|
|
|
|
|
|
getCategoryName: (categoryId) ->
|
|
|
category = @categories.find (cat) -> cat._id == categoryId
|
|
|
- category?.name || 'Без категории'
|
|
|
+ category?.name or 'Без категории'
|
|
|
|
|
|
getCategoryProductCount: (categoryId) ->
|
|
|
@products.filter((product) -> product.category == categoryId).length
|
|
|
@@ -138,45 +134,44 @@ module.exports =
|
|
|
reader = new FileReader()
|
|
|
reader.onload = (e) =>
|
|
|
try
|
|
|
- results = Papa.parse e.target.result, {
|
|
|
+ results = Papa.parse e.target.result,
|
|
|
header: true
|
|
|
delimiter: ';'
|
|
|
skipEmptyLines: true
|
|
|
encoding: 'UTF-8'
|
|
|
- }
|
|
|
|
|
|
products = results.data.filter (row) =>
|
|
|
- row && row['Артикул*'] && row['Название товара'] && row['Цена, руб.*']
|
|
|
+ row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*']
|
|
|
|
|
|
# Обрабатываем товары последовательно для загрузки изображений
|
|
|
@processProductsSequentially(products)
|
|
|
.then (couchProducts) =>
|
|
|
- @importResults = {
|
|
|
- success: true,
|
|
|
- processed: couchProducts.length,
|
|
|
+ @importResults =
|
|
|
+ success: true
|
|
|
+ processed: couchProducts.length
|
|
|
errors: []
|
|
|
- }
|
|
|
+
|
|
|
@importing = false
|
|
|
@loadProducts()
|
|
|
@loadCategories()
|
|
|
@showNotification "Импортировано #{couchProducts.length} товаров"
|
|
|
.catch (error) =>
|
|
|
- @importResults = {
|
|
|
- success: false,
|
|
|
- error: error.message,
|
|
|
- processed: 0,
|
|
|
+ @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,
|
|
|
+ @importResults =
|
|
|
+ success: false
|
|
|
+ error: error.message
|
|
|
+ processed: 0
|
|
|
errors: [error.message]
|
|
|
- }
|
|
|
+
|
|
|
@importing = false
|
|
|
@showNotification "Ошибка обработки файла: #{error.message}", 'error'
|
|
|
|
|
|
@@ -210,17 +205,16 @@ module.exports =
|
|
|
return new Promise (resolve, reject) =>
|
|
|
try
|
|
|
# Базовые поля
|
|
|
- productData = {
|
|
|
+ productData =
|
|
|
_id: "product:#{Date.now()}-#{index}"
|
|
|
type: 'product'
|
|
|
- name: product['Название товара']?.trim() || 'Без названия'
|
|
|
- sku: product['Артикул*']?.trim() || "SKU-#{Date.now()}-#{index}"
|
|
|
+ name: product['Название товара']?.trim() or 'Без названия'
|
|
|
+ sku: product['Артикул*']?.trim() or "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)
|
|
|
@@ -253,20 +247,20 @@ module.exports =
|
|
|
# Удаляем пробелы и заменяем запятые на точки
|
|
|
cleanPrice = priceString.toString().replace(/\s/g, '').replace(',', '.')
|
|
|
price = parseFloat(cleanPrice)
|
|
|
- return isNaN(price) ? 0 : price
|
|
|
+ return if isNaN(price) then 0 else 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() || ''
|
|
|
+ productData.brand = product['Бренд*']?.trim() or ''
|
|
|
+ productData.productType = product['Тип*']?.trim() or ''
|
|
|
+ productData.weight = product['Вес товара, г']?.trim() or ''
|
|
|
+ productData.volume = product['Объем, л']?.trim() or ''
|
|
|
+ productData.country = product['Страна-изготовитель']?.trim() or ''
|
|
|
+ productData.warranty = product['Гарантия']?.trim() or ''
|
|
|
+ productData.color = product['Цвет товара']?.trim() or ''
|
|
|
|
|
|
# Цены
|
|
|
if product['Цена до скидки, руб.']
|
|
|
@@ -293,15 +287,14 @@ module.exports =
|
|
|
productData.tags = []
|
|
|
if product['#Хештеги']
|
|
|
tags = product['#Хештеги']?.split('#').filter((tag) -> tag.trim()).map((tag) -> tag.trim())
|
|
|
- productData.tags = tags || []
|
|
|
+ productData.tags = tags or []
|
|
|
|
|
|
# Статусы и флаги
|
|
|
- productData.features = {
|
|
|
+ productData.features =
|
|
|
installment: product['Рассрочка']?.toLowerCase() == 'да'
|
|
|
reviewPoints: product['Баллы за отзывы']?.toLowerCase() == 'да'
|
|
|
canBeTinted: product['Возможность колеровки']?.toLowerCase() == 'да'
|
|
|
aerosol: product['Аэрозоль']?.toLowerCase() == 'да'
|
|
|
- }
|
|
|
|
|
|
# Обработка категории с проверкой дубликатов
|
|
|
processCategory: (product, productData, index) ->
|
|
|
@@ -321,7 +314,7 @@ module.exports =
|
|
|
else
|
|
|
# Создание новой категории
|
|
|
categoryId = "category:#{Date.now()}-#{index}"
|
|
|
- newCategory = {
|
|
|
+ newCategory =
|
|
|
_id: categoryId
|
|
|
type: 'category'
|
|
|
name: categoryName
|
|
|
@@ -331,7 +324,6 @@ module.exports =
|
|
|
createdAt: new Date().toISOString()
|
|
|
updatedAt: new Date().toISOString()
|
|
|
domains: [window.location.hostname]
|
|
|
- }
|
|
|
|
|
|
productData.category = categoryId
|
|
|
|
|
|
@@ -355,7 +347,7 @@ module.exports =
|
|
|
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 = productData.attachments or {}
|
|
|
productData.attachments.main = attachmentInfo
|
|
|
return Promise.resolve()
|
|
|
.catch (error) =>
|
|
|
@@ -379,7 +371,7 @@ module.exports =
|
|
|
# Обрабатываем первые 5 изображений чтобы не перегружать
|
|
|
imagePromises = []
|
|
|
productData.additionalImages = []
|
|
|
- productData.attachments = productData.attachments || {}
|
|
|
+ productData.attachments = productData.attachments or {}
|
|
|
|
|
|
for imageUrl, i in imageUrls.slice(0, 5)
|
|
|
do (imageUrl, i) =>
|
|
|
@@ -418,18 +410,17 @@ module.exports =
|
|
|
# Сохраняем как attachment в PouchDB
|
|
|
PouchDB.putAttachment(docId, filename, arrayBuffer, blob.type)
|
|
|
.then (result) =>
|
|
|
- resolve({
|
|
|
+ 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}`))
|
|
|
+ reject(new Error("Ошибка загрузки изображения: #{xhr.status}"))
|
|
|
|
|
|
xhr.onerror = =>
|
|
|
reject(new Error('Ошибка сети при загрузке изображения'))
|
|
|
@@ -442,7 +433,7 @@ module.exports =
|
|
|
# Обработка Rich-контента JSON и преобразование в Markdown
|
|
|
processRichContent: (product, productData) ->
|
|
|
# Сначала пробуем Rich-контент JSON
|
|
|
- if product['Rich-контент JSON'] && product['Rich-контент JSON'].trim()
|
|
|
+ if product['Rich-контент JSON'] and product['Rich-контент JSON'].trim()
|
|
|
try
|
|
|
richContent = JSON.parse(product['Rich-контент JSON'])
|
|
|
productData.description = @richContentToMarkdown(richContent)
|
|
|
@@ -452,7 +443,7 @@ module.exports =
|
|
|
debug.log "Ошибка парсинга Rich-контента:", error
|
|
|
|
|
|
# Если Rich-контент невалиден или отсутствует, используем аннотацию
|
|
|
- if product['Аннотация'] && product['Аннотация'].trim()
|
|
|
+ if product['Аннотация'] and product['Аннотация'].trim()
|
|
|
productData.description = product['Аннотация'].trim()
|
|
|
else
|
|
|
productData.description = ''
|
|
|
@@ -464,12 +455,12 @@ module.exports =
|
|
|
try
|
|
|
markdownParts = []
|
|
|
|
|
|
- if richContent.content && Array.isArray(richContent.content)
|
|
|
+ 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 || 'Описание товара'
|
|
|
+ return result or 'Описание товара'
|
|
|
catch error
|
|
|
debug.log "Ошибка преобразования Rich-контента в Markdown:", error
|
|
|
return JSON.stringify(richContent)
|
|
|
@@ -495,15 +486,15 @@ module.exports =
|
|
|
textParts = []
|
|
|
|
|
|
# Заголовок
|
|
|
- if item.title && item.title.items
|
|
|
+ if item.title and item.title.items
|
|
|
titleText = @processTextItems(item.title.items)
|
|
|
if titleText
|
|
|
- level = item.title.size || 'size3'
|
|
|
+ level = item.title.size or 'size3'
|
|
|
hashes = @getHeadingLevel(level)
|
|
|
textParts.push("#{hashes} #{titleText}")
|
|
|
|
|
|
# Основной текст
|
|
|
- if item.text && item.text.items
|
|
|
+ if item.text and item.text.items
|
|
|
bodyText = @processTextItems(item.text.items)
|
|
|
if bodyText
|
|
|
textParts.push(bodyText)
|
|
|
@@ -512,10 +503,10 @@ module.exports =
|
|
|
|
|
|
# Обработка заголовка
|
|
|
processHeader: (item) ->
|
|
|
- if item.text && item.text.items
|
|
|
+ if item.text and item.text.items
|
|
|
headerText = @processTextItems(item.text.items)
|
|
|
if headerText
|
|
|
- level = item.size || 'size2'
|
|
|
+ level = item.size or 'size2'
|
|
|
hashes = @getHeadingLevel(level)
|
|
|
return "#{hashes} #{headerText}"
|
|
|
return ''
|
|
|
@@ -523,13 +514,13 @@ module.exports =
|
|
|
# Обработка изображения
|
|
|
processImage: (item) ->
|
|
|
if item.url
|
|
|
- altText = item.alt || 'Изображение товара'
|
|
|
+ altText = item.alt or 'Изображение товара'
|
|
|
return ""
|
|
|
return ''
|
|
|
|
|
|
# Обработка списка
|
|
|
processList: (item) ->
|
|
|
- return '' if !item.items || !Array.isArray(item.items)
|
|
|
+ return '' if !item.items or !Array.isArray(item.items)
|
|
|
|
|
|
listItems = []
|
|
|
for listItem in item.items
|
|
|
@@ -540,11 +531,11 @@ module.exports =
|
|
|
|
|
|
# Обработка текстовых элементов
|
|
|
processTextItems: (items) ->
|
|
|
- return '' if !items || !Array.isArray(items)
|
|
|
+ return '' if !items or !Array.isArray(items)
|
|
|
|
|
|
textParts = []
|
|
|
for textItem in items
|
|
|
- if textItem.type == 'text' && textItem.content
|
|
|
+ if textItem.type == 'text' and textItem.content
|
|
|
content = textItem.content
|
|
|
|
|
|
# Обработка форматирования
|
|
|
@@ -579,7 +570,7 @@ module.exports =
|
|
|
.replace(/^-+/, '')
|
|
|
.replace(/-+$/, '')
|
|
|
|
|
|
- # Массовые действия (остаются без изменений)
|
|
|
+ # Массовые действия
|
|
|
toggleSelectAll: ->
|
|
|
if @selectAll
|
|
|
@selectedProducts = @filteredProducts.map (product) -> product._id
|
|
|
@@ -600,12 +591,11 @@ module.exports =
|
|
|
|
|
|
promises = @selectedProducts.map (productId) =>
|
|
|
product = @products.find (p) -> p._id == productId
|
|
|
- if product && !product.active
|
|
|
- updatedProduct = {
|
|
|
- ...product
|
|
|
+ if product and !product.active
|
|
|
+ updatedProduct = Object.assign {}, product,
|
|
|
active: true
|
|
|
updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
+
|
|
|
PouchDB.saveToRemote(updatedProduct)
|
|
|
|
|
|
Promise.all(promises)
|
|
|
@@ -624,12 +614,11 @@ module.exports =
|
|
|
|
|
|
promises = @selectedProducts.map (productId) =>
|
|
|
product = @products.find (p) -> p._id == productId
|
|
|
- if product && product.active
|
|
|
- updatedProduct = {
|
|
|
- ...product
|
|
|
+ if product and product.active
|
|
|
+ updatedProduct = Object.assign {}, product,
|
|
|
active: false
|
|
|
updatedAt: new Date().toISOString()
|
|
|
- }
|
|
|
+
|
|
|
PouchDB.saveToRemote(updatedProduct)
|
|
|
|
|
|
Promise.all(promises)
|
|
|
@@ -652,7 +641,7 @@ module.exports =
|
|
|
promises = @selectedProducts.map (productId) =>
|
|
|
PouchDB.getDocument(productId)
|
|
|
.then (doc) ->
|
|
|
- PouchDB.saveToRemote({ ...doc, _deleted: true })
|
|
|
+ PouchDB.saveToRemote(Object.assign {}, doc, { _deleted: true })
|
|
|
|
|
|
Promise.all(promises)
|
|
|
.then (results) =>
|
|
|
@@ -664,7 +653,10 @@ module.exports =
|
|
|
@showNotification 'Ошибка удаления товаров', 'error'
|
|
|
|
|
|
showNotification: (message, type = 'success') ->
|
|
|
- @$root.showNotification?(message, type) || debug.log("#{type}: #{message}")
|
|
|
+ if @$root.showNotification?
|
|
|
+ @$root.showNotification(message, type)
|
|
|
+ else
|
|
|
+ debug.log("#{type}: #{message}")
|
|
|
|
|
|
mounted: ->
|
|
|
@loadProducts()
|