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