瀏覽代碼

restart projeckt

Gogs 3 周之前
父節點
當前提交
c9e9ac6b3d
共有 7 個文件被更改,包括 834 次插入153 次删除
  1. 50 14
      README.md
  2. 247 82
      app/design/site.coffee
  3. 38 42
      app/index.coffee
  4. 116 0
      app/services/CategoryService.coffee
  5. 95 0
      app/services/DomainService.coffee
  6. 131 0
      app/services/ProductService.coffee
  7. 157 15
      app/utils/pouch.coffee

+ 50 - 14
README.md

@@ -1278,7 +1278,39 @@ Domain компоненты: MultilevelMenu, CartWidget, ProductGrid, ProductCar
 Главное приложение регистрирует глобальные компоненты
 
 Созданы все необходимые заглушки для полнофункционального приложения
+Полноценный PouchDB сервис:
 
+Подключение к CouchDB с системой аутентификации
+
+Двусторонняя синхронизация с фильтрацией по доменам
+
+Обработка ошибок и автоматические повторы
+
+Локальное кэширование и офлайн-работа
+
+Дизайн-документы CouchDB:
+
+Views для товаров, категорий, заказов с индексацией
+
+Validate_doc_update функции для валидации данных 
+
+Система поисковых индексов для быстрого поиска
+
+Сервисы данных:
+
+ProductService: работа с товарами, поиск, фильтрация
+
+CategoryService: управление категориями, иерархия
+
+DomainService: мультидоменность, настройки доменов
+
+Интеграция в приложение:
+
+Правильная инициализация всех сервисов
+
+Обработка состояний загрузки и ошибок
+
+Подробное логгирование всех операций
 
 ### 🎯 БЛИЖАЙШИЕ ЗАДАЧИ
 
@@ -1327,30 +1359,34 @@ Domain компоненты: MultilevelMenu, CartWidget, ProductGrid, ProductCar
 
  ⚠️ ПРИОРИТЕТ
 
-ЭТАП 1.4: НАСТРОЙКА POUCHDB СЕРВИСА И ДИЗАЙН-ДОКУМЕНТОВ
+ЭТАП 1.5: РАЗРАБОТКА АДМИН-ПАНЕЛИ И СИСТЕМЫ ИМПОРТА
+
+Создание админ-панели:
+
+Layout администратора с навигацией
+
+Компонент управления товарами (DataTable)
 
-Реализовать полноценный PouchDB сервис:
+Редактор категорий с загрузкой изображений
 
-Подключение к CouchDB с аутентификацией
+Система импорта товаров:
 
-Система синхронизации с фильтрацией по доменам
+Компонент загрузки CSV файлов
 
-Обработка ошибок и повторов
+Парсинг и валидация данных CSV
 
-Создать дизайн-документы CouchDB:
+Пакетная обработка товаров
 
-Views для товаров, категорий, заказов
+Создание категорий на лету
 
-Validate_doc_update функции
+Управление медиа-файлами:
 
-Система индексов для поиска
+Загрузка изображений товаров
 
-Разработать сервисы данных:
+Прикрепление файлов к документам CouchDB
 
-ProductService для работы с товарами
+Система кэширования медиа
 
-CategoryService для управления категориями
+Приоритет: Высокий ⚠️ (необходимо для наполнения магазина товарами)
 
-DomainService для мультидоменности
 
-    Приоритет: Высокий ⚠️

+ 247 - 82
app/design/site.coffee

@@ -1,85 +1,250 @@
-module.exports = {
-  _id: '_design/site',
-  version: '1.0.0', 
-  appVersion: '1.0.0',
-  hash: 'site_v1_0_0_' + Date.now(),
-  
-  views: {
-    # Активные товары для каталога
-    active_products: {
-      map: ((doc) ->
-        if doc.type == 'product' && doc.active == true
-          emit([doc.category, doc.name], {
-            _id: doc._id,
-            name: doc.name,
-            price: doc.price,
-            oldPrice: doc.oldPrice,
-            sku: doc.sku,
-            image: doc.image,
-            category: doc.category,
-            description: doc.description,
-            attributes: doc.attributes
-          })
-      ).toString()
-    },
-    
-    # Опубликованные статьи блога
-    published_articles: {
-      map: ((doc) ->
-        if doc.type == 'blog_article' && doc.published == true
-          emit([doc.createdAt], {
-            _id: doc._id,
-            title: doc.title,
-            slug: doc.slug,
-            excerpt: doc.excerpt,
-            image: doc.image,
-            author: doc.author,
-            createdAt: doc.createdAt,
-            content: doc.content
-          })
-      ).toString()
-    },
-    
-    # Активные слайды
-    active_slides: {
-      map: ((doc) ->
-        if doc.type == 'hero_slide' && doc.active == true
-          emit(doc.order, {
-            _id: doc._id,
-            title: doc.title,
-            subtitle: doc.subtitle,
-            image: doc.image,
-            buttonText: doc.buttonText,
-            buttonLink: doc.buttonLink
-          })
-      ).toString()
+# app/design/site.coffee
+class SiteDesignDocuments
+  constructor: ->
+    @designDocs = {
+      products: @getProductsDesignDoc()
+      categories: @getCategoriesDesignDoc()
+      orders: @getOrdersDesignDoc()
+      validation: @getValidationDesignDoc()
     }
-  },
-  
-  validates_doc_update: ((newDoc, oldDoc, userCtx) ->
-    # Базовая валидация документов
-    
-    # Запрещаем изменение design документов
-    if newDoc._id && newDoc._id.startsWith('_design/')
-      if oldDoc  # existing document
-        throw { forbidden: 'Design documents can only be updated by admins' }
-    
-    # Валидация товаров
-    if newDoc.type == 'product'
-      if !newDoc.name
-        throw { forbidden: 'Product must have a name' }
-      if !newDoc.price || isNaN(parseFloat(newDoc.price))
-        throw { forbidden: 'Product must have a valid price' }
-      if !newDoc.sku
-        throw { forbidden: 'Product must have SKU' }
+
+  getProductsDesignDoc: ->
+    {
+      _id: '_design/products'
+      views:
+        by_category:
+          map: """
+            function(doc) {
+              if (doc.type === 'product' && doc.active !== false) {
+                emit([doc.category, doc.name], {
+                  _id: doc._id,
+                  name: doc.name,
+                  price: doc.price,
+                  images: doc.images,
+                  inStock: doc.inStock,
+                  brand: doc.brand
+                });
+              }
+            }
+          """
+        by_brand:
+          map: """
+            function(doc) {
+              if (doc.type === 'product' && doc.active !== false) {
+                emit([doc.brand, doc.category], {
+                  _id: doc._id,
+                  name: doc.name,
+                  price: doc.price,
+                  category: doc.category
+                });
+              }
+            }
+          """
+        by_sku:
+          map: """
+            function(doc) {
+              if (doc.type === 'product') {
+                emit(doc.sku, {
+                  _id: doc._id,
+                  name: doc.name,
+                  price: doc.price
+                });
+              }
+            }
+          """
+        search_index:
+          map: """
+            function(doc) {
+              if (doc.type === 'product' && doc.active !== false) {
+                var searchable = [
+                  doc.name,
+                  doc.brand,
+                  doc.category,
+                  doc.description
+                ].join(' ').toLowerCase();
+                
+                var words = searchable.split(/\\\\s+/);
+                words.forEach(function(word) {
+                  if (word.length > 2) {
+                    emit(word, 1);
+                  }
+                });
+              }
+            }
+          """
+        reduce: "_sum"
+      language: "javascript"
+    }
+
+  getCategoriesDesignDoc: ->
+    {
+      _id: '_design/categories'
+      views:
+        hierarchical:
+          map: """
+            function(doc) {
+              if (doc.type === 'category') {
+                // Для построения иерархии категорий
+                var path = doc.parent ? [doc._id] : [doc.parent, doc._id];
+                emit(path, {
+                  _id: doc._id,
+                  name: doc.name,
+                  parent: doc.parent,
+                  order: doc.order,
+                  image: doc.image
+                });
+              }
+            }
+          """
+        by_slug:
+          map: """
+            function(doc) {
+              if (doc.type === 'category') {
+                emit(doc.slug, {
+                  _id: doc._id,
+                  name: doc.name,
+                  parent: doc.parent
+                });
+              }
+            }
+          """
+        active_categories:
+          map: """
+            function(doc) {
+              if (doc.type === 'category' && doc.active !== false) {
+                emit(doc.order, {
+                  _id: doc._id,
+                  name: doc.name,
+                  image: doc.image
+                });
+              }
+            }
+          """
+      language: "javascript"
+    }
+
+  getOrdersDesignDoc: ->
+    {
+      _id: '_design/orders'
+      views:
+        by_user:
+          map: """
+            function(doc) {
+              if (doc.type === 'order') {
+                emit([doc.userId, doc.createdAt], {
+                  _id: doc._id,
+                  total: doc.total,
+                  status: doc.status,
+                  items: doc.items.length
+                });
+              }
+            }
+          """
+        by_status:
+          map: """
+            function(doc) {
+              if (doc.type === 'order') {
+                emit([doc.status, doc.createdAt], {
+                  _id: doc._id,
+                  userId: doc.userId,
+                  total: doc.total
+                });
+              }
+            }
+          """
+      language: "javascript"
+    }
+
+  getValidationDesignDoc: ->
+    {
+      _id: '_design/validation'
+      validate_doc_update: """
+        function(newDoc, oldDoc, userCtx, secObj) {
+          // Функция проверки документов при сохранении :cite[2]:cite[10]
+          
+          // Проверка типа документа
+          if (newDoc.type) {
+            var validTypes = [
+              'product', 'category', 'order', 'user', 
+              'domain_settings', 'hero_slide', 'blog_article'
+            ];
+            
+            if (validTypes.indexOf(newDoc.type) === -1) {
+              throw({forbidden: 'Invalid document type: ' + newDoc.type});
+            }
+          }
+          
+          // Проверка обязательных полей для товаров :cite[2]
+          if (newDoc.type === 'product') {
+            if (!newDoc.name) {
+              throw({forbidden: 'Product must have a name'});
+            }
+            if (!newDoc.sku) {
+              throw({forbidden: 'Product must have SKU'});
+            }
+            if (typeof newDoc.price !== 'number' || newDoc.price < 0) {
+              throw({forbidden: 'Product must have valid price'});
+            }
+          }
+          
+          // Проверка категорий
+          if (newDoc.type === 'category') {
+            if (!newDoc.name) {
+              throw({forbidden: 'Category must have a name'});
+            }
+            if (!newDoc.slug) {
+              throw({forbidden: 'Category must have a slug'});
+            }
+          }
+          
+          // Проверка заказов
+          if (newDoc.type === 'order') {
+            if (!newDoc.userId) {
+              throw({forbidden: 'Order must have user ID'});
+            }
+            if (!Array.isArray(newDoc.items) || newDoc.items.length === 0) {
+              throw({forbidden: 'Order must have items'});
+            }
+          }
+          
+          // Проверка неизменяемых полей
+          if (oldDoc) {
+            if (oldDoc.type !== newDoc.type) {
+              throw({forbidden: 'Document type cannot be changed'});
+            }
+            if (oldDoc.createdAt !== newDoc.createdAt) {
+              throw({forbidden: 'Creation date cannot be changed'});
+            }
+          }
+          
+          // Проверка прав доступа :cite[2]
+          if (newDoc.type === 'order' || newDoc.type === 'user') {
+            if (userCtx.roles.indexOf('_admin') === -1 && 
+                userCtx.roles.indexOf('user') === -1) {
+              throw({unauthorized: 'You are not authorized to modify this document'});
+            }
+          }
+        }
+      """
+      language: "javascript"
+    }
+
+  saveDesignDocs: (pouchService) ->
+    log '💾 Сохранение дизайн-документов в базу...'
     
-    # Валидация статей блога
-    if newDoc.type == 'blog_article'
-      if !newDoc.title
-        throw { forbidden: 'Blog article must have a title' }
-      if !newDoc.slug
-        throw { forbidden: 'Blog article must have a slug' }
+    promises = []
+    for name, doc of @designDocs
+      promises.push(
+        pouchService.saveDocument(doc)
+          .then ->
+            log "✅ Дизайн-документ сохранен: #{name}"
+          .catch (error) ->
+            if error.status == 409
+              log "⚠️ Дизайн-документ уже существует: #{name}"
+            else
+              log "❌ Ошибка сохранения дизайн-документа #{name}:", error
+      )
     
-    return true
-  ).toString()
-}
+    return Promise.all(promises)
+
+module.exports = new SiteDesignDocuments()

+ 38 - 42
app/index.coffee

@@ -15,16 +15,7 @@ globalThis.stylFns = require 'styl.json'
 
 
 
-# Сервисы (пока заглушки)
-PouchDBService = 
-  init: -> Promise.resolve()
-  getDocument: -> Promise.resolve(null)
-  saveToRemote: -> Promise.resolve()
 
-DomainService = 
-  init: -> Promise.resolve()
-  loadDomainSettings: -> Promise.resolve(null)
-  getAvailableDomains: -> []
 
 # Мета-теги
 document.head.insertAdjacentHTML 'beforeend', '<meta charset="UTF-8">'
@@ -40,6 +31,16 @@ if stylFns['app/index.styl']
 else
   log '⚠️ Глобальные стили не найдены'
 
+
+# Добавить в секцию импортов
+PouchDBService = require 'app/utils/pouch'
+SiteDesignDocuments = require 'app/design/site'
+ProductService = require 'app/services/ProductService'
+CategoryService = require 'app/services/CategoryService'
+DomainService = require 'app/services/DomainService'
+
+
+
 # Создание Vue приложения
 app = Vue.createApp({
   data: ->
@@ -166,43 +167,38 @@ app = Vue.createApp({
       else
         @cartItems = []
     
-    # Инициализация приложения
     initializeApp: ->
       log '🔧 Начало инициализации приложения'
       @loading = true
       
-      # Инициализация темы
-      if @theme == 'dark'
-        document.documentElement.classList.add 'dark'
-        log '🌙 Темная тема активирована'
-      else
-        log '☀️ Светлая тема активирована'
-      
-      # Последовательная инициализация сервисов
-      Promise.resolve()
-        .then =>
-          log '📦 Инициализация PouchDB...'
-          PouchDBService.init()
-        .then =>
-          log '🌐 Инициализация DomainService...'
-          DomainService.init()
-        .then =>
-          log '📡 Получение доступных доменов...'
-          @availableDomains = DomainService.getAvailableDomains()
-        .then =>
-          @loadDomainData()
-        .then =>
-          @loadUserData()
-        .then =>
-          @loadCartData()
-        .then =>
-          log '✅ Приложение успешно инициализировано'
-          @showNotification('Приложение готово к работе', 'success')
-        .catch (error) =>
-          log '❌ Ошибка инициализации приложения:', error
-          @showNotification('Ошибка загрузки приложения', 'error')
-        .finally =>
-          @loading = false
+      try
+        # Инициализация PouchDB и сервисов
+        await PouchDBService.init()
+        log '✅ PouchDB инициализирован'
+        
+        # Сохранение дизайн-документов
+        await SiteDesignDocuments.saveDesignDocs(PouchDBService)
+        log '✅ Дизайн-документы сохранены'
+        
+        # Инициализация сервисов
+        await ProductService.init()
+        await CategoryService.init()
+        await DomainService.init()
+        log '✅ Все сервисы инициализированы'
+        
+        # Загрузка данных
+        await @loadDomainData()
+        await @loadUserData()
+        await @loadCartData()
+        
+        log '✅ Приложение успешно инициализировано'
+        @showNotification('Приложение готово к работе', 'success')
+        
+      catch error
+        log '❌ Ошибка инициализации приложения:', error
+        @showNotification('Ошибка загрузки приложения', 'error')
+      finally
+        @loading = false
   
   mounted: ->
     await @initializeApp()

+ 116 - 0
app/services/CategoryService.coffee

@@ -0,0 +1,116 @@
+# app/services/CategoryService.coffee
+{ Category } = require 'app/types/data'
+
+class CategoryService
+  constructor: ->
+    @pouchService = require 'app/utils/pouch'
+    @initialized = false
+
+  init: ->
+    return Promise.resolve() if @initialized
+    
+    try
+      await @pouchService.init()
+      @initialized = true
+      log '✅ CategoryService инициализирован'
+      return Promise.resolve()
+    catch error
+      log '❌ Ошибка инициализации CategoryService:', error
+      return Promise.reject(error)
+
+  getAllCategories: ->
+    await @ensureInit()
+    
+    try
+      result = await @pouchService.queryView('categories', 'active_categories', {
+        include_docs: true
+      })
+      
+      categories = result.rows.map (row) ->
+        new Category(row.doc)
+      
+      # Сортировка по порядку
+      categories.sort (a, b) -> a.order - b.order
+      
+      log '📂 Загружены категории:', categories.length
+      return categories
+    catch error
+      log '❌ Ошибка загрузки категорий:', error
+      throw error
+
+  getCategoryBySlug: (slug) ->
+    await @ensureInit()
+    
+    try
+      result = await @pouchService.queryView('categories', 'by_slug', {
+        key: slug
+        include_docs: true
+      })
+      
+      if result.rows.length > 0
+        category = new Category(result.rows[0].doc)
+        log "📂 Загружена категория по slug: #{slug}"
+        return category
+      else
+        log "⚠️ Категория не найдена по slug: #{slug}"
+        return null
+    catch error
+      log "❌ Ошибка поиска категории по slug #{slug}:", error
+      throw error
+
+  getHierarchicalCategories: ->
+    await @ensureInit()
+    
+    try
+      result = await @pouchService.queryView('categories', 'hierarchical', {
+        include_docs: true
+      })
+      
+      categories = result.rows.map (row) ->
+        new Category(row.doc)
+      
+      # Построение иерархии
+      hierarchical = @buildHierarchy(categories)
+      
+      log '🌳 Построена иерархия категорий'
+      return hierarchical
+    catch error
+      log '❌ Ошибка построения иерархии категорий:', error
+      throw error
+
+  buildHierarchy: (categories, parentId = null) ->
+    hierarchy = []
+    
+    categories
+      .filter (cat) -> cat.parent == parentId
+      .sort (a, b) -> a.order - b.order
+      .forEach (category) =>
+        children = @buildHierarchy(categories, category._id)
+        if children.length > 0
+          category.children = children
+        hierarchy.push(category)
+    
+    return hierarchy
+
+  saveCategory: (categoryData) ->
+    await @ensureInit()
+    
+    try
+      category = new Category(categoryData)
+      
+      # Генерация ID если не установлен
+      if not category._id
+        category._id = "category:#{Date.now()}"
+      
+      result = await @pouchService.saveDocument(category)
+      log "💾 Категория сохранена: #{category.name}"
+      return result
+    catch error
+      log "❌ Ошибка сохранения категории:", error
+      throw error
+
+  ensureInit: ->
+    unless @initialized
+      throw new Error('CategoryService не инициализирован. Вызовите init() сначала.')
+
+module.exports = new CategoryService()

+ 95 - 0
app/services/DomainService.coffee

@@ -0,0 +1,95 @@
+# app/services/DomainService.coffee
+{ DomainSettings } = require 'app/types/data'
+
+class DomainService
+  constructor: ->
+    @pouchService = require 'app/utils/pouch'
+    @initialized = false
+    @currentDomain = window.location.hostname
+
+  init: ->
+    return Promise.resolve() if @initialized
+    
+    try
+      await @pouchService.init()
+      @initialized = true
+      log '✅ DomainService инициализирован'
+      return Promise.resolve()
+    catch error
+      log '❌ Ошибка инициализации DomainService:', error
+      return Promise.reject(error)
+
+  loadDomainSettings: (domain = @currentDomain) ->
+    await @ensureInit()
+    
+    try
+      docId = "domain_settings:#{domain}"
+      settingsDoc = await @pouchService.getDocument(docId)
+      
+      settings = new DomainSettings(settingsDoc)
+      log "🌐 Настройки домена загружены: #{domain}"
+      return settings
+    catch error
+      if error.status == 404
+        log "⚠️ Настройки домена не найдены, создаем defaults: #{domain}"
+        return @createDefaultSettings(domain)
+      else
+        log "❌ Ошибка загрузки настроек домена #{domain}:", error
+        throw error
+
+  createDefaultSettings: (domain) ->
+    settings = new DomainSettings()
+    settings._id = "domain_settings:#{domain}"
+    settings.domain = domain
+    settings.companyName = 'Браер-Колор'
+    settings.languages = ['ru']
+    settings.defaultLanguage = 'ru'
+    settings.theme = 'light'
+    
+    # Сохраняем настройки по умолчанию
+    await @saveDomainSettings(settings)
+    return settings
+
+  saveDomainSettings: (settings) ->
+    await @ensureInit()
+    
+    try
+      result = await @pouchService.saveDocument(settings)
+      log "💾 Настройки домена сохранены: #{settings.domain}"
+      return result
+    catch error
+      log "❌ Ошибка сохранения настроек домена:", error
+      throw error
+
+  getAvailableDomains: ->
+    # Здесь будет логика получения списка доступных доменов
+    # Пока возвращаем тестовые данные
+    return [
+      'braer-color.ru'
+      's5l.ru'
+      'localhost'
+    ]
+
+  getDomainConfig: (domain) ->
+    # Конфигурация для конкретного домена
+    baseConfig = 
+      theme: 'light'
+      language: 'ru'
+      currency: 'RUB'
+    
+    # Доменные специфические настройки
+    domainConfigs =
+      'braer-color.ru':
+        companyName: 'Браер-Колор'
+        theme: 'brand'
+      's5l.ru':
+        companyName: 'S5L Group'
+        theme: 'dark'
+    
+    return { ...baseConfig, ...(domainConfigs[domain] or {}) }
+
+  ensureInit: ->
+    unless @initialized
+      throw new Error('DomainService не инициализирован. Вызовите init() сначала.')
+
+module.exports = new DomainService()

+ 131 - 0
app/services/ProductService.coffee

@@ -0,0 +1,131 @@
+# app/services/ProductService.coffee
+{ Product } = require 'app/types/data'
+
+class ProductService
+  constructor: ->
+    @pouchService = require 'app/utils/pouch'
+    @initialized = false
+
+  init: ->
+    return Promise.resolve() if @initialized
+    
+    try
+      await @pouchService.init()
+      @initialized = true
+      log '✅ ProductService инициализирован'
+      return Promise.resolve()
+    catch error
+      log '❌ Ошибка инициализации ProductService:', error
+      return Promise.reject(error)
+
+  getAllProducts: (options = {}) ->
+    await @ensureInit()
+    
+    try
+      opts = 
+        include_docs: true
+        startkey: ['product']
+        endkey: ['product', {}]
+      
+      result = await @pouchService.queryView('products', 'by_category', opts)
+      
+      products = result.rows.map (row) ->
+        new Product(row.doc)
+      
+      log '📦 Загружены товары:', products.length
+      return products
+    catch error
+      log '❌ Ошибка загрузки товаров:', error
+      throw error
+
+  getProductsByCategory: (category, options = {}) ->
+    await @ensureInit()
+    
+    try
+      opts = 
+        include_docs: true
+        key: [category]
+      
+      result = await @pouchService.queryView('products', 'by_category', opts)
+      
+      products = result.rows.map (row) ->
+        new Product(row.doc)
+      
+      log "📦 Загружены товары категории #{category}:", products.length
+      return products
+    catch error
+      log "❌ Ошибка загрузки товаров категории #{category}:", error
+      throw error
+
+  getProductBySku: (sku) ->
+    await @ensureInit()
+    
+    try
+      result = await @pouchService.queryView('products', 'by_sku', {
+        key: sku
+        include_docs: true
+      })
+      
+      if result.rows.length > 0
+        product = new Product(result.rows[0].doc)
+        log "📦 Загружен товар по SKU: #{sku}"
+        return product
+      else
+        log "⚠️ Товар не найден по SKU: #{sku}"
+        return null
+    catch error
+      log "❌ Ошибка поиска товара по SKU #{sku}:", error
+      throw error
+
+  searchProducts: (query, options = {}) ->
+    await @ensureInit()
+    
+    try
+      # Простой поиск по индексу
+      searchTerms = query.toLowerCase().split(/\s+/).filter (term) -> term.length > 2
+      
+      if searchTerms.length == 0
+        return []
+      
+      # Здесь будет реализация полнотекстового поиска
+      # Временная реализация - поиск по всем товарам и фильтрация
+      allProducts = await @getAllProducts()
+      
+      filteredProducts = allProducts.filter (product) ->
+        searchable = [
+          product.name
+          product.brand
+          product.category
+          product.description
+        ].join(' ').toLowerCase()
+        
+        searchTerms.every (term) -> searchable.includes(term)
+      
+      log "🔍 Результаты поиска '#{query}':", filteredProducts.length
+      return filteredProducts
+    catch error
+      log "❌ Ошибка поиска товаров '#{query}':", error
+      throw error
+
+  saveProduct: (productData) ->
+    await @ensureInit()
+    
+    try
+      product = new Product(productData)
+      
+      # Генерация ID если не установлен
+      if not product._id
+        product._id = "product:#{product.sku}"
+      
+      result = await @pouchService.saveDocument(product)
+      log "💾 Товар сохранен: #{product.name}"
+      return result
+    catch error
+      log "❌ Ошибка сохранения товара:", error
+      throw error
+
+  ensureInit: ->
+    unless @initialized
+      throw new Error('ProductService не инициализирован. Вызовите init() сначала.')
+
+module.exports = new ProductService()

+ 157 - 15
app/utils/pouch.coffee

@@ -6,39 +6,181 @@ class PouchDBService
     @remoteDb = null
     @syncHandler = null
     @initialized = false
+    @syncStatus = 'disconnected'
 
   init: ->
     return Promise.resolve() if @initialized
     
     try
+      log '🔄 Инициализация PouchDB сервиса'
+      
       # Инициализация локальной базы
       @localDb = new PouchDB(@localDbName or 'braer_color_cache')
-      log 'Локальная PouchDB инициализирована'
+      log 'Локальная PouchDB инициализирована'
       
       # Инициализация удаленной базы
       @remoteDb = new PouchDB(@remoteDbUrl, {
         skip_setup: false
-        fetch: (url, opts) ->
-          # Добавление обработки CORS и аутентификации
+        fetch: (url, opts) =>
           opts.credentials = 'include'
+          # Добавляем заголовки аутентификации
+          opts.headers ?= {}
+          # Здесь будут добавляться токены аутентификации
           PouchDB.fetch(url, opts)
       })
       
-      # Настройка непрерывной синхронизации:cite[7]
-      @syncHandler = PouchDB.sync(@localDb, @remoteDb, {
-        live: true,
-        retry: true,
-        filter: (doc) => @shouldSyncDocument(doc)
-      })
-      .on 'change', (info) =>
-        log 'Синхронизация: данные изменены', info
-      .on 'error', (err) =>
-        log 'Ошибка синхронизации:', err
+      log '✅ Удаленная CouchDB подключена'
+      
+      # Настройка непрерывной синхронизации
+      @setupSync()
       
       @initialized = true
-      log 'PouchDB сервис полностью инициализирован'
+      log '🎉 PouchDB сервис полностью инициализирован'
       return Promise.resolve()
       
     catch error
-      log 'Критическая ошибка инициализации PouchDB:', error
+      log 'Критическая ошибка инициализации PouchDB:', error
       return Promise.reject(error)
+
+  setupSync: ->
+    @syncHandler = PouchDB.sync(@localDb, @remoteDb, {
+      live: true,
+      retry: true,
+      batch_size: 50,
+      batches_limit: 10,
+      filter: (doc) => @shouldSyncDocument(doc)
+    })
+    .on 'change', (info) =>
+      log '📡 Синхронизация: данные изменены', info
+      @syncStatus = 'syncing'
+    .on 'paused', (err) =>
+      log '⏸️ Синхронизация приостановлена'
+      @syncStatus = 'paused'
+    .on 'active', ->
+      log '🔄 Синхронизация активна'
+      @syncStatus = 'active'
+    .on 'denied', (err) =>
+      log '🚫 Доступ запрещен:', err
+      @syncStatus = 'denied'
+    .on 'complete', (info) =>
+      log '✅ Синхронизация завершена', info
+      @syncStatus = 'complete'
+    .on 'error', (err) =>
+      log '❌ Ошибка синхронизации:', err
+      @syncStatus = 'error'
+      @handleSyncError(err)
+
+  shouldSyncDocument: (doc) ->
+    # Фильтрация по доменам и типам документов
+    return true if doc.type in ['product', 'category', 'domain_settings', 'hero_slide']
+    return true if doc.type in ['blog_article', 'route', 'user_settings']
+    
+    # Для заказов и пользовательских данных проверяем принадлежность
+    if doc.type in ['order', 'user_data', 'cart']
+      return doc.userId == @getCurrentUserId()
+    
+    # По умолчанию не синхронизируем
+    return false
+
+  getCurrentUserId: ->
+    # Получение ID текущего пользователя
+    userData = localStorage.getItem('user')
+    if userData
+      try
+        user = JSON.parse(userData)
+        return user.id
+      catch
+        return 'anonymous'
+    return 'anonymous'
+
+  handleSyncError: (error) ->
+    log '🔄 Обработка ошибки синхронизации:', error
+    
+    # Автоматический повтор при временных ошибках
+    if error.status in [408, 429, 500, 502, 503, 504]
+      log '⏳ Временная ошибка, повтор через 5 секунд...'
+      setTimeout (=>
+        @setupSync()
+      ), 5000
+    else if error.status == 401
+      log '🔐 Ошибка аутентификации, требуется перелогин'
+      # Триггер события для приложения
+      document.dispatchEvent(new CustomEvent('auth-required'))
+
+  # Основные методы работы с данными
+  getDocument: (docId) ->
+    @ensureInit()
+    
+    try
+      # Пробуем получить из локальной базы
+      doc = await @localDb.get(docId)
+      log '📄 Документ получен из локальной базы:', docId
+      return doc
+    catch localError
+      if localError.status == 404
+        try
+          # Получаем из удаленной базы и сохраняем локально
+          doc = await @remoteDb.get(docId)
+          await @localDb.put(doc)
+          log '📄 Документ получен из удаленной базы и сохранен локально:', docId
+          return doc
+        catch remoteError
+          log '❌ Документ не найден в удаленной базе:', docId
+          throw remoteError
+      else
+        log '❌ Ошибка получения документа из локальной базы:', localError
+        throw localError
+
+  saveDocument: (doc) ->
+    @ensureInit()
+    
+    try
+      # Сохраняем в локальную базу
+      result = await @localDb.put(doc)
+      log '💾 Документ сохранен локально:', doc._id
+      return result
+    catch error
+      log '❌ Ошибка сохранения документа:', error
+      throw error
+
+  bulkDocs: (docs) ->
+    @ensureInit()
+    
+    try
+      result = await @localDb.bulkDocs(docs)
+      log '📦 Пакетное сохранение документов:', docs.length
+      return result
+    catch error
+      log '❌ Ошибка пакетного сохранения:', error
+      throw error
+
+  queryView: (designDoc, viewName, options = {}) ->
+    @ensureInit()
+    
+    try
+      result = await @localDb.query("#{designDoc}/#{viewName}", options)
+      log '🔍 Выполнен запрос к view:', "#{designDoc}/#{viewName}"
+      return result
+    catch error
+      log '❌ Ошибка запроса к view:', error
+      throw error
+
+  ensureInit: ->
+    unless @initialized
+      throw new Error('PouchDB сервис не инициализирован. Вызовите init() сначала.')
+
+  destroy: ->
+    if @syncHandler
+      @syncHandler.cancel()
+    
+    if @localDb
+      await @localDb.close()
+    
+    @initialized = false
+    log '🔚 PouchDB сервис остановлен'
+
+module.exports = new PouchDBService({
+  localDbName: 'braer_color_cache',
+  remoteDbUrl: 'https://oleg:631074@couchdb.favt.ru.net/braer_color_shop',
+  appVersion: '1.0.0'
+})