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: -> products: [] categories: [] searchQuery: '' selectedCategory: '' selectedStatus: '' showProductModal: false showImportModal: false showCategoriesModal: false showCategoryModal: false showMassActionsModal: false showCategoryAssignModal: false showMassCategoryAssign: false showMassPriceModal: false editingProduct: null editingCategory: null selectedFile: null selectedCategoriesFile: null importing: false importingCategories: false importResults: null categoriesImportResults: null availableDomains: [] categoriesActiveTab: 'list' # Mass actions data selectedProducts: [] selectAll: false massCategory: '' massAllCategory: '' removeExistingCategories: false massRemoveAllCategories: false priceChangeType: 'fixed' priceChangeValue: null applyToOldPrice: false productForm: name: '' sku: '' category: '' price: 0 oldPrice: 0 brand: '' description: '' image: '' active: true domains: [] categoryForm: name: '' slug: '' description: '' parentCategory: '' sortOrder: 0 image: '' icon: '' active: true domains: [] computed: filteredProducts: -> 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 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 # Улучшенный импорт товаров с полной обработкой 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 and row['Артикул*'] and row['Название товара'] and 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() or 'Без названия' sku: product['Артикул*']?.trim() or "SKU-#{Date.now()}-#{index}" price: @parsePrice(product['Цена, руб.*']) active: true createdAt: new Date().toISOString() updatedAt: new Date().toISOString() domains: [window.location.hostname] # Текущий домен по умолчанию # Обработка всех полей CSV @processAllCSVFields(product, productData) # Обработка категории @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 if isNaN(price) then 0 else price catch return 0 # Обработка всех полей CSV processAllCSVFields: (product, productData) -> # Базовые поля productData.brand = product['Бренд*']?.trim() or '' productData.productType = product['Тип*']?.trim() or '' productData.weight = product['Вес товара, г']?.trim() or '' productData.volume = product['Объем, л']?.trim() or '' productData.country = product['Страна-изготовитель']?.trim() or '' productData.warranty = product['Гарантия']?.trim() or '' productData.color = product['Цвет товара']?.trim() or '' # Цены if product['Цена до скидки, руб.'] 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 or [] # Статусы и флаги 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 or {} productData.attachments.main = attachmentInfo return Promise.resolve() .catch (error) => debug.log "Ошибка загрузки основного изображения:", error # Продолжаем без изображения return Promise.resolve() # Обработка дополнительных изображений 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 or {} 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'] and 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['Аннотация'] and product['Аннотация'].trim() productData.description = product['Аннотация'].trim() else productData.description = '' # Преобразование 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 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 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) clearSelection: -> @selectedProducts = [] @selectAll = false 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()