Gogs il y a 4 semaines
Parent
commit
9701d0c8ef

+ 3 - 1
README.md

@@ -574,7 +574,9 @@ https://cdn1.ozone.ru/s3/multimedia-1-p/7663352533.jpg";;;ЭкоКрас;4673764
   Анализировать реализованный код, по git репозитарию https://gogs.osvoj.ru/oleg/s5l.ru-crm.git
   Проверяй промт и изменения в нём по адресу https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md
   Важно Всегда приводи только полные листинги файлов
-  в стил файлах не используй @import '../../index.styl', только стили текущего элемента
+  в coffee файлах используй debug.log в coffee.log
+  в стил файлах не используй @import '../../index.styl', только стили текущего элемента,
+  общие переменные определяй только в app/index.styl
   
   Добавь в app/pages/Admin/Products управление категориями, и загрузки изображений к ним. 
   также звгружай информацию из категорий из csv файла поле "Тип*", доделай окно редактирования товара

+ 3 - 3
app/index.coffee

@@ -56,12 +56,12 @@ app = Vue.createApp({
           if settings?.companyName
             document.title = settings.companyName
         .catch (error) =>
-          console.log 'Настройки домена не найдены, используются значения по умолчанию'
+          debug.log 'Настройки домена не найдены, используются значения по умолчанию'
           @currentDomainSettings = null
     
     showNotification: (message, type = 'success') ->
       # Реализация системы уведомлений
-      console.log "#{type.toUpperCase()}: #{message}"
+      debug.log "#{type.toUpperCase()}: #{message}"
   
   mounted: ->
     # Инициализация темы
@@ -71,7 +71,7 @@ app = Vue.createApp({
     # Инициализация PouchDB
     try
       await pouchService.init()
-      console.log 'PouchDB инициализирован'
+      debug.log 'PouchDB инициализирован'
       # Загружаем настройки домена
       await @loadDomainSettings()
     catch error

+ 227 - 96
app/index.styl

@@ -1,4 +1,4 @@
-// Переменные цветов
+// Базовые цвета
 $primary-50 = #fef2f2
 $primary-100 = #fee2e2
 $primary-200 = #fecaca
@@ -35,6 +35,186 @@ $gray-900 = #111827
 $white = #ffffff
 $black = #000000
 
+// CSS-переменные для глобального использования
+:root
+  // Основные цвета
+  --color-primary-50: #fef2f2
+  --color-primary-100: #fee2e2
+  --color-primary-200: #fecaca
+  --color-primary-300: #fca5a5
+  --color-primary-400: #f87171
+  --color-primary-500: #ef4444
+  --color-primary-600: #dc2626
+  --color-primary-700: #b91c1c
+  --color-primary-800: #991b1b
+  --color-primary-900: #7f1d1d
+
+  --color-accent-50: #f0f9ff
+  --color-accent-100: #e0f2fe
+  --color-accent-200: #bae6fd
+  --color-accent-300: #7dd3fc
+  --color-accent-400: #38bdf8
+  --color-accent-500: #0ea5e9
+  --color-accent-600: #0284c7
+  --color-accent-700: #0369a1
+  --color-accent-800: #075985
+  --color-accent-900: #0c4a6e
+
+  --color-gray-50: #f9fafb
+  --color-gray-100: #f3f4f6
+  --color-gray-200: #e5e7eb
+  --color-gray-300: #d1d5db
+  --color-gray-400: #9ca3af
+  --color-gray-500: #6b7280
+  --color-gray-600: #4b5563
+  --color-gray-700: #374151
+  --color-gray-800: #1f2937
+  --color-gray-900: #111827
+
+  --color-white: #ffffff
+  --color-black: #000000
+
+  // RGB значения для использования с прозрачностью
+  --color-primary-500-rgb: 239, 68, 68
+  --color-primary-600-rgb: 220, 38, 38
+  --color-accent-500-rgb: 14, 165, 233
+  --color-accent-600-rgb: 2, 132, 199
+  --color-gray-500-rgb: 107, 114, 128
+  --color-gray-600-rgb: 75, 85, 99
+  --color-gray-700-rgb: 55, 65, 81
+  --color-gray-800-rgb: 31, 41, 55
+  --color-gray-900-rgb: 17, 24, 39
+  --color-white-rgb: 255, 255, 255
+  --color-black-rgb: 0, 0, 0
+
+  // Готовые цвета с прозрачностью
+  // Primary colors with opacity
+  --color-primary-50-a10: rgba(254, 242, 242, 0.1)
+  --color-primary-50-a20: rgba(254, 242, 242, 0.2)
+  --color-primary-50-a30: rgba(254, 242, 242, 0.3)
+  --color-primary-50-a40: rgba(254, 242, 242, 0.4)
+  --color-primary-50-a50: rgba(254, 242, 242, 0.5)
+
+  --color-primary-500-a10: rgba(239, 68, 68, 0.1)
+  --color-primary-500-a20: rgba(239, 68, 68, 0.2)
+  --color-primary-500-a30: rgba(239, 68, 68, 0.3)
+  --color-primary-500-a40: rgba(239, 68, 68, 0.4)
+  --color-primary-500-a50: rgba(239, 68, 68, 0.5)
+  --color-primary-500-a60: rgba(239, 68, 68, 0.6)
+  --color-primary-500-a70: rgba(239, 68, 68, 0.7)
+  --color-primary-500-a80: rgba(239, 68, 68, 0.8)
+  --color-primary-500-a90: rgba(239, 68, 68, 0.9)
+
+  --color-primary-600-a10: rgba(220, 38, 38, 0.1)
+  --color-primary-600-a20: rgba(220, 38, 38, 0.2)
+  --color-primary-600-a30: rgba(220, 38, 38, 0.3)
+  --color-primary-600-a40: rgba(220, 38, 38, 0.4)
+  --color-primary-600-a50: rgba(220, 38, 38, 0.5)
+
+  // Accent colors with opacity
+  --color-accent-500-a10: rgba(14, 165, 233, 0.1)
+  --color-accent-500-a20: rgba(14, 165, 233, 0.2)
+  --color-accent-500-a30: rgba(14, 165, 233, 0.3)
+  --color-accent-500-a40: rgba(14, 165, 233, 0.4)
+  --color-accent-500-a50: rgba(14, 165, 233, 0.5)
+  --color-accent-500-a60: rgba(14, 165, 233, 0.6)
+  --color-accent-500-a70: rgba(14, 165, 233, 0.7)
+  --color-accent-500-a80: rgba(14, 165, 233, 0.8)
+  --color-accent-500-a90: rgba(14, 165, 233, 0.9)
+
+  --color-accent-600-a10: rgba(2, 132, 199, 0.1)
+  --color-accent-600-a20: rgba(2, 132, 199, 0.2)
+  --color-accent-600-a30: rgba(2, 132, 199, 0.3)
+  --color-accent-600-a40: rgba(2, 132, 199, 0.4)
+  --color-accent-600-a50: rgba(2, 132, 199, 0.5)
+
+  // Gray colors with opacity
+  --color-gray-500-a10: rgba(107, 114, 128, 0.1)
+  --color-gray-500-a20: rgba(107, 114, 128, 0.2)
+  --color-gray-500-a30: rgba(107, 114, 128, 0.3)
+  --color-gray-500-a40: rgba(107, 114, 128, 0.4)
+  --color-gray-500-a50: rgba(107, 114, 128, 0.5)
+
+  --color-gray-600-a10: rgba(75, 85, 99, 0.1)
+  --color-gray-600-a20: rgba(75, 85, 99, 0.2)
+  --color-gray-600-a30: rgba(75, 85, 99, 0.3)
+  --color-gray-600-a40: rgba(75, 85, 99, 0.4)
+  --color-gray-600-a50: rgba(75, 85, 99, 0.5)
+
+  --color-gray-700-a10: rgba(55, 65, 81, 0.1)
+  --color-gray-700-a20: rgba(55, 65, 81, 0.2)
+  --color-gray-700-a30: rgba(55, 65, 81, 0.3)
+  --color-gray-700-a40: rgba(55, 65, 81, 0.4)
+  --color-gray-700-a50: rgba(55, 65, 81, 0.5)
+  --color-gray-700-a60: rgba(55, 65, 81, 0.6)
+  --color-gray-700-a70: rgba(55, 65, 81, 0.7)
+  --color-gray-700-a80: rgba(55, 65, 81, 0.8)
+  --color-gray-700-a90: rgba(55, 65, 81, 0.9)
+
+  --color-gray-800-a10: rgba(31, 41, 55, 0.1)
+  --color-gray-800-a20: rgba(31, 41, 55, 0.2)
+  --color-gray-800-a30: rgba(31, 41, 55, 0.3)
+  --color-gray-800-a40: rgba(31, 41, 55, 0.4)
+  --color-gray-800-a50: rgba(31, 41, 55, 0.5)
+  --color-gray-800-a60: rgba(31, 41, 55, 0.6)
+  --color-gray-800-a70: rgba(31, 41, 55, 0.7)
+  --color-gray-800-a80: rgba(31, 41, 55, 0.8)
+  --color-gray-800-a90: rgba(31, 41, 55, 0.9)
+
+  --color-gray-900-a10: rgba(17, 24, 39, 0.1)
+  --color-gray-900-a20: rgba(17, 24, 39, 0.2)
+  --color-gray-900-a30: rgba(17, 24, 39, 0.3)
+  --color-gray-900-a40: rgba(17, 24, 39, 0.4)
+  --color-gray-900-a50: rgba(17, 24, 39, 0.5)
+  --color-gray-900-a60: rgba(17, 24, 39, 0.6)
+  --color-gray-900-a70: rgba(17, 24, 39, 0.7)
+  --color-gray-900-a80: rgba(17, 24, 39, 0.8)
+  --color-gray-900-a90: rgba(17, 24, 39, 0.9)
+
+  // White and black with opacity
+  --color-white-a10: rgba(255, 255, 255, 0.1)
+  --color-white-a20: rgba(255, 255, 255, 0.2)
+  --color-white-a30: rgba(255, 255, 255, 0.3)
+  --color-white-a40: rgba(255, 255, 255, 0.4)
+  --color-white-a50: rgba(255, 255, 255, 0.5)
+  --color-white-a60: rgba(255, 255, 255, 0.6)
+  --color-white-a70: rgba(255, 255, 255, 0.7)
+  --color-white-a80: rgba(255, 255, 255, 0.8)
+  --color-white-a90: rgba(255, 255, 255, 0.9)
+
+  --color-black-a10: rgba(0, 0, 0, 0.1)
+  --color-black-a20: rgba(0, 0, 0, 0.2)
+  --color-black-a30: rgba(0, 0, 0, 0.3)
+  --color-black-a40: rgba(0, 0, 0, 0.4)
+  --color-black-a50: rgba(0, 0, 0, 0.5)
+  --color-black-a60: rgba(0, 0, 0, 0.6)
+  --color-black-a70: rgba(0, 0, 0, 0.7)
+  --color-black-a80: rgba(0, 0, 0, 0.8)
+  --color-black-a90: rgba(0, 0, 0, 0.9)
+
+  // Семантические переменные с прозрачностью
+  --color-background-overlay: var(--color-black-a50)
+  --color-background-modal: var(--color-white-a95)
+  --color-background-tooltip: var(--color-gray-900-a90)
+  --color-background-hover: var(--color-gray-500-a10)
+  --color-background-active: var(--color-primary-500-a20)
+  --color-border-light: var(--color-gray-300-a30)
+  --color-border-medium: var(--color-gray-500-a40)
+  --color-text-secondary: var(--color-gray-600-a80)
+  --color-text-disabled: var(--color-gray-500-a50)
+  --color-shadow-light: var(--color-black-a10)
+  --color-shadow-medium: var(--color-black-a20)
+  --color-shadow-heavy: var(--color-black-a30)
+
+  // Переменные для темной темы с прозрачностью
+  --color-dark-background-overlay: var(--color-white-a10)
+  --color-dark-background-modal: var(--color-gray-900-a95)
+  --color-dark-background-tooltip: var(--color-white-a90)
+  --color-dark-background-hover: var(--color-white-a10)
+  --color-dark-background-active: var(--color-accent-500-a20)
+  --color-dark-border-light: var(--color-white-a20)
+  --color-dark-border-medium: var(--color-white-a30)
+
 // Миксины
 flex-center()
   display: flex
@@ -60,7 +240,7 @@ transition-all($duration = 0.3s)
 // Header
 .header
   background-color: $white
-  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)
+  box-shadow: 0 1px 3px 0 var(--color-shadow-light)
   position: sticky
   top: 0
   z-index: 50
@@ -82,8 +262,8 @@ transition-all($duration = 0.3s)
   gap: 0.75rem
 
 .header__logo
-  width: auto
-  height: 5rem
+  width: 2rem
+  height: 2rem
   object-fit: contain
 
 .header__nav-name
@@ -127,14 +307,14 @@ transition-all($duration = 0.3s)
   cursor: pointer
 
   &:hover
-    background-color: $gray-200
+    background-color: var(--color-background-hover)
 
   .dark &
     background-color: $gray-700
     color: $gray-300
 
     &:hover
-      background-color: $gray-600
+      background-color: var(--color-dark-background-hover)
 
 // Main content
 .main
@@ -202,9 +382,9 @@ transition-all($duration = 0.3s)
   &:hover
     color: $white
 
-// Утилитарные классы
+// Утилитарные классы с использованием CSS-переменных
 .btn-primary
-  background-color: $primary-500
+  background-color: var(--color-primary-600)
   color: $white
   padding: 0.5rem 1rem
   border-radius: 0.5rem
@@ -217,11 +397,11 @@ transition-all($duration = 0.3s)
   text-align: center
 
   &:hover
-    background-color: $primary-600
+    background-color: var(--color-primary-700)
 
 .btn-secondary
-  background-color: $gray-200
-  color: $gray-700
+  background-color: var(--color-gray-200)
+  color: var(--color-gray-700)
   padding: 0.5rem 1rem
   border-radius: 0.5rem
   font-weight: 500
@@ -233,19 +413,19 @@ transition-all($duration = 0.3s)
   text-align: center
 
   &:hover
-    background-color: $gray-300
+    background-color: var(--color-gray-300)
 
   .dark &
-    background-color: $gray-700
-    color: $gray-300
+    background-color: var(--color-gray-700)
+    color: var(--color-gray-300)
 
     &:hover
-      background-color: $gray-600
+      background-color: var(--color-gray-600)
 
 .card
   background-color: $white
   border-radius: 0.5rem
-  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)
+  box-shadow: 0 1px 3px 0 var(--color-shadow-light)
   padding: 1.5rem
 
   .dark &
@@ -254,7 +434,7 @@ transition-all($duration = 0.3s)
 .form-input
   width: 100%
   padding: 0.5rem 0.75rem
-  border: 1px solid $gray-300
+  border: 1px solid var(--color-border-light)
   border-radius: 0.5rem
   background-color: $white
   color: $gray-900
@@ -264,12 +444,12 @@ transition-all($duration = 0.3s)
   &:focus
     outline: none
     border-color: $primary-500
-    box-shadow: 0 0 0 2px rgba($primary-500, 0.2)
+    box-shadow: 0 0 0 2px var(--color-primary-500-a20)
 
   .dark &
     background-color: $gray-700
     color: $white
-    border-color: $gray-600
+    border-color: var(--color-dark-border-light)
 
     &:focus
       border-color: $primary-500
@@ -284,92 +464,43 @@ transition-all($duration = 0.3s)
   .dark &
     color: $gray-300
 
-// Admin panel overrides
-.admin
-  .header
-    position: static
-    box-shadow: none
-    border-bottom: 1px solid $gray-200
-
-    .dark &
-      border-bottom-color: $gray-700
-
-// Responsive design
-@media (max-width: 768px)
-  .header__nav
-    padding: 0.5rem
-
-  .header__nav-block
-    flex-direction: column
-    gap: 1rem
-
-  .header__nav-menu
-    width: 100%
-    justify-content: center
-
-  .header__brand
-    justify-content: center
-
-  .footer__sections
-    grid-template-columns: 1fr
-    gap: 1.5rem
-
-  .footer__content
-    padding: 0 0.5rem
-
-@media (max-width: 480px)
-  .header__nav-menu
-    flex-wrap: wrap
-    gap: 0.5rem
+// Специальные классы для работы с прозрачностью
+.overlay
+  background-color: var(--color-background-overlay)
 
-  .header__menu-link
-    font-size: 0.875rem
+.modal-backdrop
+  background-color: var(--color-background-modal)
 
-  .header__logo
-    width: 1.5rem
-    height: 1.5rem
-
-  .header__nav-name
-    font-size: 1.25rem
-
-// Print styles
-@media print
-  .header,
-  .footer
-    display: none
-
-  #app
-    background-color: $white
-
-// High contrast mode support
-@media (prefers-contrast: high)
-  .header__menu-link
-    color: $black
+.tooltip
+  background-color: var(--color-background-tooltip)
+  color: $white
+  padding: 0.5rem 0.75rem
+  border-radius: 0.375rem
+  font-size: 0.875rem
 
-    .dark &
-      color: $white
+.hover-effect
+  transition: background-color 0.2s ease-in-out
 
-  .btn-primary
-    background-color: $black
-    color: $white
+  &:hover
+    background-color: var(--color-background-hover)
 
-    .dark &
-      background-color: $white
-      color: $black
+.active-state
+  background-color: var(--color-background-active)
 
-// Reduced motion support
-@media (prefers-reduced-motion: reduce)
-  .page-slide
-    &-enter-active,
-    &-leave-active
-      transition: none
+.border-transparent
+  border-color: var(--color-border-light)
 
-  .header__menu-link,
-  .btn-primary,
-  .btn-secondary
-    transition: none
+.text-muted
+  color: var(--color-text-secondary)
 
 // Темная тема
 @media (prefers-color-scheme: dark)
   :root
     color-scheme: dark
+
+// Утилиты для отладки
+.debug-border
+  border: 1px solid var(--color-primary-500-a50)
+
+.debug-bg
+  background-color: var(--color-accent-500-a20)

+ 371 - 20
app/pages/Admin/Products/index.coffee

@@ -1,4 +1,4 @@
-document.head.insertAdjacentHTML('beforeend','<style type="text/css">'+stylFns['app/pages/Admin/Products/index.styl']+'</style>')
+document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/pages/Admin/Products/index.styl']+'</style>')
 
 PouchDB = require 'app/utils/pouch'
 Papa = require 'papaparse'
@@ -17,10 +17,43 @@ module.exports =
       selectedStatus: ''
       showProductModal: false
       showImportModal: false
+      showCategoriesModal: false
+      showCategoryModal: false
       editingProduct: null
+      editingCategory: null
       selectedFile: null
+      selectedCategoriesFile: null
       importing: false
+      importingCategories: false
       importResults: null
+      categoriesImportResults: null
+      availableDomains: []
+      categoriesActiveTab: 'list'
+      
+      productForm: {
+        name: ''
+        sku: ''
+        category: ''
+        price: 0
+        oldPrice: 0
+        brand: ''
+        description: ''
+        image: ''
+        active: true
+        domains: []
+      }
+      
+      categoryForm: {
+        name: ''
+        slug: ''
+        description: ''
+        parentCategory: ''
+        sortOrder: 0
+        image: ''
+        icon: ''
+        active: true
+        domains: []
+      }
     }
   
   computed:
@@ -63,41 +96,157 @@ module.exports =
         .catch (error) =>
           console.error 'Ошибка загрузки категорий:', error
     
+    loadDomains: ->
+      PouchDB.queryView('admin', 'domain_settings', { include_docs: true })
+        .then (result) =>
+          @availableDomains = result.rows.map (row) -> row.doc
+        .catch (error) =>
+          console.error 'Ошибка загрузки доменов:', error
+    
     getCategoryName: (categoryId) ->
       category = @categories.find (cat) -> cat._id == categoryId
       category?.name || 'Без категории'
     
+    getCategoryProductCount: (categoryId) ->
+      @products.filter((product) -> product.category == categoryId).length
+    
+    # Управление товарами
     editProduct: (product) ->
       @editingProduct = product
+      @productForm = {
+        name: product.name || ''
+        sku: product.sku || ''
+        category: product.category || ''
+        price: product.price || 0
+        oldPrice: product.oldPrice || 0
+        brand: product.brand || ''
+        description: product.description || ''
+        image: product.image || ''
+        active: product.active != false
+        domains: product.domains || []
+      }
       @showProductModal = true
     
-    toggleProductStatus: (product) ->
-      updatedProduct = {
-        ...product
-        active: !product.active
+    saveProduct: ->
+      if !@productForm.name || !@productForm.sku || !@productForm.price
+        @showNotification 'Заполните обязательные поля (Название, Артикул, Цена)', 'error'
+        return
+      
+      productData = {
+        type: 'product'
+        ...@productForm
         updatedAt: new Date().toISOString()
       }
       
-      PouchDB.saveToRemote(updatedProduct)
+      if @editingProduct
+        productData._id = @editingProduct._id
+        productData._rev = @editingProduct._rev
+        productData.createdAt = @editingProduct.createdAt
+      else
+        productData._id = "product:#{Date.now()}"
+        productData.createdAt = new Date().toISOString()
+      
+      PouchDB.saveToRemote(productData)
         .then (result) =>
+          @showProductModal = false
+          @resetProductForm()
           @loadProducts()
-          @showNotification 'Статус товара обновлен'
+          @showNotification 'Товар успешно сохранен'
         .catch (error) =>
-          console.error 'Ошибка обновления статуса:', error
-          @showNotification 'Ошибка обновления статуса', 'error'
+          console.error 'Ошибка сохранения товара:', error
+          @showNotification 'Ошибка сохранения товара', 'error'
     
-    deleteProduct: (productId) ->
-      if confirm('Вы уверены, что хотите удалить этот товар?')
-        PouchDB.getDocument(productId)
+    removeProductImage: ->
+      @productForm.image = ''
+    
+    onProductImageUpload: (event) ->
+      file = event.target.files[0]
+      if file
+        reader = new FileReader()
+        reader.onload = (e) =>
+          @productForm.image = e.target.result
+        reader.readAsDataURL(file)
+    
+    # Управление категориями
+    editCategory: (category) ->
+      @editingCategory = category
+      @categoryForm = {
+        name: category.name || ''
+        slug: category.slug || ''
+        description: category.description || ''
+        parentCategory: category.parentCategory || ''
+        sortOrder: category.sortOrder || 0
+        image: category.image || ''
+        icon: category.icon || ''
+        active: category.active != false
+        domains: category.domains || []
+      }
+      @showCategoryModal = true
+    
+    saveCategory: ->
+      if !@categoryForm.name || !@categoryForm.slug
+        @showNotification 'Заполните обязательные поля (Название, URL slug)', 'error'
+        return
+      
+      categoryData = {
+        type: 'category'
+        ...@categoryForm
+        updatedAt: new Date().toISOString()
+      }
+      
+      if @editingCategory
+        categoryData._id = @editingCategory._id
+        categoryData._rev = @editingCategory._rev
+        categoryData.createdAt = @editingCategory.createdAt
+      else
+        categoryData._id = "category:#{Date.now()}"
+        categoryData.createdAt = new Date().toISOString()
+      
+      PouchDB.saveToRemote(categoryData)
+        .then (result) =>
+          @showCategoryModal = false
+          @resetCategoryForm()
+          @loadCategories()
+          @showNotification 'Категория успешно сохранена'
+        .catch (error) =>
+          console.error 'Ошибка сохранения категории:', error
+          @showNotification 'Ошибка сохранения категории', 'error'
+    
+    removeCategoryImage: ->
+      @categoryForm.image = ''
+    
+    removeCategoryIcon: ->
+      @categoryForm.icon = ''
+    
+    onCategoryImageUpload: (event) ->
+      file = event.target.files[0]
+      if file
+        reader = new FileReader()
+        reader.onload = (e) =>
+          @categoryForm.image = e.target.result
+        reader.readAsDataURL(file)
+    
+    onCategoryIconUpload: (event) ->
+      file = event.target.files[0]
+      if file
+        reader = new FileReader()
+        reader.onload = (e) =>
+          @categoryForm.icon = e.target.result
+        reader.readAsDataURL(file)
+    
+    deleteCategory: (categoryId) ->
+      if confirm('Вы уверены, что хотите удалить эту категорию?')
+        PouchDB.getDocument(categoryId)
           .then (doc) ->
             PouchDB.saveToRemote({ ...doc, _deleted: true })
           .then (result) =>
-            @loadProducts()
-            @showNotification 'Товар удален'
+            @loadCategories()
+            @showNotification 'Категория удалена'
           .catch (error) =>
-            console.error 'Ошибка удаления товара:', error
-            @showNotification 'Ошибка удаления товара', 'error'
+            console.error 'Ошибка удаления категории:', error
+            @showNotification 'Ошибка удаления категории', 'error'
     
+    # Импорт товаров
     onFileSelect: (event) ->
       @selectedFile = event.target.files[0]
       @importResults = null
@@ -129,17 +278,32 @@ module.exports =
           # Пакетное сохранение
           PouchDB.bulkDocs(couchProducts)
             .then (result) =>
-              @importResults = { success: true, processed: couchProducts.length }
+              @importResults = { 
+                success: true, 
+                processed: couchProducts.length,
+                errors: []
+              }
               @importing = false
               @loadProducts()
+              @loadCategories() # Перезагружаем категории, т.к. могли добавиться новые
               @showNotification "Импортировано #{couchProducts.length} товаров"
             .catch (error) =>
-              @importResults = { success: false, error: error.message, processed: 0 }
+              @importResults = { 
+                success: false, 
+                error: error.message, 
+                processed: 0,
+                errors: [error.message]
+              }
               @importing = false
               @showNotification "Ошибка импорта: #{error.message}", 'error'
         
         catch error
-          @importResults = { success: false, error: error.message, processed: 0 }
+          @importResults = { 
+            success: false, 
+            error: error.message, 
+            processed: 0,
+            errors: [error.message]
+          }
           @importing = false
           @showNotification "Ошибка обработки файла: #{error.message}", 'error'
       
@@ -158,6 +322,33 @@ module.exports =
         updatedAt: new Date().toISOString()
       }
       
+      # Обработка категории из поля "Тип*"
+      if product['Тип*']
+        categoryName = product['Тип*'].trim()
+        # Ищем существующую категорию
+        existingCategory = @categories.find (cat) -> 
+          cat.name?.toLowerCase() == categoryName.toLowerCase()
+        
+        if existingCategory
+          productData.category = existingCategory._id
+        else
+          # Создаем новую категорию
+          categoryId = "category:#{Date.now()}-#{index}"
+          newCategory = {
+            _id: categoryId
+            type: 'category'
+            name: categoryName
+            slug: @generateSlug(categoryName)
+            sortOrder: @categories.length
+            active: true
+            createdAt: new Date().toISOString()
+            updatedAt: new Date().toISOString()
+            domains: @availableDomains?.map((d) -> d.domain) || []
+          }
+          # Сохраняем новую категорию
+          PouchDB.saveToRemote(newCategory)
+          productData.category = categoryId
+      
       # Дополнительные поля
       if product['Цена до скидки, руб.']
         productData.oldPrice = parseFloat(product['Цена до скидки, руб.'].replace(/\s/g, '').replace(',', '.'))
@@ -181,15 +372,174 @@ module.exports =
       else
         productData.description = product['Аннотация'] || ''
       
-      # Домены (все товары доступны на всех доменах по умолчанию)
+      # Домены
       productData.domains = @availableDomains?.map((d) -> d.domain) || []
       
       return productData
     
+    # Импорт категорий
+    onCategoriesFileSelect: (event) ->
+      @selectedCategoriesFile = event.target.files[0]
+      @categoriesImportResults = null
+    
+    importCategories: ->
+      if !@selectedCategoriesFile
+        @showNotification 'Выберите файл категорий для импорта', 'error'
+        return
+      
+      @importingCategories = true
+      @categoriesImportResults = null
+      
+      reader = new FileReader()
+      reader.onload = (e) =>
+        try
+          results = Papa.parse e.target.result, {
+            header: true
+            delimiter: ','
+            skipEmptyLines: true
+            encoding: 'UTF-8'
+          }
+          
+          categories = results.data.filter (row) => 
+            row && row.name && row.slug
+          
+          couchCategories = categories.map (category, index) =>
+            @transformCategoryData(category, index)
+          
+          # Пакетное сохранение категорий
+          PouchDB.bulkDocs(couchCategories)
+            .then (result) =>
+              @categoriesImportResults = { 
+                success: true, 
+                processed: couchCategories.length,
+                errors: []
+              }
+              @importingCategories = false
+              @loadCategories()
+              @showNotification "Импортировано #{couchCategories.length} категорий"
+            .catch (error) =>
+              @categoriesImportResults = { 
+                success: false, 
+                error: error.message, 
+                processed: 0,
+                errors: [error.message]
+              }
+              @importingCategories = false
+              @showNotification "Ошибка импорта категорий: #{error.message}", 'error'
+        
+        catch error
+          @categoriesImportResults = { 
+            success: false, 
+            error: error.message, 
+            processed: 0,
+            errors: [error.message]
+          }
+          @importingCategories = false
+          @showNotification "Ошибка обработки файла категорий: #{error.message}", 'error'
+      
+      reader.readAsText(@selectedCategoriesFile, 'UTF-8')
+    
+    transformCategoryData: (category, index) ->
+      categoryData = {
+        _id: "category:import-#{Date.now()}-#{index}"
+        type: 'category'
+        name: category.name
+        slug: category.slug
+        description: category.description || ''
+        parentCategory: category.parentCategory || ''
+        sortOrder: parseInt(category.sortOrder) || @categories.length + index
+        active: category.active != 'false'
+        createdAt: new Date().toISOString()
+        updatedAt: new Date().toISOString()
+        domains: @availableDomains?.map((d) -> d.domain) || []
+      }
+      
+      if category.image
+        categoryData.image = category.image
+      
+      if category.icon
+        categoryData.icon = category.icon
+      
+      return categoryData
+    
+    # Вспомогательные методы
+    generateSlug: (text) ->
+      text.toLowerCase()
+        .replace(/\s+/g, '-')
+        .replace(/[^\w\-]+/g, '')
+        .replace(/\-\-+/g, '-')
+        .replace(/^-+/, '')
+        .replace(/-+$/, '')
+    
     richContentToMarkdown: (richContent) ->
       # Простое преобразование rich content в markdown
       return JSON.stringify(richContent) # Временная реализация
     
+    toggleProductStatus: (product) ->
+      updatedProduct = {
+        ...product
+        active: !product.active
+        updatedAt: new Date().toISOString()
+      }
+      
+      PouchDB.saveToRemote(updatedProduct)
+        .then (result) =>
+          @loadProducts()
+          @showNotification 'Статус товара обновлен'
+        .catch (error) =>
+          console.error 'Ошибка обновления статуса:', error
+          @showNotification 'Ошибка обновления статуса', 'error'
+    
+    deleteProduct: (productId) ->
+      if confirm('Вы уверены, что хотите удалить этот товар?')
+        PouchDB.getDocument(productId)
+          .then (doc) ->
+            PouchDB.saveToRemote({ ...doc, _deleted: true })
+          .then (result) =>
+            @loadProducts()
+            @showNotification 'Товар удален'
+          .catch (error) =>
+            console.error 'Ошибка удаления товара:', error
+            @showNotification 'Ошибка удаления товара', 'error'
+    
+    resetProductForm: ->
+      @editingProduct = null
+      @productForm = {
+        name: ''
+        sku: ''
+        category: ''
+        price: 0
+        oldPrice: 0
+        brand: ''
+        description: ''
+        image: ''
+        active: true
+        domains: []
+      }
+    
+    resetCategoryForm: ->
+      @editingCategory = null
+      @categoryForm = {
+        name: ''
+        slug: ''
+        description: ''
+        parentCategory: ''
+        sortOrder: 0
+        image: ''
+        icon: ''
+        active: true
+        domains: []
+      }
+    
+    getCategoriesTabClass: (tabId) ->
+      baseClass = 'admin-products__categories-tab'
+      isActive = @categoriesActiveTab == tabId
+      
+      if isActive
+        return "#{baseClass} admin-products__categories-tab--active"
+      else
+        return baseClass
+    
     formatPrice: (price) ->
       return '0 ₽' if !price
       new Intl.NumberFormat('ru-RU', {
@@ -211,3 +561,4 @@ module.exports =
   mounted: ->
     @loadProducts()
     @loadCategories()
+    @loadDomains()

+ 364 - 5
app/pages/Admin/Products/index.pug

@@ -10,7 +10,11 @@ div(class="admin-products")
         @click="showImportModal = true"
         class="admin-products__btn admin-products__btn--secondary"
       ) Импорт из CSV
-  
+      button(
+        @click="showCategoriesModal = true"
+        class="admin-products__btn admin-products__btn--secondary"
+      ) Управление категориями
+
   div(class="admin-products__content")
     div(class="admin-products__filters")
       div(class="admin-products__filter-group")
@@ -93,13 +97,140 @@ div(class="admin-products")
                     @click="deleteProduct(product._id)"
                     class="admin-products__action-btn admin-products__action-btn--delete"
                   ) Удалить
-  
-  // Модальное окно товара
+
+  // Модальное окно редактирования товара
   div(v-if="showProductModal" class="admin-products__modal")
     div(class="admin-products__modal-content")
       h3(class="admin-products__modal-title") {{ editingProduct ? 'Редактирование' : 'Добавление' }} товара
-      // Форма будет реализована в следующем шаге
-  
+      
+      div(class="admin-products__modal-form")
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Название товара *
+          input(
+            v-model="productForm.name"
+            type="text"
+            class="admin-products__input"
+            placeholder="Введите название товара"
+            required
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Артикул *
+          input(
+            v-model="productForm.sku"
+            type="text"
+            class="admin-products__input"
+            placeholder="Артикул товара"
+            required
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Категория
+          select(v-model="productForm.category" class="admin-products__select")
+            option(value="") Выберите категорию
+            option(
+              v-for="category in categories"
+              :key="category._id"
+              :value="category._id"
+            ) {{ category.name }}
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Цена *
+          input(
+            v-model="productForm.price"
+            type="number"
+            class="admin-products__input"
+            placeholder="0.00"
+            min="0"
+            step="0.01"
+            required
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Старая цена
+          input(
+            v-model="productForm.oldPrice"
+            type="number"
+            class="admin-products__input"
+            placeholder="0.00"
+            min="0"
+            step="0.01"
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Бренд
+          input(
+            v-model="productForm.brand"
+            type="text"
+            class="admin-products__input"
+            placeholder="Бренд производителя"
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Описание
+          textarea(
+            v-model="productForm.description"
+            class="admin-products__textarea"
+            placeholder="Описание товара"
+            rows="4"
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Изображение товара
+          div(class="admin-products__image-upload")
+            div(v-if="productForm.image" class="admin-products__image-preview")
+              img(:src="productForm.image" :alt="productForm.name" class="admin-products__preview-image")
+              button(
+                @click="removeProductImage"
+                class="admin-products__btn admin-products__btn--danger"
+              ) Удалить
+            div(v-else class="admin-products__image-placeholder") Изображение не загружено
+            
+            input(
+              type="file"
+              ref="productImageInput"
+              @change="onProductImageUpload"
+              accept="image/*"
+              class="admin-products__file-input"
+              id="product-image-upload"
+            )
+            label(for="product-image-upload" class="admin-products__btn admin-products__btn--secondary") Выбрать изображение
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__checkbox-label")
+            input(
+              v-model="productForm.active"
+              type="checkbox"
+              class="admin-products__checkbox"
+            )
+            span Активный товар
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Домены
+          div(class="admin-products__domains-list")
+            label(
+              v-for="domain in availableDomains"
+              :key="domain._id"
+              class="admin-products__domain-label"
+            )
+              input(
+                type="checkbox"
+                :value="domain.domain"
+                v-model="productForm.domains"
+                class="admin-products__domain-checkbox"
+              )
+              span {{ domain.domain }}
+      
+      div(class="admin-products__modal-actions")
+        button(
+          @click="saveProduct"
+          class="admin-products__btn admin-products__btn--primary"
+        ) {{ editingProduct ? 'Обновить' : 'Создать' }}
+        button(
+          @click="showProductModal = false"
+          class="admin-products__btn admin-products__btn--secondary"
+        ) Отмена
+
   // Модальное окно импорта
   div(v-if="showImportModal" class="admin-products__modal")
     div(class="admin-products__modal-content")
@@ -114,6 +245,7 @@ div(class="admin-products")
             accept=".csv"
             class="admin-products__file-input"
           )
+          p(class="admin-products__help-text") Поддерживается формат CSV с разделителем ; и кодировкой UTF-8
         
         div(v-if="selectedFile" class="admin-products__file-info")
           p Выбран файл: {{ selectedFile.name }}
@@ -127,6 +259,10 @@ div(class="admin-products")
           h4(v-if="importResults.success" class="admin-products__success") Импорт успешно завершен!
           h4(v-else class="admin-products__error") Ошибка импорта
           p Обработано товаров: {{ importResults.processed }}
+          p(v-if="importResults.errors && importResults.errors.length")
+            strong Ошибки:
+            ul
+              li(v-for="error in importResults.errors" :key="error") {{ error }}
           p(v-if="importResults.error") Ошибка: {{ importResults.error }}
       
       div(class="admin-products__modal-actions")
@@ -134,3 +270,226 @@ div(class="admin-products")
           @click="showImportModal = false"
           class="admin-products__btn admin-products__btn--secondary"
         ) Закрыть
+
+  // Модальное окно управления категориями
+  div(v-if="showCategoriesModal" class="admin-products__modal")
+    div(class="admin-products__modal-content admin-products__modal-content--large")
+      h3(class="admin-products__modal-title") Управление категориями
+      
+      div(class="admin-products__categories-tabs")
+        button(
+          @click="categoriesActiveTab = 'list'"
+          :class="getCategoriesTabClass('list')"
+        ) Список категорий
+        button(
+          @click="categoriesActiveTab = 'import'"
+          :class="getCategoriesTabClass('import')"
+        ) Импорт категорий
+      
+      // Список категорий
+      div(v-if="categoriesActiveTab === 'list'" class="admin-products__categories-list")
+        div(class="admin-products__categories-header")
+          button(
+            @click="showCategoryModal = true"
+            class="admin-products__btn admin-products__btn--primary"
+          ) Добавить категорию
+        
+        div(class="admin-products__categories-grid")
+          div(
+            v-for="category in categories"
+            :key="category._id"
+            class="admin-products__category-item"
+          )
+            div(class="admin-products__category-preview")
+              img(
+                v-if="category.image"
+                :src="category.image"
+                :alt="category.name"
+                class="admin-products__category-image"
+              )
+              div(v-else class="admin-products__category-no-image") Нет изображения
+              div(class="admin-products__category-info")
+                h4(class="admin-products__category-name") {{ category.name }}
+                p(class="admin-products__category-description") {{ category.description || 'Без описания' }}
+                div(class="admin-products__category-meta")
+                  span(class="admin-products__category-products") Товаров: {{ getCategoryProductCount(category._id) }}
+                  span(
+                    :class="getStatusClass(category.active)"
+                  ) {{ category.active ? 'Активна' : 'Неактивна' }}
+            
+            div(class="admin-products__category-actions")
+              button(
+                @click="editCategory(category)"
+                class="admin-products__btn admin-products__btn--edit"
+              ) Редактировать
+              button(
+                @click="deleteCategory(category._id)"
+                class="admin-products__btn admin-products__btn--delete"
+              ) Удалить
+      
+      // Импорт категорий
+      div(v-if="categoriesActiveTab === 'import'" class="admin-products__categories-import")
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Выберите CSV файл категорий
+          input(
+            type="file"
+            @change="onCategoriesFileSelect"
+            accept=".csv"
+            class="admin-products__file-input"
+          )
+          p(class="admin-products__help-text") Формат CSV с полями: name, slug, description, parentCategory, sortOrder, active
+        
+        div(v-if="selectedCategoriesFile" class="admin-products__file-info")
+          p Выбран файл: {{ selectedCategoriesFile.name }}
+          button(
+            @click="importCategories"
+            :disabled="importingCategories"
+            class="admin-products__btn admin-products__btn--primary"
+          ) {{ importingCategories ? 'Импорт...' : 'Импорт категорий' }}
+        
+        div(v-if="categoriesImportResults" class="admin-products__import-results")
+          h4(v-if="categoriesImportResults.success" class="admin-products__success") Импорт категорий завершен!
+          h4(v-else class="admin-products__error") Ошибка импорта категорий
+          p Обработано категорий: {{ categoriesImportResults.processed }}
+          p(v-if="categoriesImportResults.errors && categoriesImportResults.errors.length")
+            strong Ошибки:
+            ul
+              li(v-for="error in categoriesImportResults.errors" :key="error") {{ error }}
+      
+      div(class="admin-products__modal-actions")
+        button(
+          @click="showCategoriesModal = false"
+          class="admin-products__btn admin-products__btn--secondary"
+        ) Закрыть
+
+  // Модальное окно редактирования категории
+  div(v-if="showCategoryModal" class="admin-products__modal")
+    div(class="admin-products__modal-content")
+      h3(class="admin-products__modal-title") {{ editingCategory ? 'Редактирование' : 'Добавление' }} категории
+      
+      div(class="admin-products__modal-form")
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Название категории *
+          input(
+            v-model="categoryForm.name"
+            type="text"
+            class="admin-products__input"
+            placeholder="Введите название категории"
+            required
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") URL slug *
+          input(
+            v-model="categoryForm.slug"
+            type="text"
+            class="admin-products__input"
+            placeholder="url-slug"
+            required
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Описание
+          textarea(
+            v-model="categoryForm.description"
+            class="admin-products__textarea"
+            placeholder="Описание категории"
+            rows="3"
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Родительская категория
+          select(v-model="categoryForm.parentCategory" class="admin-products__select")
+            option(value="") Без родительской категории
+            option(
+              v-for="cat in categories.filter(c => c._id !== editingCategory?._id)"
+              :key="cat._id"
+              :value="cat._id"
+            ) {{ cat.name }}
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Порядок сортировки
+          input(
+            v-model="categoryForm.sortOrder"
+            type="number"
+            class="admin-products__input"
+            placeholder="0"
+            min="0"
+          )
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Изображение категории
+          div(class="admin-products__image-upload")
+            div(v-if="categoryForm.image" class="admin-products__image-preview")
+              img(:src="categoryForm.image" :alt="categoryForm.name" class="admin-products__preview-image")
+              button(
+                @click="removeCategoryImage"
+                class="admin-products__btn admin-products__btn--danger"
+              ) Удалить
+            div(v-else class="admin-products__image-placeholder") Изображение не загружено
+            
+            input(
+              type="file"
+              ref="categoryImageInput"
+              @change="onCategoryImageUpload"
+              accept="image/*"
+              class="admin-products__file-input"
+              id="category-image-upload"
+            )
+            label(for="category-image-upload" class="admin-products__btn admin-products__btn--secondary") Выбрать изображение
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Иконка для меню
+          div(class="admin-products__image-upload")
+            div(v-if="categoryForm.icon" class="admin-products__image-preview admin-products__image-preview--small")
+              img(:src="categoryForm.icon" :alt="categoryForm.name" class="admin-products__preview-image")
+              button(
+                @click="removeCategoryIcon"
+                class="admin-products__btn admin-products__btn--danger"
+              ) Удалить
+            div(v-else class="admin-products__image-placeholder") Иконка не загружена
+            
+            input(
+              type="file"
+              ref="categoryIconInput"
+              @change="onCategoryIconUpload"
+              accept="image/*"
+              class="admin-products__file-input"
+              id="category-icon-upload"
+            )
+            label(for="category-icon-upload" class="admin-products__btn admin-products__btn--secondary") Выбрать иконку
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__checkbox-label")
+            input(
+              v-model="categoryForm.active"
+              type="checkbox"
+              class="admin-products__checkbox"
+            )
+            span Активная категория
+        
+        div(class="admin-products__form-group")
+          label(class="admin-products__label") Домены
+          div(class="admin-products__domains-list")
+            label(
+              v-for="domain in availableDomains"
+              :key="domain._id"
+              class="admin-products__domain-label"
+            )
+              input(
+                type="checkbox"
+                :value="domain.domain"
+                v-model="categoryForm.domains"
+                class="admin-products__domain-checkbox"
+              )
+              span {{ domain.domain }}
+      
+      div(class="admin-products__modal-actions")
+        button(
+          @click="saveCategory"
+          class="admin-products__btn admin-products__btn--primary"
+        ) {{ editingCategory ? 'Обновить' : 'Создать' }}
+        button(
+          @click="showCategoryModal = false"
+          class="admin-products__btn admin-products__btn--secondary"
+        ) Отмена

+ 359 - 115
app/pages/Admin/Products/index.styl

@@ -8,19 +8,19 @@
   display: flex
   justify-content: space-between
   align-items: center
-  border-bottom: 1px solid $gray-200
+  border-bottom: 1px solid var(--color-gray-200)
   padding-bottom: 1.5rem
 
   .dark &
-    border-bottom-color: $gray-700
+    border-bottom-color: var(--color-gray-700)
 
 .admin-products__title
   font-size: 1.875rem
   font-weight: bold
-  color: $gray-900
+  color: var(--color-gray-900)
 
   .dark &
-    color: $white
+    color: var(--color-white)
 
 .admin-products__actions
   display: flex
@@ -40,36 +40,61 @@
   align-items: center
   justify-content: center
 
+  &:focus
+    outline: none
+    box-shadow: 0 0 0 2px var(--color-primary-500-a20)
+
 .admin-products__btn--primary
-  background-color: $primary-600
-  color: $white
+  background-color: var(--color-primary-600)
+  color: var(--color-white)
 
   &:hover
-    background-color: $primary-700
+    background-color: var(--color-primary-700)
 
 .admin-products__btn--secondary
-  background-color: $gray-200
-  color: $gray-700
+  background-color: var(--color-gray-200)
+  color: var(--color-gray-700)
 
   &:hover
-    background-color: $gray-300
+    background-color: var(--color-gray-300)
 
   .dark &
-    background-color: $gray-700
-    color: $gray-300
+    background-color: var(--color-gray-700)
+    color: var(--color-gray-300)
 
     &:hover
-      background-color: $gray-600
+      background-color: var(--color-gray-600)
+
+.admin-products__btn--danger
+  background-color: #dc2626
+  color: var(--color-white)
+
+  &:hover
+    background-color: #b91c1c
+
+.admin-products__btn--edit
+  background-color: #dbeafe
+  color: #1e40af
+
+  &:hover
+    background-color: #bfdbfe
+
+  .dark &
+    background-color: #1e3a8a
+    color: #93c5fd
+
+    &:hover
+      background-color: #1e40af
 
 // Content area
 .admin-products__content
-  background-color: $white
+  background-color: var(--color-white)
   border-radius: 0.5rem
-  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)
+  box-shadow: 0 1px 3px 0 var(--color-shadow-light)
   padding: 1.5rem
 
   .dark &
-    background-color: $gray-800
+    background-color: var(--color-gray-800)
 
 // Filters
 .admin-products__filters
@@ -90,58 +115,41 @@
   display: block
   font-size: 0.875rem
   font-weight: 500
-  color: $gray-700
+  color: var(--color-gray-700)
 
   .dark &
-    color: $gray-300
+    color: var(--color-gray-300)
 
 .admin-products__input
   width: 100%
   padding: 0.5rem 0.75rem
-  border: 1px solid $gray-300
+  border: 1px solid var(--color-gray-300)
   border-radius: 0.5rem
-  background-color: $white
-  color: $gray-900
+  background-color: var(--color-white)
+  color: var(--color-gray-900)
   font-size: 0.875rem
   transition: all 0.2s ease-in-out
 
   &:focus
     outline: none
-    border-color: $primary-500
-    box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2)
+    border-color: var(--color-primary-500)
+    box-shadow: 0 0 0 2px var(--color-primary-500-a20)
 
   .dark &
-    background-color: $gray-700
-    color: $white
-    border-color: $gray-600
+    background-color: var(--color-gray-700)
+    color: var(--color-white)
+    border-color: var(--color-gray-600)
 
     &:focus
-      border-color: $primary-500
-      box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2)
+      border-color: var(--color-primary-500)
 
 .admin-products__select
-  width: 100%
-  padding: 0.5rem 0.75rem
-  border: 1px solid $gray-300
-  border-radius: 0.5rem
-  background-color: $white
-  color: $gray-900
-  font-size: 0.875rem
-  transition: all 0.2s ease-in-out
-
-  &:focus
-    outline: none
-    border-color: $primary-500
-    box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2)
+  @extend .admin-products__input
 
-  .dark &
-    background-color: $gray-700
-    color: $white
-    border-color: $gray-600
-
-    &:focus
-      border-color: $primary-500
-      box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2)
+.admin-products__textarea
+  @extend .admin-products__input
+  resize: vertical
+  min-height: 5rem
 
 // Table styles
 .admin-products__table-container
@@ -150,41 +158,41 @@
 .admin-products__table
   width: 100%
   border-collapse: collapse
-  background-color: $white
+  background-color: var(--color-white)
 
   .dark &
-    background-color: $gray-800
+    background-color: var(--color-gray-800)
 
 .admin-products__th
-  background-color: $gray-50
+  background-color: var(--color-gray-50)
   padding: 0.75rem 1rem
   text-align: left
   font-size: 0.75rem
   font-weight: 500
-  color: $gray-500
+  color: var(--color-gray-500)
   text-transform: uppercase
   letter-spacing: 0.05em
-  border-bottom: 1px solid $gray-200
+  border-bottom: 1px solid var(--color-gray-200)
 
   .dark &
-    background-color: $gray-700
-    color: $gray-300
-    border-bottom-color: $gray-600
+    background-color: var(--color-gray-700)
+    color: var(--color-gray-300)
+    border-bottom-color: var(--color-gray-600)
 
 .admin-products__tr
   &:hover
-    background-color: $gray-50
+    background-color: var(--color-background-hover)
 
     .dark &
-      background-color: $gray-700
+      background-color: var(--color-dark-background-hover)
 
 .admin-products__td
   padding: 0.75rem 1rem
-  border-bottom: 1px solid $gray-200
+  border-bottom: 1px solid var(--color-gray-200)
   font-size: 0.875rem
 
   .dark &
-    border-bottom-color: $gray-600
+    border-bottom-color: var(--color-gray-600)
 
 .admin-products__image
   width: 2.5rem
@@ -195,24 +203,24 @@
 .admin-products__no-image
   width: 2.5rem
   height: 2.5rem
-  background-color: $gray-200
+  background-color: var(--color-gray-200)
   border-radius: 0.25rem
   display: flex
   align-items: center
   justify-content: center
   font-size: 0.75rem
-  color: $gray-500
+  color: var(--color-gray-500)
 
   .dark &
-    background-color: $gray-600
-    color: $gray-400
+    background-color: var(--color-gray-600)
+    color: var(--color-gray-400)
 
 .admin-products__name
   font-weight: 500
-  color: $gray-900
+  color: var(--color-gray-900)
 
   .dark &
-    color: $white
+    color: var(--color-white)
 
 .admin-products__price
   font-weight: 500
@@ -223,11 +231,11 @@
 
 .admin-products__old-price
   font-size: 0.75rem
-  color: $gray-500
+  color: var(--color-gray-500)
   text-decoration: line-through
 
   .dark &
-    color: $gray-400
+    color: var(--color-gray-400)
 
 .admin-products__status
   padding: 0.25rem 0.5rem
@@ -236,7 +244,7 @@
   font-weight: 500
 
 .admin-products__status--active
-  background-color: #f0fdf4
+  background-color: var(--color-primary-500-a10)
   color: #16a34a
 
   .dark &
@@ -244,7 +252,7 @@
     color: #4ade80
 
 .admin-products__status--inactive
-  background-color: #fef2f2
+  background-color: var(--color-primary-500-a10)
   color: #dc2626
 
   .dark &
@@ -265,11 +273,11 @@
   cursor: pointer
 
 .admin-products__action-btn--edit
-  background-color: #dbeafe
+  background-color: var(--color-accent-500-a20)
   color: #1e40af
 
   &:hover
-    background-color: #bfdbfe
+    background-color: var(--color-accent-500-a30)
 
   .dark &
     background-color: #1e3a8a
@@ -279,25 +287,25 @@
       background-color: #1e40af
 
 .admin-products__action-btn--toggle
-  background-color: $gray-100
-  color: $gray-700
+  background-color: var(--color-gray-100)
+  color: var(--color-gray-700)
 
   &:hover
-    background-color: $gray-200
+    background-color: var(--color-gray-200)
 
   .dark &
-    background-color: $gray-600
-    color: $gray-300
+    background-color: var(--color-gray-600)
+    color: var(--color-gray-300)
 
     &:hover
-      background-color: $gray-500
+      background-color: var(--color-gray-500)
 
 .admin-products__action-btn--delete
-  background-color: #fee2e2
+  background-color: var(--color-primary-500-a10)
   color: #dc2626
 
   &:hover
-    background-color: #fecaca
+    background-color: var(--color-primary-500-a20)
 
   .dark &
     background-color: #7f1d1d
@@ -310,14 +318,14 @@
 .admin-products__modal
   position: fixed
   inset: 0
-  background-color: rgba(0, 0, 0, 0.5)
+  background-color: var(--color-background-overlay)
   display: flex
   align-items: center
   justify-content: center
   z-index: 50
 
 .admin-products__modal-content
-  background-color: $white
+  background-color: var(--color-white)
   border-radius: 0.5rem
   padding: 1.5rem
   max-width: 48rem
@@ -327,59 +335,154 @@
   overflow-y: auto
 
   .dark &
-    background-color: $gray-800
+    background-color: var(--color-gray-800)
+
+  &.admin-products__modal-content--large
+    max-width: 64rem
 
 .admin-products__modal-title
   font-size: 1.125rem
   font-weight: 500
-  color: $gray-900
+  color: var(--color-gray-900)
   margin-bottom: 1rem
 
   .dark &
-    color: $white
+    color: var(--color-white)
 
-// Import form styles
-.admin-products__import-form
+.admin-products__modal-form
   display: flex
   flex-direction: column
   gap: 1rem
   margin-bottom: 1.5rem
 
-.admin-products__file-input
-  display: block
+.admin-products__form-group
+  display: flex
+  flex-direction: column
+  gap: 0.5rem
+
+// Image upload styles
+.admin-products__image-upload
+  display: flex
+  flex-direction: column
+  gap: 0.75rem
+
+.admin-products__image-preview
+  position: relative
   width: 100%
+  max-width: 200px
+  border: 1px solid var(--color-gray-300)
+  border-radius: 0.5rem
+  overflow: hidden
+
+  &.admin-products__image-preview--small
+    max-width: 100px
+
+  .dark &
+    border-color: var(--color-gray-600)
+
+.admin-products__preview-image
+  width: 100%
+  height: auto
+  display: block
+
+.admin-products__image-placeholder
+  padding: 2rem
+  background-color: var(--color-gray-100)
+  border: 1px dashed var(--color-gray-300)
+  border-radius: 0.5rem
+  text-align: center
+  color: var(--color-gray-500)
   font-size: 0.875rem
-  color: $gray-500
-
-  &::file-selector-button
-    margin-right: 1rem
-    padding: 0.5rem 1rem
-    border-radius: 9999px
-    border: none
-    background-color: $primary-50
-    color: $primary-700
-    font-size: 0.875rem
-    font-weight: 500
-    cursor: pointer
-    transition: background-color 0.2s ease-in-out
 
-    &:hover
-      background-color: $primary-100
+  .dark &
+    background-color: var(--color-gray-700)
+    border-color: var(--color-gray-600)
+    color: var(--color-gray-400)
 
-    .dark &
-      background-color: $primary-900
-      color: $primary-300
+.admin-products__file-input
+  display: none
+
+// Checkbox and domain styles
+.admin-products__checkbox-label
+  display: flex
+  align-items: center
+  gap: 0.5rem
+  color: var(--color-gray-700)
+  font-size: 0.875rem
+
+  .dark &
+    color: var(--color-gray-300)
+
+.admin-products__checkbox
+  border-radius: 0.25rem
+  border: 1px solid var(--color-gray-300)
+  background-color: var(--color-white)
+  color: var(--color-primary-600)
+
+  &:focus
+    border-color: var(--color-primary-500)
+    box-shadow: 0 0 0 2px var(--color-primary-500-a20)
+
+  .dark &
+    background-color: var(--color-gray-700)
+    border-color: var(--color-gray-600)
+
+.admin-products__domains-list
+  display: grid
+  grid-template-columns: repeat(2, 1fr)
+  gap: 0.5rem
+
+.admin-products__domain-label
+  display: flex
+  align-items: center
+  gap: 0.5rem
+  font-size: 0.875rem
+  color: var(--color-gray-700)
+
+  .dark &
+    color: var(--color-gray-300)
+
+.admin-products__domain-checkbox
+  border-radius: 0.25rem
+  border: 1px solid var(--color-gray-300)
+  background-color: var(--color-white)
+  color: var(--color-primary-600)
+
+  &:focus
+    border-color: var(--color-primary-500)
+    box-shadow: 0 0 0 2px var(--color-primary-500-a20)
+
+  .dark &
+    background-color: var(--color-gray-700)
+    border-color: var(--color-gray-600)
+
+.admin-products__modal-actions
+  display: flex
+  gap: 0.75rem
+  justify-content: flex-end
+
+// Import form styles
+.admin-products__import-form
+  display: flex
+  flex-direction: column
+  gap: 1rem
+  margin-bottom: 1.5rem
 
-      &:hover
-        background-color: $primary-800
+.admin-products__help-text
+  font-size: 0.75rem
+  color: var(--color-gray-500)
+  margin-top: 0.25rem
+
+  .dark &
+    color: var(--color-gray-400)
 
 .admin-products__file-info
   padding: 1rem
-  background-color: $gray-50
+  background-color: var(--color-gray-50)
   border-radius: 0.5rem
 
   .dark &
-    background-color: $gray-700
+    background-color: var(--color-gray-700)
 
 .admin-products__import-results
   padding: 1rem
@@ -401,10 +504,132 @@
   .dark &
     color: #fca5a5
 
-.admin-products__modal-actions
+// Categories management styles
+.admin-products__categories-tabs
   display: flex
-  gap: 0.75rem
-  justify-content: flex-end
+  gap: 0
+  border-bottom: 1px solid var(--color-gray-200)
+  margin-bottom: 1.5rem
+
+  .dark &
+    border-bottom-color: var(--color-gray-700)
+
+.admin-products__categories-tab
+  padding: 0.75rem 1.5rem
+  background: none
+  border: none
+  border-bottom: 2px solid transparent
+  color: var(--color-gray-600)
+  font-weight: 500
+  cursor: pointer
+  transition: all 0.2s ease-in-out
+
+  &:hover
+    color: var(--color-gray-900)
+
+  .dark &
+    color: var(--color-gray-400)
+
+    &:hover
+      color: var(--color-white)
+
+  &.admin-products__categories-tab--active
+    color: var(--color-primary-600)
+    border-bottom-color: var(--color-primary-600)
+
+    .dark &
+      color: var(--color-primary-400)
+      border-bottom-color: var(--color-primary-400)
+
+.admin-products__categories-header
+  display: flex
+  justify-content: space-between
+  align-items: center
+  margin-bottom: 1.5rem
+
+.admin-products__categories-grid
+  display: grid
+  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))
+  gap: 1rem
+
+.admin-products__category-item
+  background-color: var(--color-gray-50)
+  border: 1px solid var(--color-gray-200)
+  border-radius: 0.5rem
+  overflow: hidden
+
+  .dark &
+    background-color: var(--color-gray-700)
+    border-color: var(--color-gray-600)
+
+.admin-products__category-preview
+  padding: 1rem
+
+.admin-products__category-image
+  width: 100%
+  height: 120px
+  object-fit: cover
+  border-radius: 0.375rem
+  margin-bottom: 0.75rem
+
+.admin-products__category-no-image
+  width: 100%
+  height: 120px
+  background-color: var(--color-gray-200)
+  border-radius: 0.375rem
+  display: flex
+  align-items: center
+  justify-content: center
+  color: var(--color-gray-500)
+  font-size: 0.875rem
+  margin-bottom: 0.75rem
+
+  .dark &
+    background-color: var(--color-gray-600)
+    color: var(--color-gray-400)
+
+.admin-products__category-info
+  text-align: center
+
+.admin-products__category-name
+  font-weight: 600
+  color: var(--color-gray-900)
+  margin-bottom: 0.5rem
+
+  .dark &
+    color: var(--color-white)
+
+.admin-products__category-description
+  color: var(--color-gray-600)
+  font-size: 0.875rem
+  margin-bottom: 0.75rem
+  line-height: 1.4
+
+  .dark &
+    color: var(--color-gray-400)
+
+.admin-products__category-meta
+  display: flex
+  justify-content: space-between
+  align-items: center
+  font-size: 0.75rem
+
+.admin-products__category-products
+  color: var(--color-gray-500)
+
+  .dark &
+    color: var(--color-gray-400)
+
+.admin-products__category-actions
+  display: flex
+  gap: 0.5rem
+  padding: 0.75rem
+  border-top: 1px solid var(--color-gray-200)
+  background-color: var(--color-white)
+
+  .dark &
+    border-top-color: var(--color-gray-600)
+    background-color: var(--color-gray-800)
 
 // Responsive adjustments
 @media (max-width: 768px)
@@ -416,6 +641,7 @@
   .admin-products__actions
     width: 100%
     justify-content: flex-start
+    flex-wrap: wrap
 
   .admin-products__filters
     grid-template-columns: 1fr
@@ -429,3 +655,21 @@
   .admin-products__modal-content
     margin: 0 0.5rem
     padding: 1rem
+
+  .admin-products__categories-grid
+    grid-template-columns: 1fr
+
+  .admin-products__domains-list
+    grid-template-columns: 1fr
+
+  .admin-products__categories-tabs
+    flex-direction: column
+
+  .admin-products__categories-tab
+    text-align: center
+
+@media (max-width: 480px)
+  .admin-products__category-meta
+    flex-direction: column
+    gap: 0.5rem
+    align-items: flex-start

+ 2 - 2
app/pages/Admin/index.coffee

@@ -161,7 +161,7 @@ module.exports =
         .then (settings) =>
           @currentDomainSettings = settings
         .catch (error) =>
-          console.log 'Настройки домена не найдены, используются значения по умолчанию'
+          debug.log 'Настройки домена не найдены, используются значения по умолчанию'
           @currentDomainSettings = null
     
     loadUserData: ->
@@ -195,7 +195,7 @@ module.exports =
         @breadcrumbs = [{ title: 'Главная' }]
     
     showNotification: (message, type = 'success') ->
-      @$root.showNotification?(message, type) || console.log("#{type}: #{message}")
+      @$root.showNotification?(message, type) || debug.log("#{type}: #{message}")
   
   watch:
     currentRoute: