index.coffee 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. document.head.insertAdjacentHTML('beforeend','<style type="text/css">'+stylFns['app/pages/Admin/Products/index.styl']+'</style>')
  2. PouchDB = require 'app/utils/pouch'
  3. module.exports =
  4. name: 'AdminProducts'
  5. render: (new Function '_ctx', '_cache', renderFns['app/pages/Admin/Products/index.pug'])()
  6. data: ->
  7. return {
  8. products: []
  9. categories: []
  10. showProductModal: false
  11. showImportModal: false
  12. selectedFile: null
  13. importing: false
  14. importProgress: 0
  15. importResults: null
  16. currentProduct: {
  17. _id: ''
  18. type: 'product'
  19. name: ''
  20. sku: ''
  21. price: 0
  22. oldPrice: 0
  23. category: ''
  24. description: ''
  25. active: true
  26. domains: []
  27. attributes: {}
  28. images: []
  29. createdAt: ''
  30. updatedAt: ''
  31. }
  32. searchQuery: ''
  33. selectedCategory: ''
  34. bulkActions: []
  35. selectedProducts: []
  36. }
  37. computed:
  38. filteredProducts: ->
  39. products = @products
  40. if @searchQuery
  41. query = @searchQuery.toLowerCase()
  42. products = products.filter (product) =>
  43. product.name?.toLowerCase().includes(query) or
  44. product.sku?.toLowerCase().includes(query)
  45. if @selectedCategory
  46. products = products.filter (product) =>
  47. product.category == @selectedCategory
  48. return products
  49. availableDomains: ->
  50. @$root.currentDomainSettings?.domains or [@$root.currentDomain]
  51. mounted: ->
  52. @loadProducts()
  53. @loadCategories()
  54. methods:
  55. loadProducts: ->
  56. debug.log '📥 Загрузка товаров...'
  57. PouchDB.queryView('admin', 'products', { include_docs: true })
  58. .then (result) =>
  59. @products = result.rows.map (row) -> row.doc
  60. debug.log "✅ Загружено #{@products.length} товаров"
  61. .catch (error) =>
  62. debug.log '❌ Ошибка загрузки товаров:', error
  63. @showNotification 'Ошибка загрузки товаров', 'error'
  64. loadCategories: ->
  65. debug.log '📥 Загрузка категорий...'
  66. PouchDB.queryView('admin', 'categories', { include_docs: true })
  67. .then (result) =>
  68. @categories = result.rows.map (row) -> row.doc
  69. debug.log "✅ Загружено #{@categories.length} категорий"
  70. .catch (error) =>
  71. debug.log '❌ Ошибка загрузки категорий:', error
  72. createCategory: (categoryName) ->
  73. categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9а-яё]/g, '-').replace(/-+/g, '-')
  74. categoryData = {
  75. _id: "category:#{categorySlug}"
  76. type: 'category'
  77. name: categoryName
  78. slug: categorySlug
  79. active: true
  80. order: @categories.length
  81. domains: @availableDomains
  82. createdAt: new Date().toISOString()
  83. updatedAt: new Date().toISOString()
  84. }
  85. PouchDB.saveToRemote(categoryData)
  86. .then (result) =>
  87. debug.log "✅ Создана категория: #{categoryName}"
  88. @loadCategories()
  89. return categorySlug
  90. .catch (error) =>
  91. debug.log '❌ Ошибка создания категории:', error
  92. throw error
  93. transformProductData: (csvData, index) ->
  94. debug.log "🔄 Преобразование данных товара #{index + 1}: #{csvData['Артикул*']}"
  95. sku = csvData['Артикул*']?.toString().trim()
  96. return null unless sku
  97. # Определяем категорию
  98. categoryName = csvData['Тип*']?.trim() or 'Другое'
  99. debug.log "🔍 Поиск категории: #{categoryName}"
  100. # Ищем существующую категорию
  101. existingCategory = @categories.find (cat) -> cat.name == categoryName
  102. if existingCategory
  103. categorySlug = existingCategory.slug
  104. debug.log "✅ Использована существующая категория: #{categoryName}"
  105. else
  106. debug.log "🆕 Создание новой категории: #{categoryName}"
  107. categorySlug = categoryName.toLowerCase().replace(/[^a-z0-9а-яё]/g, '-').replace(/-+/g, '-')
  108. # Категория будет создана позже в процессе импорта
  109. # Базовые данные товара
  110. productData = {
  111. _id: "product:#{sku}"
  112. type: 'product'
  113. name: csvData['Название товара']?.trim() or "Товар #{sku}"
  114. sku: sku
  115. price: parseFloat(csvData['Цена, руб.*']?.replace(/\s/g, '')?.replace(',', '.') or 0)
  116. oldPrice: parseFloat(csvData['Цена до скидки, руб.']?.replace(/\s/g, '')?.replace(',', '.') or 0)
  117. category: categorySlug
  118. brand: csvData['Бренд*']?.trim()
  119. description: csvData['Аннотация']?.trim() or ''
  120. active: true
  121. domains: @availableDomains
  122. attributes: {}
  123. images: []
  124. createdAt: new Date().toISOString()
  125. updatedAt: new Date().toISOString()
  126. }
  127. # Дополнительные атрибуты
  128. additionalAttributes = {}
  129. for key, value of csvData
  130. if value and not key in ['Артикул*', 'Название товара', 'Цена, руб.*', 'Цена до скидки, руб.', 'Тип*', 'Бренд*', 'Аннотация', 'Rich-контент JSON', 'Ссылка на главное фото', 'Ссылки на дополнительные фото']
  131. additionalAttributes[key] = value
  132. productData.attributes = additionalAttributes
  133. # Rich-контент
  134. if csvData['Rich-контент JSON']
  135. try
  136. richContent = JSON.parse(csvData['Rich-контент JSON'])
  137. productData.richContent = richContent
  138. catch error
  139. debug.log '⚠️ Ошибка парсинга Rich-контента:', error
  140. debug.log "✅ Данные товара полностью преобразованы: #{sku}"
  141. return productData
  142. downloadAndStoreImage: (imageUrl, docId, filename) ->
  143. return new Promise (resolve, reject) =>
  144. debug.log "🔄 Начало загрузки изображения: #{imageUrl}"
  145. # Проверяем валидность URL
  146. unless imageUrl and imageUrl.startsWith('http')
  147. debug.log '⚠️ Невалидный URL изображения:', imageUrl
  148. return resolve(null)
  149. # Создаем уникальное имя файла
  150. fileExtension = imageUrl.split('.').pop()?.split('?')[0] or 'jpg'
  151. uniqueFilename = "#{filename}.#{fileExtension}"
  152. debug.log "📁 Документ: #{docId}, Файл: #{uniqueFilename}"
  153. # Используем fetch вместо XMLHttpRequest для лучшей обработки ошибок
  154. fetch(imageUrl)
  155. .then (response) =>
  156. unless response.ok
  157. throw new Error("HTTP #{response.status}: #{response.statusText}")
  158. return response.blob()
  159. .then (blob) =>
  160. debug.log "✅ Blob получен, размер: #{blob.size} байт"
  161. if blob.size == 0
  162. throw new Error('Пустой blob')
  163. # Читаем blob как ArrayBuffer
  164. reader = new FileReader()
  165. reader.onload = (event) =>
  166. try
  167. arrayBuffer = event.target.result
  168. debug.log "✅ ArrayBuffer успешно прочитан, размер: #{arrayBuffer.byteLength} байт"
  169. # Сохраняем attachment в PouchDB
  170. PouchDB.localDb.putAttachment(
  171. docId,
  172. uniqueFilename,
  173. @currentProduct._rev,
  174. blob,
  175. blob.type
  176. )
  177. .then (result) =>
  178. debug.log "✅ Attachment сохранен: #{uniqueFilename}"
  179. resolve({
  180. filename: uniqueFilename
  181. contentType: blob.type
  182. size: blob.size
  183. })
  184. .catch (attachmentError) =>
  185. debug.log "❌ Ошибка сохранения attachment:", attachmentError
  186. reject(attachmentError)
  187. catch readError
  188. debug.log "❌ Ошибка чтения blob:", readError
  189. reject(readError)
  190. reader.onerror = (error) =>
  191. debug.log "❌ Ошибка FileReader:", error
  192. reject(error)
  193. reader.readAsArrayBuffer(blob)
  194. .catch (fetchError) =>
  195. debug.log "❌ Ошибка загрузки изображения:", fetchError
  196. reject(fetchError)
  197. processProductImages: (productData, csvData) ->
  198. debug.log "🖼️ Начало обработки изображений для товара: #{productData.sku}"
  199. imagePromises = []
  200. # Основное изображение
  201. mainImageUrl = csvData['Ссылка на главное фото']?.trim()
  202. if mainImageUrl
  203. imagePromises.push(
  204. @downloadAndStoreImage(mainImageUrl, productData._id, 'main')
  205. .then (imageInfo) =>
  206. if imageInfo
  207. productData.mainImage = imageInfo.filename
  208. return imageInfo
  209. return null
  210. .catch (error) =>
  211. debug.log "⚠️ Не удалось загрузить основное изображение:", error
  212. return null
  213. )
  214. # Дополнительные изображения
  215. additionalImages = csvData['Ссылки на дополнительные фото']
  216. if additionalImages
  217. # Разделяем строку по переносам и фильтруем пустые значения
  218. imageUrls = additionalImages.split('\n')
  219. .map((url) -> url.trim())
  220. .filter((url) -> url and url.startsWith('http'))
  221. .slice(0, 5) # Ограничиваем 5 изображениями
  222. imageUrls.forEach (imageUrl, index) =>
  223. imagePromises.push(
  224. @downloadAndStoreImage(imageUrl, productData._id, "additional-#{index + 1}")
  225. .then (imageInfo) =>
  226. if imageInfo
  227. return imageInfo.filename
  228. return null
  229. .catch (error) =>
  230. debug.log "⚠️ Не удалось загрузить дополнительное изображение:", error
  231. return null
  232. )
  233. return Promise.allSettled(imagePromises)
  234. .then (results) =>
  235. # Фильтруем успешно загруженные изображения
  236. successfulResults = results.filter (r) -> r.status == 'fulfilled' and r.value
  237. additionalFilenames = successfulResults.slice(1).map (r) -> r.value?.filename
  238. productData.additionalImages = additionalFilenames.filter (filename) -> filename
  239. debug.log "✅ Обработано изображений: #{successfulResults.length}"
  240. return productData
  241. .catch (error) =>
  242. debug.log "❌ Ошибка обработки изображений:", error
  243. return productData
  244. saveProduct: (productData) ->
  245. debug.log "💾 Попытка сохранения товара: #{productData.sku}"
  246. return new Promise (resolve, reject) =>
  247. # Сначала пытаемся получить существующий документ для получения _rev
  248. PouchDB.getDocument(productData._id)
  249. .then (existingDoc) =>
  250. debug.log "🔄 Обновление существующего товара: #{productData.sku}"
  251. productData._rev = existingDoc._rev
  252. productData.updatedAt = new Date().toISOString()
  253. # Сохраняем в удаленную БД
  254. PouchDB.saveToRemote(productData)
  255. .then (result) =>
  256. debug.log "✅ Товар сохранен, получение обновленной версии: #{productData.sku}"
  257. # Получаем обновленный документ
  258. PouchDB.getDocument(productData._id)
  259. .then (updatedDoc) =>
  260. debug.log "✅ Документ получен с актуальным _rev: #{updatedDoc._rev?.substring(0, 10)}..."
  261. resolve(updatedDoc)
  262. .catch (getError) =>
  263. debug.log "⚠️ Не удалось получить обновленный документ:", getError
  264. resolve(result)
  265. .catch (saveError) =>
  266. debug.log "❌ Ошибка сохранения товара:", saveError
  267. reject(saveError)
  268. .catch (getError) =>
  269. if getError.status == 404
  270. debug.log "🆕 Создание нового товара: #{productData.sku}"
  271. productData.createdAt = new Date().toISOString()
  272. productData.updatedAt = productData.createdAt
  273. PouchDB.saveToRemote(productData)
  274. .then (result) =>
  275. debug.log "✅ Товар сохранен в БД: #{productData.sku}"
  276. resolve(result)
  277. .catch (saveError) =>
  278. debug.log "❌ Ошибка создания товара:", saveError
  279. reject(saveError)
  280. else
  281. debug.log "❌ Ошибка при получении документа:", getError
  282. reject(getError)
  283. readFile: (file) ->
  284. return new Promise (resolve, reject) =>
  285. reader = new FileReader()
  286. reader.onload = (event) -> resolve(event.target.result)
  287. reader.onerror = (error) -> reject(error)
  288. reader.readAsText(file, 'UTF-8')
  289. importProducts: ->
  290. unless @selectedFile
  291. @showNotification 'Выберите файл для импорта', 'error'
  292. return
  293. @importing = true
  294. @importProgress = 0
  295. @importResults = null
  296. debug.log '📦 Начало импорта товаров...'
  297. @readFile(@selectedFile)
  298. .then (text) =>
  299. # Парсим CSV
  300. results = Papa.parse(text, {
  301. header: true
  302. delimiter: ';'
  303. skipEmptyLines: true
  304. encoding: 'UTF-8'
  305. })
  306. # Фильтруем валидные строки
  307. validProducts = results.data.filter (row, index) =>
  308. row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*']
  309. debug.log "📊 Найдено валидных товаров: #{validProducts.length}"
  310. if validProducts.length == 0
  311. throw new Error('Не найдено валидных товаров для импорта')
  312. # Создаем массив для обещаний
  313. importPromises = []
  314. processedCount = 0
  315. errors = []
  316. # Обрабатываем каждый товар
  317. validProducts.forEach (product, index) =>
  318. promise = =>
  319. debug.log "🔧 Обработка товара #{index + 1}/#{validProducts.length}: #{product['Название товара']?.substring(0, 50)}..."
  320. try
  321. # Преобразуем данные CSV в объект товара
  322. productData = @transformProductData(product, index)
  323. return Promise.resolve(null) unless productData
  324. # Обрабатываем категорию
  325. categoryName = product['Тип*']?.trim() or 'Другое'
  326. existingCategory = @categories.find (cat) -> cat.name == categoryName
  327. if not existingCategory
  328. debug.log "🏷️ Создание категории: #{categoryName}"
  329. return @createCategory(categoryName)
  330. .then (categorySlug) =>
  331. productData.category = categorySlug
  332. # Перезагружаем категории
  333. @loadCategories()
  334. return productData
  335. .catch (categoryError) =>
  336. debug.log "⚠️ Не удалось создать категорию, используется 'Другое'"
  337. productData.category = 'drugoe'
  338. return productData
  339. else
  340. return Promise.resolve(productData)
  341. catch transformError
  342. debug.log "❌ Ошибка преобразования товара:", transformError
  343. errors.push("Товар #{index + 1}: #{transformError.message}")
  344. return Promise.resolve(null)
  345. .then (productData) =>
  346. return null unless productData
  347. # Обрабатываем изображения
  348. return @processProductImages(productData, product)
  349. .then (productWithImages) =>
  350. # Сохраняем товар
  351. return @saveProduct(productWithImages)
  352. .then (savedProduct) =>
  353. processedCount++
  354. @importProgress = Math.round((processedCount / validProducts.length) * 100)
  355. debug.log "✅ Обработан товар #{processedCount}/#{validProducts.length}: #{savedProduct.sku}"
  356. return savedProduct
  357. .catch (saveError) =>
  358. errorMsg = "Товар #{index + 1} (#{productData.sku}): #{saveError.message}"
  359. debug.log "❌ Ошибка обработки товара #{index + 1}:", saveError
  360. errors.push(errorMsg)
  361. return null
  362. importPromises.push(promise())
  363. # Ожидаем завершения всех операций
  364. return Promise.allSettled(importPromises)
  365. .then (results) =>
  366. successfulImports = results.filter((r) -> r.status == 'fulfilled' and r.value).length
  367. failedImports = results.filter((r) -> r.status == 'rejected').length
  368. @importResults = {
  369. success: true
  370. processed: validProducts.length
  371. successful: successfulImports
  372. failed: failedImports
  373. errors: errors
  374. }
  375. debug.log "🎉 Импорт завершен: #{successfulImports} успешно, #{failedImports} с ошибками"
  376. if successfulImports > 0
  377. @showNotification "Импортировано #{successfulImports} товаров"
  378. @loadProducts() # Перезагружаем список
  379. else
  380. @showNotification 'Не удалось импортировать ни одного товара', 'error'
  381. .catch (error) =>
  382. debug.log '❌ Ошибка импорта:', error
  383. @importResults = {
  384. success: false
  385. error: error.message
  386. processed: 0
  387. successful: 0
  388. failed: 0
  389. errors: [error.message]
  390. }
  391. @showNotification "Ошибка импорта: #{error.message}", 'error'
  392. .finally =>
  393. @importing = false
  394. @selectedFile = null
  395. editProduct: (product) ->
  396. @currentProduct = Object.assign({}, product)
  397. @showProductModal = true
  398. deleteProduct: (product) ->
  399. if confirm("Удалить товар \"#{product.name}\"?")
  400. PouchDB.localDb.remove(product)
  401. .then =>
  402. PouchDB.saveToRemote(product) # Удаляем из удаленной БД
  403. .then =>
  404. @showNotification 'Товар удален'
  405. @loadProducts()
  406. .catch (error) =>
  407. debug.log '❌ Ошибка удаления товара из удаленной БД:', error
  408. @showNotification 'Ошибка удаления товара', 'error'
  409. .catch (error) =>
  410. debug.log '❌ Ошибка удаления товара:', error
  411. @showNotification 'Ошибка удаления товара', 'error'
  412. saveProductForm: ->
  413. unless @currentProduct.name and @currentProduct.sku and @currentProduct.price
  414. @showNotification 'Заполните обязательные поля', 'error'
  415. return
  416. productData = Object.assign({}, @currentProduct)
  417. if productData._id
  418. # Обновление существующего товара
  419. productData.updatedAt = new Date().toISOString()
  420. else
  421. # Создание нового товара
  422. productData._id = "product:#{productData.sku}"
  423. productData.type = 'product'
  424. productData.createdAt = new Date().toISOString()
  425. productData.updatedAt = productData.createdAt
  426. @saveProduct(productData)
  427. .then (result) =>
  428. @showProductModal = false
  429. @resetCurrentProduct()
  430. @showNotification 'Товар сохранен'
  431. @loadProducts()
  432. .catch (error) =>
  433. debug.log '❌ Ошибка сохранения товара:', error
  434. @showNotification 'Ошибка сохранения товара', 'error'
  435. resetCurrentProduct: ->
  436. @currentProduct = {
  437. _id: ''
  438. type: 'product'
  439. name: ''
  440. sku: ''
  441. price: 0
  442. oldPrice: 0
  443. category: ''
  444. description: ''
  445. active: true
  446. domains: @availableDomains
  447. attributes: {}
  448. images: []
  449. createdAt: ''
  450. updatedAt: ''
  451. }
  452. showNotification: (message, type = 'success') ->
  453. @$root.showNotification(message, type)
  454. handleFileSelect: (event) ->
  455. @selectedFile = event.target.files[0]
  456. debug.log "📁 Выбран файл: #{@selectedFile?.name}"
  457. toggleAllProducts: (event) ->
  458. if event.target.checked
  459. @selectedProducts = @filteredProducts.map (product) -> product._id
  460. else
  461. @selectedProducts = []