document.head.insertAdjacentHTML('beforeend','') PouchDB = require 'app/utils/pouch' module.exports = name: 'AdminProducts' render: (new Function '_ctx', '_cache', renderFns['app/pages/Admin/Products/index.pug'])() data: -> return { products: [] categories: [] showProductModal: false showImportModal: false selectedFile: null importing: false importProgress: 0 importResults: null currentProduct: { _id: '' type: 'product' name: '' sku: '' price: 0 oldPrice: 0 category: '' description: '' active: true domains: [] attributes: {} images: [] createdAt: '' updatedAt: '' } searchQuery: '' selectedCategory: '' bulkActions: [] selectedProducts: [] } 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 return products availableDomains: -> @$root.currentDomainSettings?.domains or [@$root.currentDomain] mounted: -> @loadProducts() @loadCategories() methods: loadProducts: -> debug.log '📥 Загрузка товаров...' PouchDB.queryView('admin', 'products', { include_docs: true }) .then (result) => @products = result.rows.map (row) -> row.doc debug.log "✅ Загружено #{@products.length} товаров" .catch (error) => debug.log '❌ Ошибка загрузки товаров:', error @showNotification 'Ошибка загрузки товаров', 'error' loadCategories: -> debug.log '📥 Загрузка категорий...' PouchDB.queryView('admin', 'categories', { include_docs: true }) .then (result) => @categories = result.rows.map (row) -> row.doc debug.log "✅ Загружено #{@categories.length} категорий" .catch (error) => debug.log '❌ Ошибка загрузки категорий:', error createCategory: (categoryName) -> categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9а-яё]/g, '-').replace(/-+/g, '-') categoryData = { _id: "category:#{categorySlug}" type: 'category' name: categoryName slug: categorySlug active: true order: @categories.length domains: @availableDomains createdAt: new Date().toISOString() updatedAt: new Date().toISOString() } PouchDB.saveToRemote(categoryData) .then (result) => debug.log "✅ Создана категория: #{categoryName}" @loadCategories() return categorySlug .catch (error) => debug.log '❌ Ошибка создания категории:', error throw error transformProductData: (csvData, index) -> debug.log "🔄 Преобразование данных товара #{index + 1}: #{csvData['Артикул*']}" sku = csvData['Артикул*']?.toString().trim() return null unless sku # Определяем категорию categoryName = csvData['Тип*']?.trim() or 'Другое' debug.log "🔍 Поиск категории: #{categoryName}" # Ищем существующую категорию existingCategory = @categories.find (cat) -> cat.name == categoryName if existingCategory categorySlug = existingCategory.slug debug.log "✅ Использована существующая категория: #{categoryName}" else debug.log "🆕 Создание новой категории: #{categoryName}" categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9а-яё]/g, '-').replace(/-+/g, '-') # Категория будет создана позже в процессе импорта # Базовые данные товара productData = { _id: "product:#{sku}" type: 'product' name: csvData['Название товара']?.trim() or "Товар #{sku}" sku: sku price: parseFloat(csvData['Цена, руб.*']?.replace(/\s/g, '')?.replace(',', '.') or 0) oldPrice: parseFloat(csvData['Цена до скидки, руб.']?.replace(/\s/g, '')?.replace(',', '.') or 0) category: categorySlug brand: csvData['Бренд*']?.trim() description: csvData['Аннотация']?.trim() or '' active: true domains: @availableDomains attributes: {} images: [] createdAt: new Date().toISOString() updatedAt: new Date().toISOString() } # Дополнительные атрибуты additionalAttributes = {} for key, value of csvData if value and not key in ['Артикул*', 'Название товара', 'Цена, руб.*', 'Цена до скидки, руб.', 'Тип*', 'Бренд*', 'Аннотация', 'Rich-контент JSON', 'Ссылка на главное фото', 'Ссылки на дополнительные фото'] additionalAttributes[key] = value productData.attributes = additionalAttributes # Rich-контент if csvData['Rich-контент JSON'] try richContent = JSON.parse(csvData['Rich-контент JSON']) productData.richContent = richContent catch error debug.log '⚠️ Ошибка парсинга Rich-контента:', error debug.log "✅ Данные товара полностью преобразованы: #{sku}" return productData downloadAndStoreImage: (imageUrl, docId, filename) -> return new Promise (resolve, reject) => debug.log "🔄 Начало загрузки изображения: #{imageUrl}" # Проверяем валидность URL unless imageUrl and imageUrl.startsWith('http') debug.log '⚠️ Невалидный URL изображения:', imageUrl return resolve(null) # Создаем уникальное имя файла fileExtension = imageUrl.split('.').pop()?.split('?')[0] or 'jpg' uniqueFilename = "#{filename}.#{fileExtension}" debug.log "📁 Документ: #{docId}, Файл: #{uniqueFilename}" # Используем fetch вместо XMLHttpRequest для лучшей обработки ошибок fetch(imageUrl) .then (response) => unless response.ok throw new Error("HTTP #{response.status}: #{response.statusText}") return response.blob() .then (blob) => debug.log "✅ Blob получен, размер: #{blob.size} байт" if blob.size == 0 throw new Error('Пустой blob') # Читаем blob как ArrayBuffer reader = new FileReader() reader.onload = (event) => try arrayBuffer = event.target.result debug.log "✅ ArrayBuffer успешно прочитан, размер: #{arrayBuffer.byteLength} байт" # Сохраняем attachment в PouchDB PouchDB.localDb.putAttachment( docId, uniqueFilename, @currentProduct._rev, blob, blob.type ) .then (result) => debug.log "✅ Attachment сохранен: #{uniqueFilename}" resolve({ filename: uniqueFilename contentType: blob.type size: blob.size }) .catch (attachmentError) => debug.log "❌ Ошибка сохранения attachment:", attachmentError reject(attachmentError) catch readError debug.log "❌ Ошибка чтения blob:", readError reject(readError) reader.onerror = (error) => debug.log "❌ Ошибка FileReader:", error reject(error) reader.readAsArrayBuffer(blob) .catch (fetchError) => debug.log "❌ Ошибка загрузки изображения:", fetchError reject(fetchError) processProductImages: (productData, csvData) -> debug.log "🖼️ Начало обработки изображений для товара: #{productData.sku}" imagePromises = [] # Основное изображение mainImageUrl = csvData['Ссылка на главное фото']?.trim() if mainImageUrl imagePromises.push( @downloadAndStoreImage(mainImageUrl, productData._id, 'main') .then (imageInfo) => if imageInfo productData.mainImage = imageInfo.filename return imageInfo return null .catch (error) => debug.log "⚠️ Не удалось загрузить основное изображение:", error return null ) # Дополнительные изображения additionalImages = csvData['Ссылки на дополнительные фото'] if additionalImages # Разделяем строку по переносам и фильтруем пустые значения imageUrls = additionalImages.split('\n') .map((url) -> url.trim()) .filter((url) -> url and url.startsWith('http')) .slice(0, 5) # Ограничиваем 5 изображениями imageUrls.forEach (imageUrl, index) => imagePromises.push( @downloadAndStoreImage(imageUrl, productData._id, "additional-#{index + 1}") .then (imageInfo) => if imageInfo return imageInfo.filename return null .catch (error) => debug.log "⚠️ Не удалось загрузить дополнительное изображение:", error return null ) return Promise.allSettled(imagePromises) .then (results) => # Фильтруем успешно загруженные изображения successfulResults = results.filter (r) -> r.status == 'fulfilled' and r.value additionalFilenames = successfulResults.slice(1).map (r) -> r.value?.filename productData.additionalImages = additionalFilenames.filter (filename) -> filename debug.log "✅ Обработано изображений: #{successfulResults.length}" return productData .catch (error) => debug.log "❌ Ошибка обработки изображений:", error return productData saveProduct: (productData) -> debug.log "💾 Попытка сохранения товара: #{productData.sku}" return new Promise (resolve, reject) => # Сначала пытаемся получить существующий документ для получения _rev PouchDB.getDocument(productData._id) .then (existingDoc) => debug.log "🔄 Обновление существующего товара: #{productData.sku}" productData._rev = existingDoc._rev productData.updatedAt = new Date().toISOString() # Сохраняем в удаленную БД PouchDB.saveToRemote(productData) .then (result) => debug.log "✅ Товар сохранен, получение обновленной версии: #{productData.sku}" # Получаем обновленный документ PouchDB.getDocument(productData._id) .then (updatedDoc) => debug.log "✅ Документ получен с актуальным _rev: #{updatedDoc._rev?.substring(0, 10)}..." resolve(updatedDoc) .catch (getError) => debug.log "⚠️ Не удалось получить обновленный документ:", getError resolve(result) .catch (saveError) => debug.log "❌ Ошибка сохранения товара:", saveError reject(saveError) .catch (getError) => if getError.status == 404 debug.log "🆕 Создание нового товара: #{productData.sku}" productData.createdAt = new Date().toISOString() productData.updatedAt = productData.createdAt PouchDB.saveToRemote(productData) .then (result) => debug.log "✅ Товар сохранен в БД: #{productData.sku}" resolve(result) .catch (saveError) => debug.log "❌ Ошибка создания товара:", saveError reject(saveError) else debug.log "❌ Ошибка при получении документа:", getError reject(getError) readFile: (file) -> return new Promise (resolve, reject) => reader = new FileReader() reader.onload = (event) -> resolve(event.target.result) reader.onerror = (error) -> reject(error) reader.readAsText(file, 'UTF-8') importProducts: -> unless @selectedFile @showNotification 'Выберите файл для импорта', 'error' return @importing = true @importProgress = 0 @importResults = null debug.log '📦 Начало импорта товаров...' @readFile(@selectedFile) .then (text) => # Парсим CSV results = Papa.parse(text, { header: true delimiter: ';' skipEmptyLines: true encoding: 'UTF-8' }) # Фильтруем валидные строки validProducts = results.data.filter (row, index) => row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*'] debug.log "📊 Найдено валидных товаров: #{validProducts.length}" if validProducts.length == 0 throw new Error('Не найдено валидных товаров для импорта') # Создаем массив для обещаний importPromises = [] processedCount = 0 errors = [] # Обрабатываем каждый товар validProducts.forEach (product, index) => promise = => debug.log "🔧 Обработка товара #{index + 1}/#{validProducts.length}: #{product['Название товара']?.substring(0, 50)}..." try # Преобразуем данные CSV в объект товара productData = @transformProductData(product, index) return Promise.resolve(null) unless productData # Обрабатываем категорию categoryName = product['Тип*']?.trim() or 'Другое' existingCategory = @categories.find (cat) -> cat.name == categoryName if not existingCategory debug.log "🏷️ Создание категории: #{categoryName}" return @createCategory(categoryName) .then (categorySlug) => productData.category = categorySlug # Перезагружаем категории @loadCategories() return productData .catch (categoryError) => debug.log "⚠️ Не удалось создать категорию, используется 'Другое'" productData.category = 'drugoe' return productData else return Promise.resolve(productData) catch transformError debug.log "❌ Ошибка преобразования товара:", transformError errors.push("Товар #{index + 1}: #{transformError.message}") return Promise.resolve(null) .then (productData) => return null unless productData # Обрабатываем изображения return @processProductImages(productData, product) .then (productWithImages) => # Сохраняем товар return @saveProduct(productWithImages) .then (savedProduct) => processedCount++ @importProgress = Math.round((processedCount / validProducts.length) * 100) debug.log "✅ Обработан товар #{processedCount}/#{validProducts.length}: #{savedProduct.sku}" return savedProduct .catch (saveError) => errorMsg = "Товар #{index + 1} (#{productData.sku}): #{saveError.message}" debug.log "❌ Ошибка обработки товара #{index + 1}:", saveError errors.push(errorMsg) return null importPromises.push(promise()) # Ожидаем завершения всех операций return Promise.allSettled(importPromises) .then (results) => successfulImports = results.filter((r) -> r.status == 'fulfilled' and r.value).length failedImports = results.filter((r) -> r.status == 'rejected').length @importResults = { success: true processed: validProducts.length successful: successfulImports failed: failedImports errors: errors } debug.log "🎉 Импорт завершен: #{successfulImports} успешно, #{failedImports} с ошибками" if successfulImports > 0 @showNotification "Импортировано #{successfulImports} товаров" @loadProducts() # Перезагружаем список else @showNotification 'Не удалось импортировать ни одного товара', 'error' .catch (error) => debug.log '❌ Ошибка импорта:', error @importResults = { success: false error: error.message processed: 0 successful: 0 failed: 0 errors: [error.message] } @showNotification "Ошибка импорта: #{error.message}", 'error' .finally => @importing = false @selectedFile = null editProduct: (product) -> @currentProduct = Object.assign({}, product) @showProductModal = true deleteProduct: (product) -> if confirm("Удалить товар \"#{product.name}\"?") PouchDB.localDb.remove(product) .then => PouchDB.saveToRemote(product) # Удаляем из удаленной БД .then => @showNotification 'Товар удален' @loadProducts() .catch (error) => debug.log '❌ Ошибка удаления товара из удаленной БД:', error @showNotification 'Ошибка удаления товара', 'error' .catch (error) => debug.log '❌ Ошибка удаления товара:', error @showNotification 'Ошибка удаления товара', 'error' saveProductForm: -> unless @currentProduct.name and @currentProduct.sku and @currentProduct.price @showNotification 'Заполните обязательные поля', 'error' return productData = Object.assign({}, @currentProduct) if productData._id # Обновление существующего товара productData.updatedAt = new Date().toISOString() else # Создание нового товара productData._id = "product:#{productData.sku}" productData.type = 'product' productData.createdAt = new Date().toISOString() productData.updatedAt = productData.createdAt @saveProduct(productData) .then (result) => @showProductModal = false @resetCurrentProduct() @showNotification 'Товар сохранен' @loadProducts() .catch (error) => debug.log '❌ Ошибка сохранения товара:', error @showNotification 'Ошибка сохранения товара', 'error' resetCurrentProduct: -> @currentProduct = { _id: '' type: 'product' name: '' sku: '' price: 0 oldPrice: 0 category: '' description: '' active: true domains: @availableDomains attributes: {} images: [] createdAt: '' updatedAt: '' } showNotification: (message, type = 'success') -> @$root.showNotification(message, type) handleFileSelect: (event) -> @selectedFile = event.target.files[0] debug.log "📁 Выбран файл: #{@selectedFile?.name}" toggleAllProducts: (event) -> if event.target.checked @selectedProducts = @filteredProducts.map (product) -> product._id else @selectedProducts = []