index.coffee 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146
  1. document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/pages/Admin/Products/index.styl']+'</style>')
  2. PouchDB = require 'app/utils/pouch'
  3. Papa = require 'papaparse'
  4. module.exports =
  5. name: 'AdminProducts'
  6. render: (new Function '_ctx', '_cache', renderFns['app/pages/Admin/Products/index.pug'])()
  7. data: ->
  8. return {
  9. products: []
  10. categories: []
  11. searchQuery: ''
  12. selectedCategory: ''
  13. selectedStatus: ''
  14. showProductModal: false
  15. showImportModal: false
  16. showCategoriesModal: false
  17. showCategoryModal: false
  18. showMassActionsModal: false
  19. showCategoryAssignModal: false
  20. showMassCategoryAssign: false
  21. showMassPriceModal: false
  22. editingProduct: null
  23. editingCategory: null
  24. selectedFile: null
  25. selectedCategoriesFile: null
  26. importing: false
  27. importingCategories: false
  28. importResults: null
  29. categoriesImportResults: null
  30. availableDomains: []
  31. categoriesActiveTab: 'list'
  32. importProgress: 0
  33. processedCount: 0
  34. totalCount: 0
  35. uploadingImages: false
  36. newImageUrl: ''
  37. newAdditionalImageUrl: ''
  38. newAttributeKey: ''
  39. newAttributeValue: ''
  40. newTag: ''
  41. # Mass actions data
  42. selectedProducts: []
  43. selectAll: false
  44. massCategory: ''
  45. massAllCategory: ''
  46. removeExistingCategories: false
  47. massRemoveAllCategories: false
  48. priceChangeType: 'fixed'
  49. priceChangeValue: null
  50. applyToOldPrice: false
  51. productForm:
  52. _id: ''
  53. name: ''
  54. sku: ''
  55. category: ''
  56. price: 0
  57. oldPrice: 0
  58. brand: ''
  59. description: ''
  60. image: ''
  61. additionalImages: []
  62. active: true
  63. domains: []
  64. attributes: {}
  65. tags: []
  66. categoryForm:
  67. _id: ''
  68. name: ''
  69. slug: ''
  70. description: ''
  71. parentCategory: ''
  72. sortOrder: 0
  73. image: ''
  74. icon: ''
  75. active: true
  76. domains: []
  77. }
  78. computed:
  79. filteredProducts: ->
  80. products = @products
  81. if @searchQuery
  82. query = @searchQuery.toLowerCase()
  83. products = products.filter (product) =>
  84. product.name?.toLowerCase().includes(query) or
  85. product.sku?.toLowerCase().includes(query)
  86. if @selectedCategory
  87. products = products.filter (product) =>
  88. product.category == @selectedCategory
  89. if @selectedStatus == 'active'
  90. products = products.filter (product) => product.active
  91. else if @selectedStatus == 'inactive'
  92. products = products.filter (product) => !product.active
  93. return products
  94. isEditing: ->
  95. @editingProduct != null
  96. attributesList: ->
  97. list = []
  98. for key, value of @productForm.attributes
  99. list.push
  100. key: key
  101. value: value
  102. return list
  103. methods:
  104. loadProducts: ->
  105. PouchDB.queryView('admin', 'products', { include_docs: true })
  106. .then (result) =>
  107. @products = result.rows.map (row) -> row.doc
  108. .catch (error) =>
  109. debug.log 'Ошибка загрузки товаров:', error
  110. @showNotification 'Ошибка загрузки товаров', 'error'
  111. loadCategories: ->
  112. PouchDB.queryView('admin', 'categories', { include_docs: true })
  113. .then (result) =>
  114. @categories = result.rows.map (row) -> row.doc
  115. .catch (error) =>
  116. debug.log 'Ошибка загрузки категорий:', error
  117. loadDomains: ->
  118. PouchDB.queryView('admin', 'domain_settings', { include_docs: true })
  119. .then (result) =>
  120. @availableDomains = result.rows.map (row) -> row.doc
  121. .catch (error) =>
  122. debug.log 'Ошибка загрузки доменов:', error
  123. getCategoryName: (categoryId) ->
  124. category = @categories.find (cat) -> cat._id == categoryId
  125. category?.name or 'Без категории'
  126. getCategoryProductCount: (categoryId) ->
  127. @products.filter((product) -> product.category == categoryId).length
  128. # Управление категориями
  129. showCategoriesManager: ->
  130. @showCategoriesModal = true
  131. createCategory: ->
  132. @editingCategory = null
  133. @resetCategoryForm()
  134. @showCategoryModal = true
  135. editCategory: (category) ->
  136. @editingCategory = category
  137. @categoryForm = Object.assign {},
  138. _id: category._id
  139. name: category.name or ''
  140. slug: category.slug or ''
  141. description: category.description or ''
  142. parentCategory: category.parentCategory or ''
  143. sortOrder: category.sortOrder or 0
  144. image: category.image or ''
  145. icon: category.icon or ''
  146. active: category.active != false
  147. domains: category.domains or [window.location.hostname]
  148. @showCategoryModal = true
  149. saveCategory: ->
  150. if !@categoryForm.name
  151. @showNotification 'Введите название категории', 'error'
  152. return
  153. categoryData = Object.assign {}, @categoryForm
  154. delete categoryData._id
  155. if !categoryData.slug
  156. categoryData.slug = @generateSlug(categoryData.name)
  157. if @editingCategory
  158. # Обновление существующей категории
  159. categoryData._id = @editingCategory._id
  160. categoryData._rev = @editingCategory._rev
  161. categoryData.updatedAt = new Date().toISOString()
  162. else
  163. # Создание новой категории
  164. categoryData._id = "category:#{Date.now()}"
  165. categoryData.type = 'category'
  166. categoryData.createdAt = new Date().toISOString()
  167. categoryData.updatedAt = categoryData.createdAt
  168. PouchDB.saveToRemote(categoryData)
  169. .then (result) =>
  170. @showCategoryModal = false
  171. @resetCategoryForm()
  172. @loadCategories()
  173. @showNotification "Категория #{if @editingCategory then 'обновлена' else 'создана'}"
  174. .catch (error) =>
  175. debug.log 'Ошибка сохранения категории:', error
  176. @showNotification 'Ошибка сохранения категории', 'error'
  177. deleteCategory: (category) ->
  178. if !confirm("Удалить категорию \"#{category.name}\"? Товары в этой категории не будут удалены.")
  179. return
  180. PouchDB.getDocument(category._id)
  181. .then (doc) ->
  182. PouchDB.saveToRemote(Object.assign {}, doc, { _deleted: true })
  183. .then (result) =>
  184. @loadCategories()
  185. @showNotification 'Категория удалена'
  186. .catch (error) =>
  187. debug.log 'Ошибка удаления категории:', error
  188. @showNotification 'Ошибка удаления категории', 'error'
  189. resetCategoryForm: ->
  190. @categoryForm =
  191. _id: ''
  192. name: ''
  193. slug: ''
  194. description: ''
  195. parentCategory: ''
  196. sortOrder: 0
  197. image: ''
  198. icon: ''
  199. active: true
  200. domains: [window.location.hostname]
  201. # Редактирование и создание товаров
  202. createProduct: ->
  203. @editingProduct = null
  204. @resetProductForm()
  205. @showProductModal = true
  206. editProduct: (product) ->
  207. @editingProduct = product
  208. @productForm = Object.assign {},
  209. _id: product._id
  210. name: product.name or ''
  211. sku: product.sku or ''
  212. category: product.category or ''
  213. price: product.price or 0
  214. oldPrice: product.oldPrice or 0
  215. brand: product.brand or ''
  216. description: product.description or ''
  217. image: product.image or ''
  218. additionalImages: product.additionalImages or []
  219. active: product.active != false
  220. domains: product.domains or [window.location.hostname]
  221. attributes: product.attributes or {}
  222. tags: product.tags or []
  223. @showProductModal = true
  224. saveProduct: ->
  225. if !@productForm.name or !@productForm.sku or !@productForm.price
  226. @showNotification 'Заполните обязательные поля: название, артикул и цена', 'error'
  227. return
  228. productData = Object.assign {}, @productForm
  229. delete productData._id
  230. if @isEditing
  231. # Обновление существующего товара
  232. productData._id = @editingProduct._id
  233. productData._rev = @editingProduct._rev
  234. productData.updatedAt = new Date().toISOString()
  235. else
  236. # Создание нового товара
  237. productData._id = "product:#{@productForm.sku}"
  238. productData.type = 'product'
  239. productData.createdAt = new Date().toISOString()
  240. productData.updatedAt = productData.createdAt
  241. PouchDB.saveToRemote(productData)
  242. .then (result) =>
  243. @showProductModal = false
  244. @resetProductForm()
  245. @loadProducts()
  246. @showNotification "Товар #{if @isEditing then 'обновлен' else 'создан'}"
  247. .catch (error) =>
  248. debug.log 'Ошибка сохранения товара:', error
  249. @showNotification 'Ошибка сохранения товара', 'error'
  250. resetProductForm: ->
  251. @productForm =
  252. _id: ''
  253. name: ''
  254. sku: ''
  255. category: ''
  256. price: 0
  257. oldPrice: 0
  258. brand: ''
  259. description: ''
  260. image: ''
  261. additionalImages: []
  262. active: true
  263. domains: [window.location.hostname]
  264. attributes: {}
  265. tags: []
  266. # Управление атрибутами товара
  267. addAttribute: ->
  268. if !@newAttributeKey
  269. @showNotification 'Введите название атрибута', 'error'
  270. return
  271. if @productForm.attributes[@newAttributeKey]
  272. @showNotification 'Атрибут с таким названием уже существует', 'error'
  273. return
  274. @productForm.attributes[@newAttributeKey] = @newAttributeValue or ''
  275. @newAttributeKey = ''
  276. @newAttributeValue = ''
  277. @showNotification 'Атрибут добавлен'
  278. removeAttribute: (key) ->
  279. delete @productForm.attributes[key]
  280. @showNotification 'Атрибут удален'
  281. updateAttribute: (key, value) ->
  282. @productForm.attributes[key] = value
  283. # Управление тегами
  284. addTag: ->
  285. if !@newTag
  286. @showNotification 'Введите тег', 'error'
  287. return
  288. if @productForm.tags.includes(@newTag)
  289. @showNotification 'Такой тег уже существует', 'error'
  290. return
  291. @productForm.tags.push(@newTag)
  292. @newTag = ''
  293. @showNotification 'Тег добавлен'
  294. removeTag: (index) ->
  295. @productForm.tags.splice(index, 1)
  296. @showNotification 'Тег удален'
  297. # Загрузка изображений для редактирования
  298. uploadMainImage: ->
  299. if !@newImageUrl
  300. @showNotification 'Введите URL изображения', 'error'
  301. return
  302. @uploadingImages = true
  303. productId = if @isEditing then @editingProduct._id else "product:#{@productForm.sku}"
  304. @downloadAndStoreImage(@newImageUrl, productId, 'main.jpg')
  305. .then (attachmentInfo) =>
  306. @productForm.image = "/d/braer_color_shop/#{productId}/main.jpg"
  307. @newImageUrl = ''
  308. @showNotification 'Основное изображение загружено'
  309. .catch (error) =>
  310. debug.log 'Ошибка загрузки основного изображения:', error
  311. @showNotification 'Ошибка загрузки изображения', 'error'
  312. .finally =>
  313. @uploadingImages = false
  314. uploadAdditionalImage: ->
  315. if !@newAdditionalImageUrl
  316. @showNotification 'Введите URL изображения', 'error'
  317. return
  318. @uploadingImages = true
  319. productId = if @isEditing then @editingProduct._id else "product:#{@productForm.sku}"
  320. index = @productForm.additionalImages.length
  321. @downloadAndStoreImage(@newAdditionalImageUrl, productId, "additional-#{index}.jpg")
  322. .then (attachmentInfo) =>
  323. imagePath = "/d/braer_color_shop/#{productId}/additional-#{index}.jpg"
  324. @productForm.additionalImages.push(imagePath)
  325. @newAdditionalImageUrl = ''
  326. @showNotification 'Дополнительное изображение загружено'
  327. .catch (error) =>
  328. debug.log 'Ошибка загрузки дополнительного изображения:', error
  329. @showNotification 'Ошибка загрузки изображения', 'error'
  330. .finally =>
  331. @uploadingImages = false
  332. removeAdditionalImage: (index) ->
  333. @productForm.additionalImages.splice(index, 1)
  334. @showNotification 'Изображение удалено'
  335. # ИСПРАВЛЕННЫЙ метод загрузки и сохранения изображения как attachment
  336. downloadAndStoreImage: (imageUrl, docId, filename) ->
  337. return new Promise (resolve, reject) =>
  338. debug.log "🔄 Начало загрузки изображения: #{imageUrl}"
  339. debug.log "📁 Документ: #{docId}, Файл: #{filename}"
  340. try
  341. # Создаем XMLHttpRequest для загрузки изображения
  342. xhr = new XMLHttpRequest()
  343. xhr.open('GET', imageUrl, true)
  344. xhr.responseType = 'blob'
  345. xhr.onload = =>
  346. debug.log "📡 Статус ответа XHR: #{xhr.status}"
  347. if xhr.status == 200
  348. blob = xhr.response
  349. debug.log "✅ Blob получен:",
  350. type: blob.type
  351. size: blob.size
  352. isBlob: blob instanceof Blob
  353. # Читаем blob как ArrayBuffer
  354. reader = new FileReader()
  355. reader.onloadstart = ->
  356. debug.log "📖 Начало чтения blob как ArrayBuffer"
  357. reader.onload = (e) =>
  358. debug.log "✅ ArrayBuffer успешно прочитан"
  359. arrayBuffer = e.target.result
  360. debug.log "📊 ArrayBuffer:",
  361. byteLength: arrayBuffer.byteLength
  362. isArrayBuffer: arrayBuffer instanceof ArrayBuffer
  363. # Получаем текущий документ для правильного _rev
  364. debug.log "🔍 Получение документа #{docId} для _rev"
  365. PouchDB.getDocument(docId)
  366. .then (doc) =>
  367. debug.log "✅ Документ получен:",
  368. _id: doc._id
  369. _rev: doc._rev?.substring(0, 10) + '...'
  370. # Сохраняем как attachment в PouchDB
  371. debug.log "💾 Сохранение attachment..."
  372. return PouchDB.putAttachment(docId, filename, doc._rev, arrayBuffer, blob.type)
  373. .then (result) =>
  374. debug.log "✅ Attachment успешно сохранен:", result
  375. resolve
  376. filename: filename
  377. contentType: blob.type
  378. size: blob.size
  379. url: "/d/braer_color_shop/#{docId}/#{filename}"
  380. .catch (error) =>
  381. debug.log "❌ Ошибка при работе с документом:", error
  382. if error.status == 404
  383. debug.log "📄 Документ не найден, создаем временный"
  384. # Документа нет - создаем временный для attachment
  385. tempDoc =
  386. _id: docId
  387. type: 'product'
  388. name: 'Temp'
  389. sku: 'temp'
  390. price: 0
  391. active: false
  392. createdAt: new Date().toISOString()
  393. updatedAt: new Date().toISOString()
  394. PouchDB.saveToRemote(tempDoc)
  395. .then =>
  396. debug.log "✅ Временный документ создан"
  397. PouchDB.putAttachment(docId, filename, tempDoc._rev, arrayBuffer, blob.type)
  398. .then (result) =>
  399. debug.log "✅ Attachment сохранен во временный документ:", result
  400. resolve
  401. filename: filename
  402. contentType: blob.type
  403. size: blob.size
  404. url: "/d/braer_color_shop/#{docId}/#{filename}"
  405. .catch (err) ->
  406. debug.log "❌ Ошибка сохранения attachment во временный документ:", err
  407. reject(err)
  408. else
  409. debug.log "❌ Другая ошибка при получении документа:", error
  410. reject(error)
  411. reader.onerror = (error) =>
  412. debug.log "❌ Ошибка чтения blob:", error
  413. reject(new Error("Ошибка чтения blob: #{error}"))
  414. reader.onabort = ->
  415. debug.log "⚠️ Чтение blob прервано"
  416. debug.log "🔁 Чтение blob как ArrayBuffer..."
  417. reader.readAsArrayBuffer(blob)
  418. else
  419. errorMsg = "Ошибка загрузки изображения: #{xhr.status}"
  420. debug.log "❌ #{errorMsg}"
  421. reject(new Error(errorMsg))
  422. xhr.onerror = =>
  423. errorMsg = 'Ошибка сети при загрузке изображения'
  424. debug.log "❌ #{errorMsg}"
  425. reject(new Error(errorMsg))
  426. xhr.ontimeout = =>
  427. errorMsg = 'Таймаут загрузки изображения'
  428. debug.log "❌ #{errorMsg}"
  429. reject(new Error(errorMsg))
  430. xhr.onabort = =>
  431. debug.log "⚠️ Загрузка изображения прервана"
  432. debug.log "🚀 Отправка XHR запроса..."
  433. xhr.send()
  434. catch error
  435. debug.log "💥 Критическая ошибка в downloadAndStoreImage:", error
  436. reject(error)
  437. # Импорт товаров из CSV
  438. onFileSelect: (event) ->
  439. @selectedFile = event.target.files[0]
  440. @importResults = null
  441. @importProgress = 0
  442. @processedCount = 0
  443. importProducts: ->
  444. if !@selectedFile
  445. @showNotification 'Выберите файл для импорта', 'error'
  446. return
  447. @importing = true
  448. @importResults = null
  449. @importProgress = 0
  450. @processedCount = 0
  451. reader = new FileReader()
  452. reader.onload = (e) =>
  453. try
  454. results = Papa.parse e.target.result,
  455. header: true
  456. delimiter: ';'
  457. skipEmptyLines: true
  458. encoding: 'UTF-8'
  459. products = results.data.filter (row) =>
  460. row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*']
  461. @totalCount = products.length
  462. debug.log "📊 Найдено товаров для импорта: #{@totalCount}"
  463. # Обрабатываем товары последовательно
  464. @processProductsSequentially(products)
  465. .then (results) =>
  466. successCount = results.filter((r) -> r.success).length
  467. errorCount = results.filter((r) -> !r.success).length
  468. @importResults =
  469. success: true
  470. processed: successCount
  471. errors: results.filter((r) -> !r.success).map((r) -> r.error)
  472. total: @totalCount
  473. @importing = false
  474. @loadProducts()
  475. @loadCategories()
  476. @showNotification "Импортировано #{successCount} товаров (#{errorCount} ошибок)"
  477. .catch (error) =>
  478. @importResults =
  479. success: false
  480. error: error.message
  481. processed: 0
  482. errors: [error.message]
  483. @importing = false
  484. @showNotification "Ошибка импорта: #{error.message}", 'error'
  485. catch error
  486. @importResults =
  487. success: false
  488. error: error.message
  489. processed: 0
  490. errors: [error.message]
  491. @importing = false
  492. @showNotification "Ошибка обработки файла: #{error.message}", 'error'
  493. reader.readAsText(@selectedFile, 'UTF-8')
  494. # Исправленная последовательная обработка товаров
  495. processProductsSequentially: (products) ->
  496. return new Promise (resolve, reject) =>
  497. results = []
  498. currentIndex = 0
  499. processNextProduct = =>
  500. if currentIndex >= products.length
  501. debug.log "✅ Все товары обработаны. Успешно: #{results.filter((r) -> r.success).length}, Ошибок: #{results.filter((r) -> !r.success).length}"
  502. resolve(results)
  503. return
  504. product = products[currentIndex]
  505. currentIndex++
  506. debug.log "🔧 Обработка товара #{currentIndex}/#{products.length}: #{product['Название товара']?.substring(0, 50)}..."
  507. @transformProductData(product, currentIndex)
  508. .then (productData) =>
  509. debug.log "✅ Данные товара преобразованы: #{productData.sku}"
  510. # Сохраняем каждый товар в отдельный документ
  511. return @saveProductToDB(productData)
  512. .then (savedProduct) =>
  513. debug.log "✅ Товар сохранен в БД: #{savedProduct.sku}"
  514. # Затем обрабатываем изображения для сохраненного товара
  515. return @processProductImages(product, savedProduct)
  516. .then (finalProduct) =>
  517. @processedCount = currentIndex
  518. @importProgress = Math.round((currentIndex / products.length) * 100)
  519. results.push(success: true, product: finalProduct)
  520. debug.log "✅ Товар полностью обработан: #{finalProduct.sku}"
  521. processNextProduct()
  522. .catch (error) =>
  523. debug.log "❌ Ошибка обработки товара #{currentIndex}:", error
  524. @processedCount = currentIndex
  525. @importProgress = Math.round((currentIndex / products.length) * 100)
  526. results.push(success: false, error: error.message, product: product)
  527. # Продолжаем обработку следующих товаров даже при ошибке
  528. debug.log "➡️ Продолжение обработки следующих товаров..."
  529. processNextProduct()
  530. debug.log "🚀 Запуск последовательной обработки #{products.length} товаров"
  531. processNextProduct()
  532. # Сохранение товара в БД с правильной обработкой ревизий
  533. saveProductToDB: (productData) ->
  534. return new Promise (resolve, reject) =>
  535. debug.log "💾 Попытка сохранения товара: #{productData.sku}"
  536. # Сначала пытаемся получить существующий документ
  537. PouchDB.getDocument(productData._id)
  538. .then (existingDoc) =>
  539. # Документ существует - обновляем
  540. debug.log "🔄 Обновление существующего товара: #{productData.sku}"
  541. # Сохраняем только данные, без _rev (PouchDB сам обработает)
  542. updatedData = Object.assign {}, productData
  543. delete updatedData._rev
  544. updatedData.updatedAt = new Date().toISOString()
  545. return PouchDB.saveToRemote(updatedData)
  546. .catch (error) =>
  547. if error.status == 404
  548. # Документ не существует - создаем новый
  549. debug.log "🆕 Создание нового товара: #{productData.sku}"
  550. productData.createdAt = new Date().toISOString()
  551. productData.updatedAt = productData.createdAt
  552. return PouchDB.saveToRemote(productData)
  553. else
  554. throw error
  555. .then (result) =>
  556. # Получаем обновленный документ с правильным _rev
  557. debug.log "✅ Товар сохранен, получение обновленной версии: #{productData.sku}"
  558. return PouchDB.getDocument(productData._id)
  559. .then (savedDoc) =>
  560. debug.log "✅ Документ получен с актуальным _rev: #{savedDoc._rev?.substring(0, 10)}..."
  561. resolve(savedDoc)
  562. .catch (error) =>
  563. debug.log "❌ Ошибка сохранения товара #{productData.sku}:", error
  564. reject(error)
  565. # Обработка изображений после сохранения основного документа
  566. processProductImages: (product, savedProduct) ->
  567. debug.log "🖼️ Начало обработки изображений для товара: #{savedProduct.sku}"
  568. promises = []
  569. # Обработка основного изображения
  570. if product['Ссылка на главное фото*']
  571. imageUrl = product['Ссылка на главное фото*'].trim()
  572. if imageUrl
  573. debug.log "📸 Загрузка основного изображения: #{imageUrl}"
  574. promises.push @downloadAndStoreImage(imageUrl, savedProduct._id, 'main.jpg')
  575. .then (attachmentInfo) =>
  576. debug.log "✅ Основное изображение загружено, обновление товара"
  577. savedProduct.image = "/d/braer_color_shop/#{savedProduct._id}/main.jpg"
  578. return PouchDB.saveToRemote(savedProduct)
  579. .catch (error) =>
  580. debug.log "❌ Ошибка загрузки основного изображения:", error
  581. return savedProduct
  582. else
  583. debug.log "⏭️ Основное изображение отсутствует"
  584. # Обработка дополнительных изображений
  585. if product['Ссылки на дополнительные фото']
  586. additionalImages = product['Ссылки на дополнительные фото']
  587. if typeof additionalImages == 'string'
  588. imageUrls = additionalImages.split('\n').filter((url) -> url.trim())
  589. else
  590. imageUrls = []
  591. debug.log "🖼️ Найдено дополнительных изображений: #{imageUrls.length}"
  592. if imageUrls.length > 0
  593. savedProduct.additionalImages = []
  594. for imageUrl, i in imageUrls.slice(0, 3)
  595. do (imageUrl, i) =>
  596. debug.log "📸 Загрузка дополнительного изображения #{i}: #{imageUrl}"
  597. promise = @downloadAndStoreImage(imageUrl.trim(), savedProduct._id, "additional-#{i}.jpg")
  598. .then (attachmentInfo) =>
  599. imagePath = "/d/braer_color_shop/#{savedProduct._id}/additional-#{i}.jpg"
  600. savedProduct.additionalImages.push(imagePath)
  601. debug.log "✅ Дополнительное изображение #{i} загружено"
  602. return savedProduct
  603. .catch (error) =>
  604. debug.log "❌ Ошибка загрузки дополнительного изображения #{i}:", error
  605. return savedProduct
  606. promises.push(promise)
  607. if promises.length == 0
  608. debug.log "⏭️ Нет изображений для загрузки"
  609. return Promise.resolve(savedProduct)
  610. debug.log "⏳ Ожидание загрузки #{promises.length} изображений..."
  611. return Promise.all(promises)
  612. .then =>
  613. debug.log "✅ Все изображения загружены, обновление документа"
  614. # Обновляем документ с информацией об изображениях
  615. return PouchDB.saveToRemote(savedProduct)
  616. .then (result) =>
  617. debug.log "✅ Документ обновлен с информацией об изображениях"
  618. return PouchDB.getDocument(savedProduct._id)
  619. .catch (error) =>
  620. debug.log "❌ Ошибка обновления товара с изображениями:", error
  621. return savedProduct
  622. # Полное преобразование данных товара с правильной структурой
  623. transformProductData: (product, index) ->
  624. return new Promise (resolve, reject) =>
  625. try
  626. # Генерируем ID на основе артикула для постоянства
  627. sku = product['Артикул*']?.trim() or "SKU-#{Date.now()}-#{index}"
  628. productId = "product:#{sku}"
  629. debug.log "🔄 Преобразование данных товара #{index}: #{sku}"
  630. # Базовые поля согласно design документам
  631. productData =
  632. _id: productId
  633. type: 'product'
  634. name: product['Название товара']?.trim() or 'Без названия'
  635. sku: sku
  636. price: @parsePrice(product['Цена, руб.*'])
  637. active: true
  638. createdAt: new Date().toISOString()
  639. updatedAt: new Date().toISOString()
  640. domains: [window.location.hostname]
  641. additionalImages: []
  642. attributes: {}
  643. tags: []
  644. # Обработка всех полей CSV в единую структуру attributes
  645. @processAllCSVFields(product, productData)
  646. # Обработка категории
  647. @processCategory(product, productData, index)
  648. .then =>
  649. # Обработка Rich-контента
  650. @processRichContent(product, productData)
  651. debug.log "✅ Данные товара полностью преобразованы: #{productData.sku}"
  652. resolve(productData)
  653. .catch (error) =>
  654. debug.log "⚠️ Ошибка обработки товара, возвращаем частичные данные:", error
  655. # Возвращаем товар даже с ошибками обработки
  656. resolve(productData)
  657. catch error
  658. debug.log "❌ Критическая ошибка преобразования данных товара:", error
  659. reject(error)
  660. # Парсинг цены
  661. parsePrice: (priceString) ->
  662. return 0 if !priceString
  663. try
  664. # Удаляем пробелы и заменяем запятые на точки
  665. cleanPrice = priceString.toString().replace(/\s/g, '').replace(',', '.')
  666. price = parseFloat(cleanPrice)
  667. return if isNaN(price) then 0 else Math.round(price * 100) / 100
  668. catch
  669. return 0
  670. # Обработка всех полей CSV в единую структуру attributes
  671. processAllCSVFields: (product, productData) ->
  672. # Базовые поля
  673. if product['Бренд*']?.trim()
  674. productData.brand = product['Бренд*']?.trim()
  675. if product['Аннотация']?.trim()
  676. productData.description = product['Аннотация']?.trim()
  677. # Цены
  678. if product['Цена до скидки, руб.']
  679. productData.oldPrice = @parsePrice(product['Цена до скидки, руб.'])
  680. # Основные характеристики с оригинальными названиями
  681. @setAttribute productData, 'Вес товара, г', product
  682. @setAttribute productData, 'Объем, л', product
  683. @setAttribute productData, 'Страна-изготовитель', product
  684. @setAttribute productData, 'Гарантия', product
  685. @setAttribute productData, 'Цвет товара', product
  686. @setAttribute productData, 'Название цвета', product
  687. @setAttribute productData, 'Класс опасности товара*', product
  688. @setAttribute productData, 'Степень блеска покрытия', product
  689. @setAttribute productData, 'Работы', product
  690. @setAttribute productData, 'Количество товара в УЕИ', product
  691. # Технические характеристики
  692. @setAttribute productData, 'Расход, л/м2', product
  693. @setAttribute productData, 'Время высыхания, часов', product
  694. @setAttribute productData, 'Вид краски', product
  695. @setAttribute productData, 'Основа краски', product
  696. @setAttribute productData, 'Способ нанесения', product
  697. @setAttribute productData, 'Область применения состава', product
  698. @setAttribute productData, 'Назначение грунтовки', product
  699. @setAttribute productData, 'Рекомендуемое количество слоев', product
  700. @setAttribute productData, 'Расход, кг/м2', product
  701. @setAttribute productData, 'Количество компонентов', product
  702. @setAttribute productData, 'Особенности ЛКМ', product
  703. @setAttribute productData, 'Макс. температура эксплуатации, С°', product
  704. @setAttribute productData, 'Материал основания', product
  705. @setAttribute productData, 'Основа грунтовки', product
  706. @setAttribute productData, 'Форма выпуска средства', product
  707. @setAttribute productData, 'Назначение', product
  708. @setAttribute productData, 'Тип помещения', product
  709. @setAttribute productData, 'Вид выпуска товара', product
  710. @setAttribute productData, 'Тип растворителя', product
  711. @setAttribute productData, 'Эффект краски', product
  712. @setAttribute productData, 'Марка эмали', product
  713. @setAttribute productData, 'Базис', product
  714. @setAttribute productData, 'Помещение', product
  715. # Флаги и булевы значения (объединены с attributes)
  716. @setBooleanAttribute productData, 'Рассрочка', product
  717. @setBooleanAttribute productData, 'Баллы за отзывы', product
  718. @setBooleanAttribute productData, 'Возможность колеровки', product
  719. @setBooleanAttribute productData, 'Аэрозоль', product
  720. @setBooleanAttribute productData, 'Можно мыть', product
  721. # Мета-данные
  722. if product['#Хештеги']
  723. tags = product['#Хештеги']?.split('#').filter((tag) -> tag.trim()).map((tag) -> tag.trim())
  724. productData.tags = tags or []
  725. # Вспомогательный метод для установки атрибута
  726. setAttribute: (productData, fieldName, product) ->
  727. if product[fieldName]?.trim()
  728. # Сохраняем оригинальное название поля
  729. productData.attributes[fieldName] = product[fieldName]?.trim()
  730. # Вспомогательный метод для установки булевых атрибутов
  731. setBooleanAttribute: (productData, fieldName, product) ->
  732. if product[fieldName]
  733. value = product[fieldName]?.toLowerCase()
  734. productData.attributes[fieldName] = value == 'да'
  735. # Обработка категории с проверкой дубликатов
  736. processCategory: (product, productData, index) ->
  737. return Promise.resolve() if !product['Тип*']
  738. categoryName = product['Тип*'].trim()
  739. return Promise.resolve() if !categoryName
  740. debug.log "🔍 Поиск категории: #{categoryName}"
  741. # Поиск существующей категории (регистронезависимо)
  742. existingCategory = @categories.find (cat) ->
  743. cat.name?.toLowerCase() == categoryName.toLowerCase()
  744. if existingCategory
  745. productData.category = existingCategory._id
  746. debug.log "✅ Использована существующая категория: #{categoryName}"
  747. return Promise.resolve()
  748. else
  749. # Создание новой категории
  750. categoryId = "category:#{Date.now()}-#{index}"
  751. newCategory =
  752. _id: categoryId
  753. type: 'category'
  754. name: categoryName
  755. slug: @generateSlug(categoryName)
  756. sortOrder: @categories.length
  757. active: true
  758. createdAt: new Date().toISOString()
  759. updatedAt: new Date().toISOString()
  760. domains: [window.location.hostname]
  761. productData.category = categoryId
  762. debug.log "🆕 Создание новой категории: #{categoryName}"
  763. return PouchDB.saveToRemote(newCategory)
  764. .then (result) =>
  765. debug.log "✅ Создана новая категория: #{categoryName}"
  766. @categories.push(newCategory)
  767. return Promise.resolve()
  768. .catch (error) ->
  769. debug.log "❌ Ошибка создания категории #{categoryName}:", error
  770. # Продолжаем без категории
  771. return Promise.resolve()
  772. # Обработка Rich-контента JSON и преобразование в Markdown
  773. processRichContent: (product, productData) ->
  774. # Сначала пробуем Rich-контент JSON
  775. if product['Rich-контент JSON'] and product['Rich-контент JSON'].trim()
  776. try
  777. richContent = JSON.parse(product['Rich-контент JSON'])
  778. markdownDescription = @richContentToMarkdown(richContent)
  779. # Если получили Markdown, используем его как описание
  780. if markdownDescription and markdownDescription != 'Описание товара'
  781. productData.description = markdownDescription
  782. productData.attributes['Rich-контент JSON'] = product['Rich-контент JSON']
  783. return
  784. catch error
  785. debug.log "❌ Ошибка парсинга Rich-контента:", error
  786. # Если Rich-контент невалиден или отсутствует, используем аннотацию
  787. if product['Аннотация'] and product['Аннотация'].trim() and !productData.description
  788. productData.description = product['Аннотация'].trim()
  789. # Преобразование Rich-контента JSON в Markdown
  790. richContentToMarkdown: (richContent) ->
  791. return '' if !richContent
  792. try
  793. markdownParts = []
  794. if richContent.content and Array.isArray(richContent.content)
  795. for item in richContent.content
  796. markdownParts.push(@processContentItem(item))
  797. result = markdownParts.filter((part) -> part).join('\n\n')
  798. return result or 'Описание товара'
  799. catch error
  800. debug.log "❌ Ошибка преобразования Rich-контента в Markdown:", error
  801. return ''
  802. # Обработка отдельного элемента контента
  803. processContentItem: (item) ->
  804. return '' if !item
  805. switch item.widgetName
  806. when 'raTextBlock'
  807. return @processTextBlock(item)
  808. when 'raHeader'
  809. return @processHeader(item)
  810. when 'raImage'
  811. return @processImage(item)
  812. when 'raList'
  813. return @processList(item)
  814. else
  815. return ''
  816. # Обработка текстового блока
  817. processTextBlock: (item) ->
  818. textParts = []
  819. # Заголовок
  820. if item.title and item.title.items
  821. titleText = @processTextItems(item.title.items)
  822. if titleText
  823. level = item.title.size or 'size3'
  824. hashes = @getHeadingLevel(level)
  825. textParts.push("#{hashes} #{titleText}")
  826. # Основной текст
  827. if item.text and item.text.items
  828. bodyText = @processTextItems(item.text.items)
  829. if bodyText
  830. textParts.push(bodyText)
  831. return textParts.join('\n\n')
  832. # Обработка заголовка
  833. processHeader: (item) ->
  834. if item.text and item.text.items
  835. headerText = @processTextItems(item.text.items)
  836. if headerText
  837. level = item.size or 'size2'
  838. hashes = @getHeadingLevel(level)
  839. return "#{hashes} #{headerText}"
  840. return ''
  841. # Обработка изображения
  842. processImage: (item) ->
  843. if item.url
  844. altText = item.alt or 'Изображение товара'
  845. return "![#{altText}](#{item.url})"
  846. return ''
  847. # Обработка списка
  848. processList: (item) ->
  849. return '' if !item.items or !Array.isArray(item.items)
  850. listItems = []
  851. for listItem in item.items
  852. if listItem.content
  853. listItems.push("- #{listItem.content}")
  854. return listItems.join('\n')
  855. # Обработка текстовых элементов
  856. processTextItems: (items) ->
  857. return '' if !items or !Array.isArray(items)
  858. textParts = []
  859. for textItem in items
  860. if textItem.type == 'text' and textItem.content
  861. content = textItem.content
  862. # Обработка форматирования
  863. if textItem.formatting
  864. if textItem.formatting.bold
  865. content = "**#{content}**"
  866. if textItem.formatting.italic
  867. content = "*#{content}*"
  868. textParts.push(content)
  869. else if textItem.type == 'br'
  870. textParts.push('\n')
  871. return textParts.join('')
  872. # Получение уровня заголовка Markdown
  873. getHeadingLevel: (size) ->
  874. switch size
  875. when 'size1', 'size5' then '#' # H1
  876. when 'size2', 'size4' then '##' # H2
  877. when 'size3' then '###' # H3
  878. else '##' # H2 по умолчанию
  879. # Генерация slug
  880. generateSlug: (text) ->
  881. return '' if !text
  882. text.toLowerCase()
  883. .replace(/\s+/g, '-')
  884. .replace(/[^\w\-]+/g, '')
  885. .replace(/\-\-+/g, '-')
  886. .replace(/^-+/, '')
  887. .replace(/-+$/, '')
  888. # Массовые действия
  889. toggleSelectAll: ->
  890. if @selectAll
  891. @selectedProducts = @filteredProducts.map (product) -> product._id
  892. else
  893. @selectedProducts = []
  894. isProductSelected: (productId) ->
  895. @selectedProducts.includes(productId)
  896. toggleProductSelection: (productId) ->
  897. if @isProductSelected(productId)
  898. @selectedProducts = @selectedProducts.filter (id) -> id != productId
  899. else
  900. @selectedProducts.push(productId)
  901. clearSelection: ->
  902. @selectedProducts = []
  903. @selectAll = false
  904. toggleProductStatus: (product) ->
  905. updatedProduct = Object.assign {}, product,
  906. active: !product.active
  907. updatedAt: new Date().toISOString()
  908. PouchDB.saveToRemote(updatedProduct)
  909. .then (result) =>
  910. @loadProducts()
  911. @showNotification "Товар #{if product.active then 'деактивирован' else 'активирован'}"
  912. .catch (error) =>
  913. debug.log 'Ошибка изменения статуса товара:', error
  914. @showNotification 'Ошибка изменения статуса товара', 'error'
  915. activateSelected: ->
  916. if @selectedProducts.length == 0
  917. @showNotification 'Выберите товары для активации', 'error'
  918. return
  919. promises = @selectedProducts.map (productId) =>
  920. product = @products.find (p) -> p._id == productId
  921. if product and !product.active
  922. updatedProduct = Object.assign {}, product,
  923. active: true
  924. updatedAt: new Date().toISOString()
  925. PouchDB.saveToRemote(updatedProduct)
  926. Promise.all(promises)
  927. .then (results) =>
  928. @loadProducts()
  929. @clearSelection()
  930. @showNotification "Активировано #{results.length} товаров"
  931. .catch (error) =>
  932. debug.log 'Ошибка активации товаров:', error
  933. @showNotification 'Ошибка активации товаров', 'error'
  934. deactivateSelected: ->
  935. if @selectedProducts.length == 0
  936. @showNotification 'Выберите товары для деактивации', 'error'
  937. return
  938. promises = @selectedProducts.map (productId) =>
  939. product = @products.find (p) -> p._id == productId
  940. if product and product.active
  941. updatedProduct = Object.assign {}, product,
  942. active: false
  943. updatedAt: new Date().toISOString()
  944. PouchDB.saveToRemote(updatedProduct)
  945. Promise.all(promises)
  946. .then (results) =>
  947. @loadProducts()
  948. @clearSelection()
  949. @showNotification "Деактивировано #{results.length} товаров"
  950. .catch (error) =>
  951. debug.log 'Ошибка деактивации товаров:', error
  952. @showNotification 'Ошибка деактивации товаров', 'error'
  953. deleteSelected: ->
  954. if @selectedProducts.length == 0
  955. @showNotification 'Выберите товары для удаления', 'error'
  956. return
  957. if !confirm("Вы уверены, что хотите удалить #{@selectedProducts.length} товаров?")
  958. return
  959. promises = @selectedProducts.map (productId) =>
  960. PouchDB.getDocument(productId)
  961. .then (doc) ->
  962. PouchDB.saveToRemote(Object.assign {}, doc, { _deleted: true })
  963. Promise.all(promises)
  964. .then (results) =>
  965. @loadProducts()
  966. @clearSelection()
  967. @showNotification "Удалено #{results.length} товаров"
  968. .catch (error) =>
  969. debug.log 'Ошибка удаления товаров:', error
  970. @showNotification 'Ошибка удаления товаров', 'error'
  971. showNotification: (message, type = 'success') ->
  972. if @$root.showNotification?
  973. @$root.showNotification(message, type)
  974. else
  975. debug.log("#{type}: #{message}")
  976. mounted: ->
  977. @loadProducts()
  978. @loadCategories()
  979. @loadDomains()