Gogs 4 semanas atrás
pai
commit
cd2e2f2a8a
2 arquivos alterados com 488 adições e 370 exclusões
  1. 17 7
      README.md
  2. 471 363
      app/pages/Admin/Products/index.coffee

+ 17 - 7
README.md

@@ -579,6 +579,7 @@ https://cdn1.ozone.ru/s3/multimedia-1-p/7663352533.jpg";;;ЭкоКрас;4673764
 
 
 ### 🚧 В процессе
+ 
   Анализировать реализованный код, по git репозитарию https://gogs.osvoj.ru/oleg/s5l.ru-crm.git
   Проверяй промт и изменения в нём по адресу https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md
   Важно Всегда приводи только полные листинги файлов
@@ -586,14 +587,23 @@ https://cdn1.ozone.ru/s3/multimedia-1-p/7663352533.jpg";;;ЭкоКрас;4673764
   в стил файлах не используй @import '../../index.styl', только стили текущего элемента,
   общие переменные определяй только в app/index.styl
   
-  Анализировать реализованный код, по git репозитарию https://gogs.osvoj.ru/oleg/s5l.ru-crm.git
-  Проверяй промт и изменения в нём по адресу https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md
-  Важно Всегда приводи только полные листинги файлов
-  в coffee файлах используй debug.log вместо console.log
-  в стил файлах не используй @import '../../index.styl', только стили текущего элемента,
-  общие переменные определяй только в app/index.styl
+  исправь ошибку в файле app/pages/Admin/Products/index.coffee:
   
-  Добавь в app/pages/Admin/Products доработай импорт товаров, так что бы вся информация из фала заносилась в базу данных в соответствии с промтом, загружай все изображения из файла, преобразуй Rich-контент JSON в markdown? при импорте по умолчанию проставляй домен в котором товар импартировался.
+  reason: Error: Parsing file /app/pages/Admin/Products/index.coffee: Unexpected token (505:45)
+    at Deps.parseDeps (/mnt/shared/oleg/works/siteScript/node_modules/module-deps/index.js:519:15)
+    at getDeps (/mnt/shared/oleg/works/siteScript/node_modules/module-deps/index.js:447:44)
+    at /mnt/shared/oleg/works/siteScript/node_modules/module-deps/index.js:430:38
+    at ConcatStream.<anonymous> (/mnt/shared/oleg/works/siteScript/node_modules/concat-stream/index.js:37:43)
+    at ConcatStream.emit (node:events:519:35)
+    at finishMaybe (/mnt/shared/oleg/works/siteScript/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_writable.js:630:14)
+    at endWritable (/mnt/shared/oleg/works/siteScript/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_writable.js:638:3)
+    at Writable.end (/mnt/shared/oleg/works/siteScript/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_writable.js:594:22)
+    at DuplexWrapper.onend (/mnt/shared/oleg/works/siteScript/node_modules/duplexer2/node_modules/readable-stream/lib/_stream_readable.js:577:10)
+    at Object.onceWrapper (node:events:621:28)
+    at DuplexWrapper.emit (node:events:519:35)
+    at endReadableNT (/mnt/shared/oleg/works/siteScript/node_modules/duplexer2/node_modules/readable-stream/lib/_stream_readable.js:1010:12)
+    at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
+
     
   
 -  app/pages/Admin/Products/index.coffee

+ 471 - 363
app/pages/Admin/Products/index.coffee

@@ -93,34 +93,13 @@ module.exports =
       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'
+    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 })
@@ -136,8 +115,471 @@ module.exports =
         .catch (error) =>
           debug.log 'Ошибка загрузки доменов:', error
     
+    getCategoryName: (categoryId) ->
+      category = @categories.find (cat) -> cat._id == categoryId
+      category?.name || 'Без категории'
+    
+    getCategoryProductCount: (categoryId) ->
+      @products.filter((product) -> product.category == categoryId).length
+    
+    # Улучшенный импорт товаров с полной обработкой CSV
+    onFileSelect: (event) ->
+      @selectedFile = event.target.files[0]
+      @importResults = null
+    
+    importProducts: ->
+      if !@selectedFile
+        @showNotification 'Выберите файл для импорта', 'error'
+        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'
+          }
+          
+          products = results.data.filter (row) => 
+            row && row['Артикул*'] && row['Название товара'] && row['Цена, руб.*']
+          
+          # Обрабатываем товары последовательно для загрузки изображений
+          @processProductsSequentially(products)
+            .then (couchProducts) =>
+              @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'
+        
+        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) ->
+      couchProducts = []
+      currentIndex = 0
+      
+      processNextProduct = =>
+        if currentIndex >= products.length
+          return Promise.resolve(couchProducts)
+        
+        product = products[currentIndex]
+        currentIndex++
+        
+        @transformProductData(product, currentIndex)
+          .then (productData) =>
+            couchProducts.push(productData)
+            return processNextProduct()
+          .catch (error) =>
+            debug.log "Ошибка обработки товара #{currentIndex}:", error
+            # Продолжаем обработку следующих товаров даже при ошибке
+            return processNextProduct()
+      
+      return processNextProduct()
+    
+    # Полное преобразование данных товара с загрузкой изображений
+    transformProductData: (product, index) ->
+      return new Promise (resolve, reject) =>
+        try
+          # Базовые поля
+          productData = {
+            _id: "product:#{Date.now()}-#{index}"
+            type: 'product'
+            name: product['Название товара']?.trim() || 'Без названия'
+            sku: product['Артикул*']?.trim() || "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)
+          
+          # Обработка категории
+          @processCategory(product, productData, index)
+            .then =>
+              # Обработка основного изображения
+              return @processMainImage(product, productData)
+            .then =>
+              # Обработка дополнительных изображений
+              return @processAdditionalImages(product, productData)
+            .then =>
+              # Обработка Rich-контента
+              @processRichContent(product, productData)
+              
+              resolve(productData)
+            .catch (error) =>
+              debug.log "Ошибка обработки товара:", error
+              # Возвращаем товар даже с ошибками обработки
+              resolve(productData)
+        
+        catch error
+          reject(error)
+    
+    # Парсинг цены
+    parsePrice: (priceString) ->
+      return 0 if !priceString
+      try
+        # Удаляем пробелы и заменяем запятые на точки
+        cleanPrice = priceString.toString().replace(/\s/g, '').replace(',', '.')
+        price = parseFloat(cleanPrice)
+        return isNaN(price) ? 0 : 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() || ''
+      
+      # Цены
+      if product['Цена до скидки, руб.']
+        productData.oldPrice = @parsePrice(product['Цена до скидки, руб.'])
+      
+      # Характеристики
+      productData.attributes = {}
+      
+      # Технические характеристики
+      if product['Расход, л/м2']
+        productData.attributes.consumption = product['Расход, л/м2']
+      if product['Время высыхания, часов']
+        productData.attributes.dryingTime = product['Время высыхания, часов']
+      if product['Вид краски']
+        productData.attributes.paintType = product['Вид краски']
+      if product['Основа краски']
+        productData.attributes.paintBase = product['Основа краски']
+      if product['Способ нанесения']
+        productData.attributes.applicationMethod = product['Способ нанесения']
+      if product['Область применения состава']
+        productData.attributes.applicationArea = product['Область применения состава']
+      
+      # Мета-данные
+      productData.tags = []
+      if product['#Хештеги']
+        tags = product['#Хештеги']?.split('#').filter((tag) -> tag.trim()).map((tag) -> tag.trim())
+        productData.tags = tags || []
+      
+      # Статусы и флаги
+      productData.features = {
+        installment: product['Рассрочка']?.toLowerCase() == 'да'
+        reviewPoints: product['Баллы за отзывы']?.toLowerCase() == 'да'
+        canBeTinted: product['Возможность колеровки']?.toLowerCase() == 'да'
+        aerosol: product['Аэрозоль']?.toLowerCase() == 'да'
+      }
+    
+    # Обработка категории с проверкой дубликатов
+    processCategory: (product, productData, index) ->
+      return Promise.resolve() if !product['Тип*']
+      
+      categoryName = product['Тип*'].trim()
+      return Promise.resolve() if !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
+        
+        return PouchDB.saveToRemote(newCategory)
+          .then (result) =>
+            debug.log "Создана новая категория: #{categoryName}"
+            @categories.push(newCategory)
+            return Promise.resolve()
+          .catch (error) ->
+            debug.log "Ошибка создания категории #{categoryName}:", error
+            # Продолжаем без категории
+            return Promise.resolve()
+    
+    # Обработка основного изображения
+    processMainImage: (product, productData) ->
+      return Promise.resolve() if !product['Ссылка на главное фото*']
+      
+      imageUrl = product['Ссылка на главное фото*'].trim()
+      return Promise.resolve() if !imageUrl
+      
+      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.main = attachmentInfo
+          return Promise.resolve()
+        .catch (error) =>
+          debug.log "Ошибка загрузки основного изображения:", error
+          # Продолжаем без изображения
+          return Promise.resolve()
     
-    # Mass actions methods
+    # Обработка дополнительных изображений
+    processAdditionalImages: (product, productData) ->
+      return Promise.resolve() if !product['Ссылки на дополнительные фото']
+      
+      additionalImages = product['Ссылки на дополнительные фото']
+      if typeof additionalImages == 'string'
+        # Разделяем ссылки по переносам строк
+        imageUrls = additionalImages.split('\n').filter((url) -> url.trim())
+      else
+        imageUrls = []
+      
+      return Promise.resolve() if imageUrls.length == 0
+      
+      # Обрабатываем первые 5 изображений чтобы не перегружать
+      imagePromises = []
+      productData.additionalImages = []
+      productData.attachments = productData.attachments || {}
+      
+      for imageUrl, i in imageUrls.slice(0, 5)
+        do (imageUrl, i) =>
+          promise = @downloadAndStoreImage(imageUrl.trim(), productData._id, "additional-#{i}.jpg")
+            .then (attachmentInfo) =>
+              imagePath = "/d/braer_color_shop/#{productData._id}/additional-#{i}.jpg"
+              productData.additionalImages.push(imagePath)
+              productData.attachments["additional-#{i}"] = attachmentInfo
+              return Promise.resolve()
+            .catch (error) =>
+              debug.log "Ошибка загрузки дополнительного изображения #{i}:", error
+              return Promise.resolve()
+          
+          imagePromises.push(promise)
+      
+      return Promise.all(imagePromises)
+    
+    # Загрузка и сохранение изображения как attachment
+    downloadAndStoreImage: (imageUrl, docId, filename) ->
+      return new Promise (resolve, reject) =>
+        try
+          # Создаем XMLHttpRequest для загрузки изображения
+          xhr = new XMLHttpRequest()
+          xhr.open('GET', imageUrl, true)
+          xhr.responseType = 'blob'
+          
+          xhr.onload = =>
+            if xhr.status == 200
+              blob = xhr.response
+              
+              # Читаем blob как ArrayBuffer
+              reader = new FileReader()
+              reader.onload = (e) =>
+                arrayBuffer = e.target.result
+                
+                # Сохраняем как attachment в PouchDB
+                PouchDB.putAttachment(docId, filename, arrayBuffer, blob.type)
+                  .then (result) =>
+                    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}`))
+          
+          xhr.onerror = =>
+            reject(new Error('Ошибка сети при загрузке изображения'))
+          
+          xhr.send()
+        
+        catch error
+          reject(error)
+    
+    # Обработка Rich-контента JSON и преобразование в Markdown
+    processRichContent: (product, productData) ->
+      # Сначала пробуем Rich-контент JSON
+      if product['Rich-контент JSON'] && product['Rich-контент JSON'].trim()
+        try
+          richContent = JSON.parse(product['Rich-контент JSON'])
+          productData.description = @richContentToMarkdown(richContent)
+          productData.originalRichContent = richContent # Сохраняем оригинал
+          return
+        catch error
+          debug.log "Ошибка парсинга Rich-контента:", error
+      
+      # Если Rich-контент невалиден или отсутствует, используем аннотацию
+      if product['Аннотация'] && product['Аннотация'].trim()
+        productData.description = product['Аннотация'].trim()
+      else
+        productData.description = ''
+    
+    # Преобразование Rich-контента JSON в Markdown
+    richContentToMarkdown: (richContent) ->
+      return '' if !richContent
+      
+      try
+        markdownParts = []
+        
+        if richContent.content && Array.isArray(richContent.content)
+          for item in richContent.content
+            markdownParts.push(@processContentItem(item))
+        
+        result = markdownParts.filter((part) -> part).join('\n\n')
+        return result || 'Описание товара'
+      catch error
+        debug.log "Ошибка преобразования Rich-контента в Markdown:", error
+        return JSON.stringify(richContent)
+    
+    # Обработка отдельного элемента контента
+    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 JSON.stringify(item)
+    
+    # Обработка текстового блока
+    processTextBlock: (item) ->
+      textParts = []
+      
+      # Заголовок
+      if item.title && item.title.items
+        titleText = @processTextItems(item.title.items)
+        if titleText
+          level = item.title.size || 'size3'
+          hashes = @getHeadingLevel(level)
+          textParts.push("#{hashes} #{titleText}")
+      
+      # Основной текст
+      if item.text && item.text.items
+        bodyText = @processTextItems(item.text.items)
+        if bodyText
+          textParts.push(bodyText)
+      
+      return textParts.join('\n\n')
+    
+    # Обработка заголовка
+    processHeader: (item) ->
+      if item.text && item.text.items
+        headerText = @processTextItems(item.text.items)
+        if headerText
+          level = item.size || 'size2'
+          hashes = @getHeadingLevel(level)
+          return "#{hashes} #{headerText}"
+      return ''
+    
+    # Обработка изображения
+    processImage: (item) ->
+      if item.url
+        altText = item.alt || 'Изображение товара'
+        return "![#{altText}](#{item.url})"
+      return ''
+    
+    # Обработка списка
+    processList: (item) ->
+      return '' if !item.items || !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 || !Array.isArray(items)
+      
+      textParts = []
+      for textItem in items
+        if textItem.type == 'text' && 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
@@ -221,340 +663,6 @@ module.exports =
           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}")