document.head.insertAdjacentHTML('beforeend','') PouchDB = require 'app/utils/pouch' Papa = require 'papaparse' module.exports = name: 'AdminProducts' render: (new Function '_ctx', '_cache', renderFns['app/pages/Admin/Products/index.pug'])() data: -> return { products: [] categories: [] searchQuery: '' selectedCategory: '' selectedStatus: '' showProductModal: false showImportModal: false showCategoriesModal: false showCategoryModal: false showMassActionsModal: false showCategoryAssignModal: false showMassCategoryAssign: false showMassPriceModal: false editingProduct: null editingCategory: null selectedFile: null selectedCategoriesFile: null importing: false importingCategories: false importResults: null categoriesImportResults: null availableDomains: [] categoriesActiveTab: 'list' 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 "![#{altText}](#{item.url})" 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()