index.coffee 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  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. # Mass actions data
  33. selectedProducts: []
  34. selectAll: false
  35. massCategory: ''
  36. massAllCategory: ''
  37. removeExistingCategories: false
  38. massRemoveAllCategories: false
  39. priceChangeType: 'fixed'
  40. priceChangeValue: null
  41. applyToOldPrice: false
  42. productForm: {
  43. name: ''
  44. sku: ''
  45. category: ''
  46. price: 0
  47. oldPrice: 0
  48. brand: ''
  49. description: ''
  50. image: ''
  51. active: true
  52. domains: []
  53. }
  54. categoryForm: {
  55. name: ''
  56. slug: ''
  57. description: ''
  58. parentCategory: ''
  59. sortOrder: 0
  60. image: ''
  61. icon: ''
  62. active: true
  63. domains: []
  64. }
  65. }
  66. computed:
  67. filteredProducts: ->
  68. products = @products
  69. if @searchQuery
  70. query = @searchQuery.toLowerCase()
  71. products = products.filter (product) =>
  72. product.name?.toLowerCase().includes(query) ||
  73. product.sku?.toLowerCase().includes(query)
  74. if @selectedCategory
  75. products = products.filter (product) =>
  76. product.category == @selectedCategory
  77. if @selectedStatus == 'active'
  78. products = products.filter (product) => product.active
  79. else if @selectedStatus == 'inactive'
  80. products = products.filter (product) => !product.active
  81. return products
  82. methods:
  83. getStatusClass: (isActive) ->
  84. baseClass = 'admin-products__status'
  85. if isActive
  86. return "#{baseClass} admin-products__status--active"
  87. else
  88. return "#{baseClass} admin-products__status--inactive"
  89. formatPrice: (price) ->
  90. return '0 ₽' if !price
  91. new Intl.NumberFormat('ru-RU', {
  92. style: 'currency'
  93. currency: 'RUB'
  94. minimumFractionDigits: 0
  95. }).format(price)
  96. getCategoryName: (categoryId) ->
  97. category = @categories.find (cat) -> cat._id == categoryId
  98. category?.name || 'Без категории'
  99. getCategoryProductCount: (categoryId) ->
  100. @products.filter((product) -> product.category == categoryId).length
  101. loadProducts: ->
  102. PouchDB.queryView('admin', 'products', { include_docs: true })
  103. .then (result) =>
  104. @products = result.rows.map (row) -> row.doc
  105. .catch (error) =>
  106. debug.log 'Ошибка загрузки товаров:', error
  107. @showNotification 'Ошибка загрузки товаров', 'error'
  108. loadCategories: ->
  109. PouchDB.queryView('admin', 'categories', { include_docs: true })
  110. .then (result) =>
  111. @categories = result.rows.map (row) -> row.doc
  112. .catch (error) =>
  113. debug.log 'Ошибка загрузки категорий:', error
  114. loadDomains: ->
  115. PouchDB.queryView('admin', 'domain_settings', { include_docs: true })
  116. .then (result) =>
  117. @availableDomains = result.rows.map (row) -> row.doc
  118. .catch (error) =>
  119. debug.log 'Ошибка загрузки доменов:', error
  120. # Mass actions methods
  121. toggleSelectAll: ->
  122. if @selectAll
  123. @selectedProducts = @filteredProducts.map (product) -> product._id
  124. else
  125. @selectedProducts = []
  126. isProductSelected: (productId) ->
  127. @selectedProducts.includes(productId)
  128. clearSelection: ->
  129. @selectedProducts = []
  130. @selectAll = false
  131. activateSelected: ->
  132. if @selectedProducts.length == 0
  133. @showNotification 'Выберите товары для активации', 'error'
  134. return
  135. promises = @selectedProducts.map (productId) =>
  136. product = @products.find (p) -> p._id == productId
  137. if product && !product.active
  138. updatedProduct = {
  139. ...product
  140. active: true
  141. updatedAt: new Date().toISOString()
  142. }
  143. PouchDB.saveToRemote(updatedProduct)
  144. Promise.all(promises)
  145. .then (results) =>
  146. @loadProducts()
  147. @clearSelection()
  148. @showNotification "Активировано #{results.length} товаров"
  149. .catch (error) =>
  150. debug.log 'Ошибка активации товаров:', error
  151. @showNotification 'Ошибка активации товаров', 'error'
  152. deactivateSelected: ->
  153. if @selectedProducts.length == 0
  154. @showNotification 'Выберите товары для деактивации', 'error'
  155. return
  156. promises = @selectedProducts.map (productId) =>
  157. product = @products.find (p) -> p._id == productId
  158. if product && product.active
  159. updatedProduct = {
  160. ...product
  161. active: false
  162. updatedAt: new Date().toISOString()
  163. }
  164. PouchDB.saveToRemote(updatedProduct)
  165. Promise.all(promises)
  166. .then (results) =>
  167. @loadProducts()
  168. @clearSelection()
  169. @showNotification "Деактивировано #{results.length} товаров"
  170. .catch (error) =>
  171. debug.log 'Ошибка деактивации товаров:', error
  172. @showNotification 'Ошибка деактивации товаров', 'error'
  173. deleteSelected: ->
  174. if @selectedProducts.length == 0
  175. @showNotification 'Выберите товары для удаления', 'error'
  176. return
  177. if !confirm("Вы уверены, что хотите удалить #{@selectedProducts.length} товаров?")
  178. return
  179. promises = @selectedProducts.map (productId) =>
  180. PouchDB.getDocument(productId)
  181. .then (doc) ->
  182. PouchDB.saveToRemote({ ...doc, _deleted: true })
  183. Promise.all(promises)
  184. .then (results) =>
  185. @loadProducts()
  186. @clearSelection()
  187. @showNotification "Удалено #{results.length} товаров"
  188. .catch (error) =>
  189. debug.log 'Ошибка удаления товаров:', error
  190. @showNotification 'Ошибка удаления товаров', 'error'
  191. assignCategoryToSelected: ->
  192. if @selectedProducts.length == 0 || !@massCategory
  193. @showNotification 'Выберите товары и категорию', 'error'
  194. return
  195. promises = @selectedProducts.map (productId) =>
  196. product = @products.find (p) -> p._id == productId
  197. if product
  198. updatedProduct = {
  199. ...product
  200. updatedAt: new Date().toISOString()
  201. }
  202. if @removeExistingCategories
  203. updatedProduct.category = @massCategory
  204. else
  205. # Если категория уже есть, не перезаписываем
  206. updatedProduct.category = @massCategory
  207. PouchDB.saveToRemote(updatedProduct)
  208. Promise.all(promises)
  209. .then (results) =>
  210. @loadProducts()
  211. @clearSelection()
  212. @showCategoryAssignModal = false
  213. @massCategory = ''
  214. @removeExistingCategories = false
  215. @showNotification "Категория назначена для #{results.length} товаров"
  216. .catch (error) =>
  217. debug.log 'Ошибка назначения категории:', error
  218. @showNotification 'Ошибка назначения категории', 'error'
  219. assignCategoryToAll: ->
  220. if !@massAllCategory
  221. @showNotification 'Выберите категорию', 'error'
  222. return
  223. promises = @products.map (product) =>
  224. updatedProduct = {
  225. ...product
  226. updatedAt: new Date().toISOString()
  227. }
  228. if @massRemoveAllCategories
  229. updatedProduct.category = @massAllCategory
  230. else if !updatedProduct.category
  231. updatedProduct.category = @massAllCategory
  232. PouchDB.saveToRemote(updatedProduct)
  233. Promise.all(promises)
  234. .then (results) =>
  235. @loadProducts()
  236. @showMassCategoryAssign = false
  237. @massAllCategory = ''
  238. @massRemoveAllCategories = false
  239. @showNotification "Категория назначена для всех товаров"
  240. .catch (error) =>
  241. debug.log 'Ошибка назначения категории:', error
  242. @showNotification 'Ошибка назначения категории', 'error'
  243. massChangeStatus: (status) ->
  244. promises = @products.map (product) =>
  245. if product.active != status
  246. updatedProduct = {
  247. ...product
  248. active: status
  249. updatedAt: new Date().toISOString()
  250. }
  251. PouchDB.saveToRemote(updatedProduct)
  252. else
  253. Promise.resolve()
  254. Promise.all(promises)
  255. .then (results) =>
  256. @loadProducts()
  257. @showNotification "Статус всех товаров изменен"
  258. .catch (error) =>
  259. debug.log 'Ошибка изменения статуса:', error
  260. @showNotification 'Ошибка изменения статуса', 'error'
  261. massRemoveCategories: ->
  262. if !confirm("Удалить категории у всех товаров?")
  263. return
  264. promises = @products.map (product) =>
  265. if product.category
  266. updatedProduct = {
  267. ...product
  268. category: ''
  269. updatedAt: new Date().toISOString()
  270. }
  271. PouchDB.saveToRemote(updatedProduct)
  272. else
  273. Promise.resolve()
  274. Promise.all(promises)
  275. .then (results) =>
  276. @loadProducts()
  277. @showNotification "Категории удалены у всех товаров"
  278. .catch (error) =>
  279. debug.log 'Ошибка удаления категорий:', error
  280. @showNotification 'Ошибка удаления категорий', 'error'
  281. applyMassPriceChange: ->
  282. if !@priceChangeValue
  283. @showNotification 'Введите значение изменения', 'error'
  284. return
  285. promises = @products.map (product) =>
  286. updatedProduct = { ...product, updatedAt: new Date().toISOString() }
  287. switch @priceChangeType
  288. when 'fixed'
  289. if @applyToOldPrice
  290. updatedProduct.oldPrice = parseFloat(@priceChangeValue)
  291. else
  292. updatedProduct.price = parseFloat(@priceChangeValue)
  293. when 'percent'
  294. if @applyToOldPrice && updatedProduct.oldPrice
  295. updatedProduct.oldPrice = updatedProduct.oldPrice * (1 + parseFloat(@priceChangeValue) / 100)
  296. else
  297. updatedProduct.price = updatedProduct.price * (1 + parseFloat(@priceChangeValue) / 100)
  298. when 'increase'
  299. if @applyToOldPrice && updatedProduct.oldPrice
  300. updatedProduct.oldPrice = updatedProduct.oldPrice + parseFloat(@priceChangeValue)
  301. else
  302. updatedProduct.price = updatedProduct.price + parseFloat(@priceChangeValue)
  303. when 'decrease'
  304. if @applyToOldPrice && updatedProduct.oldPrice
  305. updatedProduct.oldPrice = Math.max(0, updatedProduct.oldPrice - parseFloat(@priceChangeValue))
  306. else
  307. updatedProduct.price = Math.max(0, updatedProduct.price - parseFloat(@priceChangeValue))
  308. PouchDB.saveToRemote(updatedProduct)
  309. Promise.all(promises)
  310. .then (results) =>
  311. @loadProducts()
  312. @showMassPriceModal = false
  313. @priceChangeValue = null
  314. @showNotification "Цены успешно обновлены"
  315. .catch (error) =>
  316. debug.log 'Ошибка изменения цен:', error
  317. @showNotification 'Ошибка изменения цен', 'error'
  318. exportProducts: ->
  319. csvData = @products.map (product) =>
  320. categoryName = @getCategoryName(product.category)
  321. return {
  322. 'Название товара': product.name
  323. 'Артикул': product.sku
  324. 'Цена, руб.': product.price
  325. 'Старая цена, руб.': product.oldPrice || ''
  326. 'Категория': categoryName
  327. 'Бренд': product.brand || ''
  328. 'Статус': if product.active then 'Активен' else 'Неактивен'
  329. 'Описание': product.description || ''
  330. }
  331. csv = Papa.unparse(csvData, {
  332. delimiter: ';'
  333. encoding: 'UTF-8'
  334. })
  335. blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' })
  336. link = document.createElement('a')
  337. url = URL.createObjectURL(blob)
  338. link.setAttribute('href', url)
  339. link.setAttribute('download', 'products_export.csv')
  340. link.style.visibility = 'hidden'
  341. document.body.appendChild(link)
  342. link.click()
  343. document.body.removeChild(link)
  344. @showNotification 'Экспорт завершен'
  345. exportSelectedProducts: ->
  346. if @selectedProducts.length == 0
  347. @showNotification 'Выберите товары для экспорта', 'error'
  348. return
  349. selectedProductsData = @products.filter (product) =>
  350. @selectedProducts.includes(product._id)
  351. csvData = selectedProductsData.map (product) =>
  352. categoryName = @getCategoryName(product.category)
  353. return {
  354. 'Название товара': product.name
  355. 'Артикул': product.sku
  356. 'Цена, руб.': product.price
  357. 'Старая цена, руб.': product.oldPrice || ''
  358. 'Категория': categoryName
  359. 'Бренд': product.brand || ''
  360. 'Статус': if product.active then 'Активен' else 'Неактивен'
  361. 'Описание': product.description || ''
  362. }
  363. csv = Papa.unparse(csvData, {
  364. delimiter: ';'
  365. encoding: 'UTF-8'
  366. })
  367. blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' })
  368. link = document.createElement('a')
  369. url = URL.createObjectURL(blob)
  370. link.setAttribute('href', url)
  371. link.setAttribute('download', 'selected_products_export.csv')
  372. link.style.visibility = 'hidden'
  373. document.body.appendChild(link)
  374. link.click()
  375. document.body.removeChild(link)
  376. @showNotification 'Экспорт выбранных товаров завершен'
  377. # Updated category creation with duplicate check
  378. transformProductData: (product, index) ->
  379. productData = {
  380. _id: "product:#{Date.now()}-#{index}"
  381. type: 'product'
  382. name: product['Название товара']
  383. sku: product['Артикул*']
  384. price: parseFloat(product['Цена, руб.*'].replace(/\s/g, '').replace(',', '.')) || 0
  385. active: true
  386. createdAt: new Date().toISOString()
  387. updatedAt: new Date().toISOString()
  388. }
  389. # Improved category handling with duplicate check
  390. if product['Тип*']
  391. categoryName = product['Тип*'].trim()
  392. # Check for existing category by name (case insensitive)
  393. existingCategory = @categories.find (cat) ->
  394. cat.name?.toLowerCase() == categoryName.toLowerCase()
  395. if existingCategory
  396. productData.category = existingCategory._id
  397. debug.log "Использована существующая категория: #{categoryName}"
  398. else
  399. # Create new category only if it doesn't exist
  400. categoryId = "category:#{Date.now()}-#{index}"
  401. newCategory = {
  402. _id: categoryId
  403. type: 'category'
  404. name: categoryName
  405. slug: @generateSlug(categoryName)
  406. sortOrder: @categories.length
  407. active: true
  408. createdAt: new Date().toISOString()
  409. updatedAt: new Date().toISOString()
  410. domains: @availableDomains?.map((d) -> d.domain) || []
  411. }
  412. # Save new category and add to local categories array
  413. PouchDB.saveToRemote(newCategory)
  414. .then (result) =>
  415. debug.log "Создана новая категория: #{categoryName}"
  416. @categories.push(newCategory)
  417. .catch (error) ->
  418. debug.log "Ошибка создания категории #{categoryName}:", error
  419. productData.category = categoryId
  420. # Rest of the method remains the same...
  421. if product['Цена до скидки, руб.']
  422. productData.oldPrice = parseFloat(product['Цена до скидки, руб.'].replace(/\s/g, '').replace(',', '.'))
  423. if product['Ссылка на главное фото*']
  424. productData.image = product['Ссылка на главное фото*']
  425. if product['Бренд*']
  426. productData.brand = product['Бренд*']
  427. if product['Тип*']
  428. productData.productType = product['Тип*']
  429. if product['Rich-контент JSON']
  430. try
  431. richContent = JSON.parse(product['Rich-контент JSON'])
  432. productData.description = @richContentToMarkdown(richContent)
  433. catch
  434. productData.description = product['Аннотация'] || ''
  435. else
  436. productData.description = product['Аннотация'] || ''
  437. productData.domains = @availableDomains?.map((d) -> d.domain) || []
  438. return productData
  439. # Updated category creation method with duplicate prevention
  440. saveCategory: ->
  441. if !@categoryForm.name || !@categoryForm.slug
  442. @showNotification 'Заполните обязательные поля (Название, URL slug)', 'error'
  443. return
  444. # Check for duplicate category name
  445. duplicateCategory = @categories.find (cat) =>
  446. cat.name?.toLowerCase() == @categoryForm.name.toLowerCase() &&
  447. (!@editingCategory || cat._id != @editingCategory._id)
  448. if duplicateCategory
  449. @showNotification 'Категория с таким названием уже существует', 'error'
  450. return
  451. categoryData = {
  452. type: 'category'
  453. ...@categoryForm
  454. updatedAt: new Date().toISOString()
  455. }
  456. if @editingCategory
  457. categoryData._id = @editingCategory._id
  458. categoryData._rev = @editingCategory._rev
  459. categoryData.createdAt = @editingCategory.createdAt
  460. else
  461. categoryData._id = "category:#{Date.now()}"
  462. categoryData.createdAt = new Date().toISOString()
  463. PouchDB.saveToRemote(categoryData)
  464. .then (result) =>
  465. @showCategoryModal = false
  466. @resetCategoryForm()
  467. @loadCategories()
  468. @showNotification 'Категория успешно сохранена'
  469. .catch (error) =>
  470. debug.log 'Ошибка сохранения категории:', error
  471. @showNotification 'Ошибка сохранения категории', 'error'
  472. # Rest of the methods remain the same...
  473. showNotification: (message, type = 'success') ->
  474. @$root.showNotification?(message, type) || debug.log("#{type}: #{message}")
  475. mounted: ->
  476. @loadProducts()
  477. @loadCategories()
  478. @loadDomains()