| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146 |
- document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/pages/Admin/Products/index.styl']+'</style>')
- 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()
|