index.coffee 19 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. editingProduct: null
  19. editingCategory: null
  20. selectedFile: null
  21. selectedCategoriesFile: null
  22. importing: false
  23. importingCategories: false
  24. importResults: null
  25. categoriesImportResults: null
  26. availableDomains: []
  27. categoriesActiveTab: 'list'
  28. productForm: {
  29. name: ''
  30. sku: ''
  31. category: ''
  32. price: 0
  33. oldPrice: 0
  34. brand: ''
  35. description: ''
  36. image: ''
  37. active: true
  38. domains: []
  39. }
  40. categoryForm: {
  41. name: ''
  42. slug: ''
  43. description: ''
  44. parentCategory: ''
  45. sortOrder: 0
  46. image: ''
  47. icon: ''
  48. active: true
  49. domains: []
  50. }
  51. }
  52. computed:
  53. filteredProducts: ->
  54. products = @products
  55. # Фильтр по поиску
  56. if @searchQuery
  57. query = @searchQuery.toLowerCase()
  58. products = products.filter (product) =>
  59. product.name?.toLowerCase().includes(query) ||
  60. product.sku?.toLowerCase().includes(query)
  61. # Фильтр по категории
  62. if @selectedCategory
  63. products = products.filter (product) =>
  64. product.category == @selectedCategory
  65. # Фильтр по статусу
  66. if @selectedStatus == 'active'
  67. products = products.filter (product) => product.active
  68. else if @selectedStatus == 'inactive'
  69. products = products.filter (product) => !product.active
  70. return products
  71. methods:
  72. loadProducts: ->
  73. PouchDB.queryView('admin', 'products', { include_docs: true })
  74. .then (result) =>
  75. @products = result.rows.map (row) -> row.doc
  76. .catch (error) =>
  77. console.error 'Ошибка загрузки товаров:', error
  78. @showNotification 'Ошибка загрузки товаров', 'error'
  79. loadCategories: ->
  80. PouchDB.queryView('admin', 'categories', { include_docs: true })
  81. .then (result) =>
  82. @categories = result.rows.map (row) -> row.doc
  83. .catch (error) =>
  84. console.error 'Ошибка загрузки категорий:', error
  85. loadDomains: ->
  86. PouchDB.queryView('admin', 'domain_settings', { include_docs: true })
  87. .then (result) =>
  88. @availableDomains = result.rows.map (row) -> row.doc
  89. .catch (error) =>
  90. console.error 'Ошибка загрузки доменов:', error
  91. getCategoryName: (categoryId) ->
  92. category = @categories.find (cat) -> cat._id == categoryId
  93. category?.name || 'Без категории'
  94. getCategoryProductCount: (categoryId) ->
  95. @products.filter((product) -> product.category == categoryId).length
  96. # Управление товарами
  97. editProduct: (product) ->
  98. @editingProduct = product
  99. @productForm = {
  100. name: product.name || ''
  101. sku: product.sku || ''
  102. category: product.category || ''
  103. price: product.price || 0
  104. oldPrice: product.oldPrice || 0
  105. brand: product.brand || ''
  106. description: product.description || ''
  107. image: product.image || ''
  108. active: product.active != false
  109. domains: product.domains || []
  110. }
  111. @showProductModal = true
  112. saveProduct: ->
  113. if !@productForm.name || !@productForm.sku || !@productForm.price
  114. @showNotification 'Заполните обязательные поля (Название, Артикул, Цена)', 'error'
  115. return
  116. productData = {
  117. type: 'product'
  118. ...@productForm
  119. updatedAt: new Date().toISOString()
  120. }
  121. if @editingProduct
  122. productData._id = @editingProduct._id
  123. productData._rev = @editingProduct._rev
  124. productData.createdAt = @editingProduct.createdAt
  125. else
  126. productData._id = "product:#{Date.now()}"
  127. productData.createdAt = new Date().toISOString()
  128. PouchDB.saveToRemote(productData)
  129. .then (result) =>
  130. @showProductModal = false
  131. @resetProductForm()
  132. @loadProducts()
  133. @showNotification 'Товар успешно сохранен'
  134. .catch (error) =>
  135. console.error 'Ошибка сохранения товара:', error
  136. @showNotification 'Ошибка сохранения товара', 'error'
  137. removeProductImage: ->
  138. @productForm.image = ''
  139. onProductImageUpload: (event) ->
  140. file = event.target.files[0]
  141. if file
  142. reader = new FileReader()
  143. reader.onload = (e) =>
  144. @productForm.image = e.target.result
  145. reader.readAsDataURL(file)
  146. # Управление категориями
  147. editCategory: (category) ->
  148. @editingCategory = category
  149. @categoryForm = {
  150. name: category.name || ''
  151. slug: category.slug || ''
  152. description: category.description || ''
  153. parentCategory: category.parentCategory || ''
  154. sortOrder: category.sortOrder || 0
  155. image: category.image || ''
  156. icon: category.icon || ''
  157. active: category.active != false
  158. domains: category.domains || []
  159. }
  160. @showCategoryModal = true
  161. saveCategory: ->
  162. if !@categoryForm.name || !@categoryForm.slug
  163. @showNotification 'Заполните обязательные поля (Название, URL slug)', 'error'
  164. return
  165. categoryData = {
  166. type: 'category'
  167. ...@categoryForm
  168. updatedAt: new Date().toISOString()
  169. }
  170. if @editingCategory
  171. categoryData._id = @editingCategory._id
  172. categoryData._rev = @editingCategory._rev
  173. categoryData.createdAt = @editingCategory.createdAt
  174. else
  175. categoryData._id = "category:#{Date.now()}"
  176. categoryData.createdAt = new Date().toISOString()
  177. PouchDB.saveToRemote(categoryData)
  178. .then (result) =>
  179. @showCategoryModal = false
  180. @resetCategoryForm()
  181. @loadCategories()
  182. @showNotification 'Категория успешно сохранена'
  183. .catch (error) =>
  184. console.error 'Ошибка сохранения категории:', error
  185. @showNotification 'Ошибка сохранения категории', 'error'
  186. removeCategoryImage: ->
  187. @categoryForm.image = ''
  188. removeCategoryIcon: ->
  189. @categoryForm.icon = ''
  190. onCategoryImageUpload: (event) ->
  191. file = event.target.files[0]
  192. if file
  193. reader = new FileReader()
  194. reader.onload = (e) =>
  195. @categoryForm.image = e.target.result
  196. reader.readAsDataURL(file)
  197. onCategoryIconUpload: (event) ->
  198. file = event.target.files[0]
  199. if file
  200. reader = new FileReader()
  201. reader.onload = (e) =>
  202. @categoryForm.icon = e.target.result
  203. reader.readAsDataURL(file)
  204. deleteCategory: (categoryId) ->
  205. if confirm('Вы уверены, что хотите удалить эту категорию?')
  206. PouchDB.getDocument(categoryId)
  207. .then (doc) ->
  208. PouchDB.saveToRemote({ ...doc, _deleted: true })
  209. .then (result) =>
  210. @loadCategories()
  211. @showNotification 'Категория удалена'
  212. .catch (error) =>
  213. console.error 'Ошибка удаления категории:', error
  214. @showNotification 'Ошибка удаления категории', 'error'
  215. # Импорт товаров
  216. onFileSelect: (event) ->
  217. @selectedFile = event.target.files[0]
  218. @importResults = null
  219. importProducts: ->
  220. if !@selectedFile
  221. @showNotification 'Выберите файл для импорта', 'error'
  222. return
  223. @importing = true
  224. @importResults = null
  225. reader = new FileReader()
  226. reader.onload = (e) =>
  227. try
  228. results = Papa.parse e.target.result, {
  229. header: true
  230. delimiter: ';'
  231. skipEmptyLines: true
  232. encoding: 'UTF-8'
  233. }
  234. products = results.data.filter (row) =>
  235. row && row['Артикул*'] && row['Название товара'] && row['Цена, руб.*']
  236. couchProducts = products.map (product, index) =>
  237. @transformProductData(product, index)
  238. # Пакетное сохранение
  239. PouchDB.bulkDocs(couchProducts)
  240. .then (result) =>
  241. @importResults = {
  242. success: true,
  243. processed: couchProducts.length,
  244. errors: []
  245. }
  246. @importing = false
  247. @loadProducts()
  248. @loadCategories() # Перезагружаем категории, т.к. могли добавиться новые
  249. @showNotification "Импортировано #{couchProducts.length} товаров"
  250. .catch (error) =>
  251. @importResults = {
  252. success: false,
  253. error: error.message,
  254. processed: 0,
  255. errors: [error.message]
  256. }
  257. @importing = false
  258. @showNotification "Ошибка импорта: #{error.message}", 'error'
  259. catch error
  260. @importResults = {
  261. success: false,
  262. error: error.message,
  263. processed: 0,
  264. errors: [error.message]
  265. }
  266. @importing = false
  267. @showNotification "Ошибка обработки файла: #{error.message}", 'error'
  268. reader.readAsText(@selectedFile, 'UTF-8')
  269. transformProductData: (product, index) ->
  270. # Базовые поля
  271. productData = {
  272. _id: "product:#{Date.now()}-#{index}"
  273. type: 'product'
  274. name: product['Название товара']
  275. sku: product['Артикул*']
  276. price: parseFloat(product['Цена, руб.*'].replace(/\s/g, '').replace(',', '.')) || 0
  277. active: true
  278. createdAt: new Date().toISOString()
  279. updatedAt: new Date().toISOString()
  280. }
  281. # Обработка категории из поля "Тип*"
  282. if product['Тип*']
  283. categoryName = product['Тип*'].trim()
  284. # Ищем существующую категорию
  285. existingCategory = @categories.find (cat) ->
  286. cat.name?.toLowerCase() == categoryName.toLowerCase()
  287. if existingCategory
  288. productData.category = existingCategory._id
  289. else
  290. # Создаем новую категорию
  291. categoryId = "category:#{Date.now()}-#{index}"
  292. newCategory = {
  293. _id: categoryId
  294. type: 'category'
  295. name: categoryName
  296. slug: @generateSlug(categoryName)
  297. sortOrder: @categories.length
  298. active: true
  299. createdAt: new Date().toISOString()
  300. updatedAt: new Date().toISOString()
  301. domains: @availableDomains?.map((d) -> d.domain) || []
  302. }
  303. # Сохраняем новую категорию
  304. PouchDB.saveToRemote(newCategory)
  305. productData.category = categoryId
  306. # Дополнительные поля
  307. if product['Цена до скидки, руб.']
  308. productData.oldPrice = parseFloat(product['Цена до скидки, руб.'].replace(/\s/g, '').replace(',', '.'))
  309. if product['Ссылка на главное фото*']
  310. productData.image = product['Ссылка на главное фото*']
  311. if product['Бренд*']
  312. productData.brand = product['Бренд*']
  313. if product['Тип*']
  314. productData.productType = product['Тип*']
  315. # Rich content преобразование
  316. if product['Rich-контент JSON']
  317. try
  318. richContent = JSON.parse(product['Rich-контент JSON'])
  319. productData.description = @richContentToMarkdown(richContent)
  320. catch
  321. productData.description = product['Аннотация'] || ''
  322. else
  323. productData.description = product['Аннотация'] || ''
  324. # Домены
  325. productData.domains = @availableDomains?.map((d) -> d.domain) || []
  326. return productData
  327. # Импорт категорий
  328. onCategoriesFileSelect: (event) ->
  329. @selectedCategoriesFile = event.target.files[0]
  330. @categoriesImportResults = null
  331. importCategories: ->
  332. if !@selectedCategoriesFile
  333. @showNotification 'Выберите файл категорий для импорта', 'error'
  334. return
  335. @importingCategories = true
  336. @categoriesImportResults = null
  337. reader = new FileReader()
  338. reader.onload = (e) =>
  339. try
  340. results = Papa.parse e.target.result, {
  341. header: true
  342. delimiter: ','
  343. skipEmptyLines: true
  344. encoding: 'UTF-8'
  345. }
  346. categories = results.data.filter (row) =>
  347. row && row.name && row.slug
  348. couchCategories = categories.map (category, index) =>
  349. @transformCategoryData(category, index)
  350. # Пакетное сохранение категорий
  351. PouchDB.bulkDocs(couchCategories)
  352. .then (result) =>
  353. @categoriesImportResults = {
  354. success: true,
  355. processed: couchCategories.length,
  356. errors: []
  357. }
  358. @importingCategories = false
  359. @loadCategories()
  360. @showNotification "Импортировано #{couchCategories.length} категорий"
  361. .catch (error) =>
  362. @categoriesImportResults = {
  363. success: false,
  364. error: error.message,
  365. processed: 0,
  366. errors: [error.message]
  367. }
  368. @importingCategories = false
  369. @showNotification "Ошибка импорта категорий: #{error.message}", 'error'
  370. catch error
  371. @categoriesImportResults = {
  372. success: false,
  373. error: error.message,
  374. processed: 0,
  375. errors: [error.message]
  376. }
  377. @importingCategories = false
  378. @showNotification "Ошибка обработки файла категорий: #{error.message}", 'error'
  379. reader.readAsText(@selectedCategoriesFile, 'UTF-8')
  380. transformCategoryData: (category, index) ->
  381. categoryData = {
  382. _id: "category:import-#{Date.now()}-#{index}"
  383. type: 'category'
  384. name: category.name
  385. slug: category.slug
  386. description: category.description || ''
  387. parentCategory: category.parentCategory || ''
  388. sortOrder: parseInt(category.sortOrder) || @categories.length + index
  389. active: category.active != 'false'
  390. createdAt: new Date().toISOString()
  391. updatedAt: new Date().toISOString()
  392. domains: @availableDomains?.map((d) -> d.domain) || []
  393. }
  394. if category.image
  395. categoryData.image = category.image
  396. if category.icon
  397. categoryData.icon = category.icon
  398. return categoryData
  399. # Вспомогательные методы
  400. generateSlug: (text) ->
  401. text.toLowerCase()
  402. .replace(/\s+/g, '-')
  403. .replace(/[^\w\-]+/g, '')
  404. .replace(/\-\-+/g, '-')
  405. .replace(/^-+/, '')
  406. .replace(/-+$/, '')
  407. richContentToMarkdown: (richContent) ->
  408. # Простое преобразование rich content в markdown
  409. return JSON.stringify(richContent) # Временная реализация
  410. toggleProductStatus: (product) ->
  411. updatedProduct = {
  412. ...product
  413. active: !product.active
  414. updatedAt: new Date().toISOString()
  415. }
  416. PouchDB.saveToRemote(updatedProduct)
  417. .then (result) =>
  418. @loadProducts()
  419. @showNotification 'Статус товара обновлен'
  420. .catch (error) =>
  421. console.error 'Ошибка обновления статуса:', error
  422. @showNotification 'Ошибка обновления статуса', 'error'
  423. deleteProduct: (productId) ->
  424. if confirm('Вы уверены, что хотите удалить этот товар?')
  425. PouchDB.getDocument(productId)
  426. .then (doc) ->
  427. PouchDB.saveToRemote({ ...doc, _deleted: true })
  428. .then (result) =>
  429. @loadProducts()
  430. @showNotification 'Товар удален'
  431. .catch (error) =>
  432. console.error 'Ошибка удаления товара:', error
  433. @showNotification 'Ошибка удаления товара', 'error'
  434. resetProductForm: ->
  435. @editingProduct = null
  436. @productForm = {
  437. name: ''
  438. sku: ''
  439. category: ''
  440. price: 0
  441. oldPrice: 0
  442. brand: ''
  443. description: ''
  444. image: ''
  445. active: true
  446. domains: []
  447. }
  448. resetCategoryForm: ->
  449. @editingCategory = null
  450. @categoryForm = {
  451. name: ''
  452. slug: ''
  453. description: ''
  454. parentCategory: ''
  455. sortOrder: 0
  456. image: ''
  457. icon: ''
  458. active: true
  459. domains: []
  460. }
  461. getCategoriesTabClass: (tabId) ->
  462. baseClass = 'admin-products__categories-tab'
  463. isActive = @categoriesActiveTab == tabId
  464. if isActive
  465. return "#{baseClass} admin-products__categories-tab--active"
  466. else
  467. return baseClass
  468. formatPrice: (price) ->
  469. return '0 ₽' if !price
  470. new Intl.NumberFormat('ru-RU', {
  471. style: 'currency'
  472. currency: 'RUB'
  473. minimumFractionDigits: 0
  474. }).format(price)
  475. getStatusClass: (isActive) ->
  476. baseClass = 'admin-products__status'
  477. if isActive
  478. return "#{baseClass} admin-products__status--active"
  479. else
  480. return "#{baseClass} admin-products__status--inactive"
  481. showNotification: (message, type = 'success') ->
  482. @$root.showNotification?(message, type) || debug.log("#{type}: #{message}")
  483. mounted: ->
  484. @loadProducts()
  485. @loadCategories()
  486. @loadDomains()