Aucune description

Gogs ea66b8da6b restart projeckt il y a 3 semaines
app ea66b8da6b restart projeckt il y a 3 semaines
.gitignore 444e275e0c restart projeckt il y a 3 semaines
README.md ea66b8da6b restart projeckt il y a 3 semaines

README.md

🎯 ПОЛНЫЙ ПРОМТ ДЛЯ РАЗРАБОТКИ ИНТЕРНЕТ-МАГАЗИНА "БРАЕР-КОЛОР"

📋 ТЕХНИЧЕСКОЕ ЗАДАНИЕ

ВАЖНО: debug является глобально объявленной переменной во всем приложении. НЕ используйте debug = require 'debug' в компонентах.

🎯 КОНТЕКСТ ПРОЕКТА

Название: Интернет-магазин лакокрасочной продукции "Браер-Колор"
Тип: SPA (Single Page Application) с современным минималистичным дизайном
Аналоги: Функциональность m-kraski.ru с дизайном https://braer-color.ru/
Архитектура: Мультидоменная, мультиязычная PWA с офлайн-режимом GIT репозитарий: https://gogs.osvoj.ru/oleg/s5l.ru-crm Текущая версия промта https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md

🛠 ТЕХНИЧЕСКИЙ СТЕК

  • Шаблонизатор: Pug с Vue компонентами
  • Стилизация: Stylus + CSS переменные + BEM методология
  • Логика: CoffeeScript + Vue.js 3 (runtime)
  • Важно Vuejs в runtime режиме, не использовать template, шаблоны СТРОГО через "render: (new Function '_ctx', '_cache', renderFns['app/index.pug'])()" во всех компанентах
  • Маршрутизация: Vue Router
  • База данных: PouchDB (клиент) + CouchDB (сервер)
  • Анимации: CSS transitions/transforms + Vue transitions

📁 СТРУКТУРА ПРОЕКТА

🏗️ АРХИТЕКТУРА ФАЙЛОВ

app/
├── index.pug                 # Главный layout
├── index.coffee              # Инициализация Vue и роутера
├── index.styl                # Глобальные стили и CSS переменные
├── config.coffee             # Конфигурация приложения
├── types/                    # Интерфейсы данных
│   ├── data.coffee
│   ├── events.coffee
│   └── api.coffee
├── services/                 # Бизнес-логика
│   ├── DomainService.coffee
│   ├── ProductService.coffee
│   ├── ImportService.coffee
│   └── CategoryService.coffee
├── utils/                    # Утилиты
│   └── pouch.coffee          # PouchDB сервис
├── design/                   # Дизайн-документы CouchDB
│   ├── admin.coffee
│   └── site.coffee
├── components/               # Переиспользуемые компоненты
│   ├── UI/
│   │   ├── Button/
│   │   ├── Modal/
│   │   └── Notification/
│   ├── Domain/
│   │   ├── ProductCard/
│   │   ├── CategoryMenu/
│   │   └── CartWidget/
│   └── Admin/
│       ├── DataTable/
│       └── FileUpload/
└── pages/                    # Страницы приложения
    ├── Home/                 # Главная страница
    │   ├── index.pug
    │   ├── index.coffee
    │   └── index.styl
    ├── Catalog/              # Каталог товаров
    │   ├── index.pug
    │   ├── index.coffee
    │   └── index.styl
    ├── Product/              # Страница товара
    │   ├── index.pug
    │   ├── index.coffee
    │   └── index.styl
    ├── Cart/                 # Корзина
    │   ├── index.pug
    │   ├── index.coffee
    │   └── index.styl
    ├── Blog/                 # Блог
    │   ├── index.pug
    │   ├── index.coffee
    │   └── index.styl
    ├── Article/              # Статья блога
    │   ├── index.pug
    │   ├── index.coffee
    │   └── index.styl
    └── Admin/                # Админ-панель
        ├── index.pug
        ├── index.coffee
        ├── index.styl
        ├── Dashboard/        # Дашборд
        ├── Products/         # Управление товарами
        │   ├── index.pug
        │   ├── index.coffee
        │   ├── index.styl
        │   └── Import/       # Импорт товаров
        │       ├── index.pug
        │       ├── index.coffee
        │       └── index.styl
        ├── Categories/       # Управление категориями
        ├── Blog/             # Управление блогом
        ├── Slider/           # Управление слайдами
        ├── Clients/          # Управление клиентами
        ├── Orders/           # Управление заказами
        ├── Routes/           # Управление маршрутами
        └── Settings/         # Настройки системы

💻 ПРИМЕРЫ КОДА

🎨 СИСТЕМА СТИЛЕЙ

app/index.styl здесь храняться базовые стили темы. которые используются во всех остальных стилевых файлах. отдельно его подключать в них не нужно, он глобально доступен.

// CSS переменные вместо rgba функций
:root
  // Основные цвета
  --color-primary: #2c5aa0
  --color-secondary: #6c757d
  --color-success: #28a745
  --color-danger: #dc3545
  --color-warning: #ffc107
  --color-light: #f8f9fa
  --color-dark: #343a40
  
  // Прозрачные варианты
  --color-primary-10: #2c5aa01a
  --color-primary-20: #2c5aa033
  --color-primary-50: #2c5aa080
  --color-dark-10: #343a401a
  --color-dark-50: #343a4080
  --color-light-10: #f8f9fa1a
  --color-light-50: #f8f9fa80
  
  // Тени
  --shadow-sm: 0 1px 2px var(--color-dark-10)
  --shadow-md: 0 4px 6px var(--color-dark-10)
  --shadow-lg: 0 10px 15px var(--color-dark-10)
  
  // Прочие переменные
  --border-radius: 8px
  --transition: all 0.3s ease

// Базовые стили
.app
  min-height: 100vh
  transition: var(--transition)
  background: var(--color-light)
  color: var(--color-dark)

  &.theme-dark
    background: var(--color-dark)
    color: var(--color-light)

.header
  display: flex
  align-items: center
  padding: 1rem 2rem
  background: var(--color-light)
  box-shadow: var(--shadow-sm)
  border-bottom: 1px solid var(--color-primary-10)
  
  .theme-dark &
    background: var(--color-dark)
    box-shadow: var(--shadow-md)

// Адаптивность
@media (max-width: 768px)
  .header
    padding: 1rem
    flex-direction: column
    gap: 1rem

app/index.pug

div(class="app" :class="{'theme-dark': theme === 'dark'}")
  header(class="header")
    nav(class="header-nav")
      div(class="header-nav-block")
        div(class="header-nav--name") {{ currentDomainSettings?.companyName || companyName }}
        div(class="header-nav--menu")
          multilevelmenu(:domains="availableDomains" :current-domain="currentDomain")
          themetoggle(:theme="theme" @theme-changed="toggleTheme")
          languagetoggle(:languages="languages" :current-language="currentLanguage" @language-changed="setLanguage")
          cartwidget(:items="cartItems" @update-cart="updateCart")
  
  main(class="main-content")
    router-view(v-slot="{ Component, route }")
      transition(name="page-slide" mode="out-in")
        component(
          :is="Component"
          :key="route.fullPath"
          :domain-settings="currentDomainSettings"
          :language="currentLanguage"
        )
  
  notification-container(:notifications="notifications")

app/index.coffee

# Глобальная инициализация debug
globalThis.log = debug.log

# Главный файл приложения
log '🚀 Инициализация приложения Браер-Колор'

# Загрузка конфигурации
config = require 'app/config'
DataTypes = require 'app/types/data'
EventTypes = require 'app/types/events'

# Инициализация глобальных переменных
globalThis.renderFns = require 'pug.json'
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">'
document.head.insertAdjacentHTML 'beforeend', '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
document.head.insertAdjacentHTML 'beforeend', '<title>Браер-Колор - Интернет-магазин лакокрасочной продукции</title>'

# Добавление глобальных стилей
if stylFns['app/index.styl']
  styleElement = document.createElement('style')
  styleElement.type = 'text/css'
  styleElement.textContent = stylFns['app/index.styl']
  document.head.appendChild(styleElement)
else
  log '⚠️ Глобальные стили не найдены'

# Создание Vue приложения
app = Vue.createApp({
  data: ->
    {
      theme: localStorage.getItem('theme') or config.defaultTheme
      companyName: config.companyName
      loading: false
      currentDomain: window.location.hostname
      currentDomainSettings: null
      availableDomains: []
      languages: config.languages
      currentLanguage: localStorage.getItem('language') or config.defaultLanguage
      user: null
      cartItems: []
      notifications: []
    }
  
  computed:
    isAdmin: -> 
      @user?.role == 'admin'
    
    domainConfig: -> 
      DomainService.getDomainConfig?(@currentDomain) or {}
  
  methods:
    # Управление темой
    toggleTheme: ->
      @theme = if @theme == 'light' then 'dark' else 'light'
      localStorage.setItem 'theme', @theme
      document.documentElement.classList.toggle 'dark'
      @$emit EventTypes.THEME_CHANGED, @theme
      log '🎨 Тема изменена:', @theme
    
    # Управление языком
    setLanguage: (lang) ->
      if @languages.includes lang
        @currentLanguage = lang
        localStorage.setItem 'language', @currentLanguage
        @$emit EventTypes.LANGUAGE_CHANGED, lang
        log '🌐 Язык изменен:', lang
      else
        log '⚠️ Язык не поддерживается:', lang
    
    # Смена домена
    changeDomain: (domain) ->
      log '🌐 Смена домена на:', domain
      @currentDomain = domain
      @loadDomainData()
    
    # Переход в корзину
    goToCart: ->
      @$router.push '/cart'
      log '🛒 Переход в корзину'
    
    # Загрузка настроек домена
    loadDomainData: ->
      log '📡 Загрузка настроек домена:', @currentDomain
      
      DomainService.loadDomainSettings(@currentDomain)
        .then (settings) =>
          @currentDomainSettings = settings
          document.title = settings?.companyName or @companyName
          log '✅ Настройки домена загружены', settings
        .catch (error) =>
          log '⚠️ Настройки домена не найдены, используются значения по умолчанию'
          @currentDomainSettings = new DataTypes.DomainSettings()
          @currentDomainSettings.companyName = @companyName
    
    # Управление корзиной
    updateCart: (items) ->
      @cartItems = items
      localStorage.setItem 'cart', JSON.stringify(items)
      @$emit EventTypes.CART_UPDATE, items
      log '🛒 Корзина обновлена:', items.length, 'товаров'
    
    # Уведомления
    showNotification: (message, type = 'info') ->
      notification = { 
        id: Date.now(), 
        message, 
        type, 
        visible: true,
        timestamp: new Date()
      }
      
      @notifications.push notification
      log '📢 Показано уведомление:', message
      
      setTimeout (=>
        notification.visible = false
        setTimeout (=>
          @notifications = @notifications.filter (n) -> n.id != notification.id
        ), 300
      ), 5000
    
    # Закрытие уведомления
    closeNotification: (id) ->
      @notifications = @notifications.filter (notification) -> notification.id != id
      log '📢 Уведомление закрыто:', id
    
    # Загрузка пользователя
    loadUserData: ->
      userData = localStorage.getItem 'user'
      if userData
        try
          @user = JSON.parse userData
          log '👤 Пользователь загружен:', @user.username
        catch error
          log '❌ Ошибка загрузки пользователя:', error
          @user = null
      else
        @user = null
    
    # Загрузка корзины
    loadCartData: ->
      cartData = localStorage.getItem 'cart'
      if cartData
        try
          @cartItems = JSON.parse cartData
          log '🛒 Корзина загружена:', @cartItems.length, 'товаров'
        catch error
          log '❌ Ошибка загрузки корзины:', error
          @cartItems = []
      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
  
  async mounted: ->
    await @initializeApp()
  
  # Рендер функция из Pug
  render: (new Function '_ctx', '_cache', globalThis.renderFns['app/index.pug'])()
})


Router = require 'app/router/index.coffee'
# Регистрация глобальных компонентов
app.component('ui-button', require 'app/components/UI/Button/index.coffee')
app.component('notification-container', require 'app/components/UI/Notification/index.coffee')
app.component('app-loader', require 'app/components/UI/AppLoader/index.coffee')
app.component('multilevel-menu', require 'app/components/Domain/MultilevelMenu/index.coffee')
app.component('theme-toggle', require 'app/components/UI/ThemeToggle/index.coffee')
app.component('language-toggle', require 'app/components/UI/LanguageToggle/index.coffee')
app.component('cart-widget', require 'app/components/Domain/CartWidget/index.coffee')

# Подключение роутера
app.use Router

# Глобальная обработка ошибок Vue
app.config.errorHandler = (err, vm, info) ->
  log '💥 Vue ошибка:', err, info
  console.error('Vue ошибка:', err, info)

# Глобальная обработка предупреждений
app.config.warnHandler = (msg, vm, trace) ->
  log '⚠️ Vue предупреждение:', msg, trace

# Монтирование приложения
try
  app.mount('body')
  log '✅ Приложение успешно смонтировано'
catch error
  log '❌ Ошибка монтирования приложения:', error
  console.error('Ошибка монтирования:', error)

Пример файла компонента, или страницы.Пиши стилевые файлы к элементам Важно не нужно делать Vue = require 'vue' или require 'VueRouter' они уже подключены глобально доступны

** app/pages/Home/index.coffee**

# app/pages/Home/index.coffee

# Добавление стилей страницы
if globalThis.stylFns and globalThis.stylFns['app/pages/Home/index.styl']
  styleElement = document.createElement('style')
  styleElement.type = 'text/css'
  styleElement.textContent = globalThis.stylFns['app/pages/Home/index.styl']
  document.head.appendChild(styleElement)
else
  log '⚠️ Стили главной страницы не найдены'

module.exports = {
  # Импорт компонентов 
  components: {
    'product-grid': require 'app/components/Domain/ProductGrid/index.coffee'
  }

  props:
    domainSettings:
      type: Object
      default: -> {}
    language:
      type: String
      default: 'ru'

  data: ->
    {
      welcomeText: 'Добро пожаловать в Браер-Колор'
      loading: false
      productsLoading: false
      featuredProducts: []
      features: [
        {
          title: 'Качественные материалы'
          description: 'Широкий ассортимент красок, грунтовок и лакокрасочных материалов от проверенных производителей'
        }
        {
          title: 'Доставка по России'
          description: 'Быстрая и надежная доставка в любой регион страны. Работаем с ведущими транспортными компаниями'
        }
        {
          title: 'Профессиональные консультации'
          description: 'Наши специалисты помогут подобрать оптимальные материалы для ваших задач и бюджета'
        }
      ]
    }

  methods:
    goToCatalog: ->
      @$router.push '/catalog'

    contactSupport: ->
      @$emit 'show-notification', 'Форма обратной связи будет добавлена позже', 'info'

    viewProduct: (productId) ->
      @$router.push "/product/#{productId}"

    loadFeaturedProducts: ->
      @productsLoading = true
      # Заглушка для загрузки товаров
      setTimeout (=>
        @featuredProducts = [
          { id: 1, name: 'Грунтовка глубокого проникновения', price: 528, image: '' },
          { id: 2, name: 'Краска акриловая белая', price: 890, image: '' },
          { id: 3, name: 'Эмаль для металла', price: 670, image: '' }
        ]
        @productsLoading = false
      ), 1000

  mounted: ->
    log 'Главная страница загружена'
    @loadFeaturedProducts()

  render: (new Function '_ctx', '_cache', globalThis.renderFns['app/pages/Home/index.pug'])()
}

# app/components/UI/Button/index.coffee

# Добавление стилей компонента
if globalThis.stylFns and globalThis.stylFns['app/components/UI/Button/index.styl']
  styleElement = document.createElement('style')
  styleElement.type = 'text/css'
  styleElement.textContent = globalThis.stylFns['app/components/UI/Button/index.styl']
  document.head.appendChild(styleElement)
else
  log '⚠️ Стили кнопки не найдены'

module.exports = 
  name: 'ui-button'
  props:
    type:
      type: String
      default: 'primary'
      validator: (value) ->
        ['primary', 'secondary', 'success', 'danger', 'outline'].includes(value)
    size:
      type: String  
      default: 'medium'
      validator: (value) ->
        ['small', 'medium', 'large'].includes(value)
    disabled:
      type: Boolean
      default: false
    loading:
      type: Boolean
      default: false

  methods:
    handleClick: (event) ->
      if not @disabled and not @loading
        @$emit 'click', event

  render: (new Function '_ctx', '_cache', globalThis.renderFns['app/components/UI/Button/index.pug'])()


app/router/index.coffee пример, VueRouter определён глобально вызывать отдельно не нужно

# app/router/index.coffee
config = require 'app/config'


# Middleware для проверки прав доступа
authGuard = (to, from, next) ->
  log 'Проверка прав доступа для route:', to.path
  # Здесь будет логика проверки пользователя из глобального состояния
  if to.matched.some (record) -> record.meta.requiresAuth
    # Проверка авторизации
    next() # Временная заглушка - всегда разрешаем доступ
  else
    next()

domainMiddleware = (to, from, next) ->
  log 'Обработка динамического домена для route'
  # Логика обработки домена будет интегрирована позже
  next()

router = VueRouter.createRouter({
  history: VueRouter.createWebHistory()
  routes: [
    {
      path: '/'
      name: 'Home'
      component: require 'app/pages/Home/index.coffee'
      beforeEnter: [domainMiddleware]
    }
    {
      path: '/catalog'
      name: 'Catalog'
      component: require 'app/pages/Catalog/index.coffee'
      beforeEnter: [domainMiddleware]
    }
    {
      path: '/catalog/:category?'
      name: 'CatalogCategory'
      component: require 'app/pages/Catalog/index.coffee'
      beforeEnter: [domainMiddleware]
    }
    {
      path: '/product/:id'
      name: 'Product'
      component: require 'app/pages/Product/index.coffee'
      beforeEnter: [domainMiddleware]
    }
    {
      path: '/cart'
      name: 'Cart'
      component: require 'app/pages/Cart/index.coffee'
      beforeEnter: [domainMiddleware]
    }
    {
      path: '/admin'
      name: 'Admin'
      component: require 'app/pages/Admin/index.coffee'
      meta: { requiresAuth: true }
      beforeEnter: [domainMiddleware, authGuard]
    }
    {
      path: '/:pathMatch(.*)*'
      name: 'NotFound'
      component: require 'app/pages/NotFound/index.coffee'
      beforeEnter: [domainMiddleware]
    }
  ]
})

# Глобальные обработчики роутера
router.beforeEach (to, from, next) ->
  log "Переход с "+ from.path +" на "+to.path+""
  next()

router.afterEach (to, from) ->
  log "Навигация завершена на "+to.path+""

module.exports = router





## 🚨 КРИТИЧЕСКИЕ ПРАВИЛА РАЗРАБОТКИ

### ❌ **ЗАПРЕЩЕНО:**

1. **Использование rgba() функций в стилях**

stylus // ❌ НЕПРАВИЛЬНО background: rgba(44, 90, 160, 0.1)

// ✅ ПРАВИЛЬНО
background: var(--color-primary-10)


2. **Прямое использование console.log**

coffee # ❌ НЕПРАВИЛЬНО console.log 'Ошибка'

# ✅ ПРАВИЛЬНО log 'Ошибка'


3. **Многострочные атрибуты в Pug**

pug // ❌ НЕПРАВИЛЬНО div(

 class="class"
 data-attr="value"

)

// ✅ ПРАВИЛЬНО div(class="class" data-attr="value")


4. **@import в стилях компонентов**

stylus // ❌ НЕПРАВИЛЬНО @import '../../index.styl'

// ✅ ПРАВИЛЬНО .my-component

 background: var(--color-primary-10)

5. **JavaScript операторы в CoffeeScript**

coffee # ❌ НЕПРАВИЛЬНО if user && user.role condition ? 'yes' : 'no'

# ✅ ПРАВИЛЬНО if user and user.role if condition then 'yes' else 'no'


6. **HTML/HEAD/BODY теги в компонентах**

pug // ❌ НЕПРАВИЛЬНО html

 body
   div.app

// ✅ ПРАВИЛЬНО div(class="app")


7. **Объявление debug в компонентах**

coffee # ❌ НЕПРАВИЛЬНО debug = require 'debug' log = debug.log

# ✅ ПРАВИЛЬНО - debug глобальный log 'Сообщение'

8. **Важно подстановка переменных в строки через конкатенацию**

coffee # ❌ НЕПРАВИЛЬНО ✅ Дизайн-документ создан: ${designDoc._id}

# ✅ ПРАВИЛЬНО "✅ Дизайн-документ создан: "+designDoc._id


### ✅ **ОБЯЗАТЕЛЬНО:**

1. **Полные листинги файлов** - всегда приводи все три файла компонента
2. **Адаптивная верстка** - mobile-first для всех компонентов
3. **BEM методология** - `block__element--modifier`
4. **CSS переменные** - только через var(--variable-name)
5. **Обработка ошибок** - try/catch для всех асинхронных операций
6. **Валидация данных** - проверка обязательных полей
7. **Прогресс операций** - индикаторы для длительных процессов
8. **Мультиязычность** - все тексты из настроек домена
9. **Использование глобального debug** - только `log 'сообщение'`

## 🗂️ СТРУКТУРА ДАННЫХ

### 📊 ТИПЫ ДОКУМЕНТОВ

**app/types/data.coffee**

coffee class DomainEntity constructor: ->

@_id = ''
@type = ''
@domains = []
@createdAt = new Date().toISOString()
@updatedAt = new Date().toISOString()
@active = true

class Product extends DomainEntity constructor: ->

super()
@type = 'product'
@name = ''
@sku = ''
@price = 0
@oldPrice = null
@category = ''
@brand = ''
@description = ''
@attributes = {}
@images = []
@richContent = null
@inStock = true
@weight = 0
@volume = 0

class Category extends DomainEntity constructor: ->

super()
@type = 'category'
@name = ''
@slug = ''
@parent = null
@order = 0
@image = ''
@description = ''

class HeroSlide extends DomainEntity constructor: ->

super()
@type = 'hero_slide'
@title = ''
@subtitle = ''
@image = ''
@buttonText = 'В каталог'
@buttonLink = '/catalog'
@order = 0

class DomainSettings extends DomainEntity constructor: ->

super()
@type = 'domain_settings'
@domain = ''
@companyName = ''
@languages = ['ru']
@theme = 'light'
@contacts = {}
@seo = {}
@social = {}

module.exports = {Product, Category, HeroSlide, DomainSettings, DomainEntity}


## 🔧 СЕРВИСЫ И УТИЛИТЫ

### 🗃️ POUCHDB СЕРВИС

**app/utils/pouch.coffee**

coffee class PouchDBService constructor: (options = {}) ->

{@localDbName, @remoteDbUrl, @userFilter, @appVersion} = options
@localDb = null
@remoteDb = null
@initialized = false

init: ->

return Promise.resolve() if @initialized

try
  @localDb = new PouchDB(@localDbName or 'braer_color_cache')
  await @ensureRemoteDatabase()
  await @loadDesignDocs()
  await @ensureDesignDocs()

  # Настройка селективной синхронизации
  PouchDB.sync(@remoteDb, @localDb, {
    live: true,
    retry: true,
    filter: (doc) => @shouldSyncDocument(doc)
  })

  @initialized = true
  return Promise.resolve()
catch error
  log 'Ошибка инициализации PouchDB:', error
  return Promise.reject(error)

getDocument: (docId) ->

@ensureInit()

try
  return await @localDb.get(docId)
catch localError
  if localError.status == 404
    try
      doc = await @remoteDb.get(docId)
      await @localDb.put(doc)
      return doc
    catch remoteError
      throw remoteError
  else
    throw localError

saveToRemote: (doc) ->

@ensureInit()

try
  existingDoc = await @remoteDb.get(doc._id)
  doc._rev = existingDoc._rev
  return await @remoteDb.put(doc)
catch error
  if error.status == 404
    return await @remoteDb.put(doc)
  else
    throw error

shouldSyncDocument: (doc) ->

if doc.type in ['product', 'category', 'settings', 'hero_slide', 'blog_article', 'route']
  return true
if doc.type in ['order', 'user_data', 'cart']
  return doc.userId == @userFilter?.userId
return false

module.exports = new PouchDBService({ localDbName: 'braer_color_cache' remoteDbUrl: 'http://localhost:5984/braer_color_shop' userFilter: { userId: 'current_user_id' } appVersion: '1.0.0' })


### 📦 СЕРВИС ИМПОРТА ТОВАРОВ


Важно: при полной реализации используй пример csv файла, При этом учти что количество полей в документе может меняться. выдели из них основные, остальные загружай по факту как свойства товара.
Индивидуальное сохранение товаров
Каждый товар сохраняется как отдельный документ в PouchDB
Используется стабильный _id на основе артикула: product:#{sku}
Автоматическое обновление существующих товаров при повторном импорте
Улучшенная обработка изображений
Загрузка основного и дополнительных изображений как attachments
Формат ссылок: /d/braer_color_shop/{doc_id}/{filename}
Все фото из csv должны быть загружены как attachment в документ товара
Объеденяй товары в группы по полю из csv файла, в списке товаров выводи главное изображение.
в редакторе нужно иметь возможность управлять всеми данными товара. некоторые атрибуты иметь возможность отметить как скрытые, по из названи. (нужно упаравление атрибутами в категории)
Прогресс импорта
Визуальный индикатор прогресса обработки
Отслеживание количества обработанных товаров
Детальная статистика по завершении импорта
Обработка Rich-контента
Преобразование JSON в Markdown для описаний товаров
Сохранение оригинальной структуры для возможного редактирования
Создать редактор товаров

Создание объектов катигорий при появлении новых значений в поле "Тип*", сделать редактор категорий, с возможностью загрузки фото
При обработке csv загружай изображения в базу данных как attachment, также сохраняй ричконтент преобразовывая в markdown, и выводи его в качестве описания товара при его наличии.
формат ссылок на attachment файлы "/d/[ИМЯ БД]/[id doc]/[имя файла]"
Важно: каждый товар должен быть привязан к домену/доменам (может одновременно быть доступен на разных доменах), тоже касается статей блога.



**app/services/ImportService.coffee**

coffee { Product, Category } = require 'app/types/data'

class ImportService constructor: ->

@batchSize = 50
@maxImages = 5

transformProductData: (csvRow, index, domain) ->

product = new Product()

product._id = "product:#{csvRow['Артикул*']}"
product.name = csvRow['Название товара']
product.sku = csvRow['Артикул*']
product.price = parseFloat(csvRow['Цена, руб.*'].replace(',', '.')) or 0
product.oldPrice = if csvRow['Цена до скидки, руб.'] then parseFloat(csvRow['Цена до скидки, руб.'].replace(',', '.')) else null
product.brand = csvRow['Бренд*']
product.category = csvRow['Тип*']
product.domains = [domain]

product.images = @processImages(csvRow, product._id)

if csvRow['Rich-контент JSON']
  try
    product.richContent = @jsonToMarkdown(JSON.parse(csvRow['Rich-контент JSON']))
  catch error
    log 'Ошибка парсинга rich-контента:', error

product.attributes = @extractAttributes(csvRow)

return product

processImages: (csvRow, docId) ->

images = []

if csvRow['Ссылка на главное фото*']
  images.push {
    url: csvRow['Ссылка на главное фото*']
    type: 'main'
    order: 0
  }

if csvRow['Ссылки на дополнительные фото']
  additionalImages = csvRow['Ссылки на дополнительные фото'].split('\n').slice(0, @maxImages)
  additionalImages.forEach (imgUrl, index) ->
    if imgUrl.trim()
      images.push {
        url: imgUrl.trim()
        type: 'additional'
        order: index + 1
      }

return images

jsonToMarkdown: (richContent) ->

markdown = ''

if richContent?.content
  richContent.content.forEach (block) ->
    if block.widgetName == 'raTextBlock' and block.text?.items
      block.text.items.forEach (item) ->
        if item.type == 'text' and item.content
          markdown += item.content + '\n\n'
        else if item.type == 'br'
          markdown += '\n'

return markdown.trim()

extractAttributes: (csvRow) ->

attributes = {}

technicalFields = [
  'Вес в упаковке, г*', 'Ширина упаковки, мм*', 'Высота упаковки, мм*', 
  'Длина упаковки, мм*', 'Объем, л', 'Время высыхания, часов', 'Расход, л/м2',
  'Макс. температура эксплуатации, С°', 'Количество компонентов'
]

technicalFields.forEach (field) ->
  if csvRow[field]
    attributes[field] = csvRow[field]

categoryFields = [
  'Вид краски', 'Назначение грунтовки', 'Материал основания', 'Основа краски',
  'Способ нанесения', 'Назначение', 'Тип помещения', 'Возможность колеровки'
]

categoryFields.forEach (field) ->
  if csvRow[field]
    attributes[field] = csvRow[field]

return attributes

importFromCSV: (file, domain, onProgress) ->

return new Promise (resolve, reject) =>
  reader = new FileReader()

  reader.onload = (e) =>
    try
      results = Papa.parse(e.target.result, {
        header: true
        delimiter: ';'
        skipEmptyLines: true
        encoding: 'UTF-8'
      })

      validProducts = results.data.filter (row) => 
        row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*']

      this.processProductsInBatches(validProducts, domain, onProgress)
        .then(resolve)
        .catch(reject)

    catch error
      reject(new Error("Ошибка парсинга CSV: "+error.message))

  reader.onerror = -> reject(new Error('Ошибка чтения файла'))
  reader.readAsText(file, 'UTF-8')

processProductsInBatches: (products, domain, onProgress) ->

batches = []
for i in [0...products.length] by @batchSize
  batches.push(products.slice(i, i + @batchSize))

processed = 0
total = products.length

processBatch = (batch) =>
  transformedProducts = batch.map (product, index) =>
    @transformProductData(product, processed + index, domain)

  return PouchDBService.bulkDocs(transformedProducts)
    .then (results) =>
      processed += batch.length
      if onProgress
        onProgress({
          processed: processed
          total: total
          percentage: Math.round((processed / total) * 100)
        })
      return results

return batches.reduce (promise, batch) =>
  promise.then -> processBatch(batch)
, Promise.resolve()

module.exports = new ImportService()


## 📊 ОБРАЗЕЦ CSV ФАЙЛА ДЛЯ ИМПОРТА

### 📝 СПЕЦИФИКАЦИЯ CSV
**Разделитель:** `;` (точка с запятой)  
**Кодировка:** UTF-8  
**Обязательные поля:** `Артикул*`, `Название товара`, `Цена, руб.*`

### 🎯 ПРИМЕР CSV СТРОКИ

csv №;Артикул;Название товара;Цена, руб.;Цена до скидки, руб.;НДС, %;Рассрочка;Баллы за отзывы;SKU;Штрихкод (Серийный номер / EAN);Вес в упаковке, г;Ширина упаковки, мм;Высота упаковки, мм;Длина упаковки, мм;Ссылка на главное фото;Ссылки на дополнительные фото;Ссылки на фото 360;Артикул фото;Бренд;Название модели (для объединения в одну карточку);Единиц в одном товаре;Цвет товара;Название цвета;Тип;Класс опасности товара;Степень блеска покрытия;Работы;Вес товара, г;Количество товара в УЕИ;#Хештеги;Аннотация;Rich-контент JSON;Название группы;Образец цвета;Партномер;Гарантия;Страна-изготовитель;Комплектация;ТН ВЭД коды ЕАЭС;Срок годности в днях;Количество заводских упаковок;Вид краски;Объем, л;Время высыхания, часов;Вес, кг;Расход, л/м2;Назначение грунтовки;Рекомендуемое количество слоев;Расход, кг/м2;Область применения состава;Количество компонентов;Особенности ЛКМ;Макс. температура эксплуатации, С°;Материал основания;Основа краски;Основа грунтовки;Способ нанесения;Форма выпуска средства;Назначение;Тип помещения;Возможность колеровки;Вид выпуска товара;Тип растворителя;Эффект краски;Марка эмали;Можно мыть;Базис;Аэрозоль;Помещение;Название модели для шаблона наименования;Ошибка;Предупреждение 1;4673764201943;Грунтовка глубокого проникновения для стен под обои и покраску ЭкоКрас 1кг;528,00;1 056,00;20;Да;Нет;;4673764201943;1000;100;55;250;https://cdn1.ozone.ru/s3/multimedia-1-o/7663357104.jpg;"https://cdn1.ozone.ru/s3/multimedia-1-8/7663357124.jpg https://cdn1.ozone.ru/s3/multimedia-1-w/7663352504.jpg https://cdn1.ozone.ru/s3/multimedia-1-r/7663357179.jpg https://cdn1.ozone.ru/s3/multimedia-1-b/7663352483.jpg https://cdn1.ozone.ru/s3/multimedia-1-c/7663352556.jpg https://cdn1.ozone.ru/s3/multimedia-1-t/7663352537.jpg https://cdn1.ozone.ru/s3/multimedia-1-o/7663352496.jpg https://cdn1.ozone.ru/s3/multimedia-1-k/7663352492.jpg https://cdn1.ozone.ru/s3/multimedia-1-y/7663365970.jpg https://cdn1.ozone.ru/s3/multimedia-1-m/7663365922.jpg https://cdn1.ozone.ru/s3/multimedia-1-1/7663365937.jpg https://cdn1.ozone.ru/s3/multimedia-1-p/7663352533.jpg";;;ЭкоКрас;4673764201943;1;прозрачный;;Грунтовка;Не опасен;;"Внутренние;Наружные";;;#Грунтовка #ГрунтДляСтен #АкриловаяГрунтовка #СтроительныеМатериалы #ГлубокогоПроникновения #УниверсальнаяГрунтовка #АдгезионнаяГрунтовка #АнтисептическаяГрунтовка #ДляВнутреннихРабот #ДляНаружныхРабот #ДляБетона #ДляГипсокартона #ДляДерева #Быстросохнущая #Водостойкая #Укрепляющая #Противогрибковая #БелаяГрунтовка #ПодОбои #ПодШтукатурку #ПодПокраску #ПодПлитку #ДляРовныхСтен #РемонтДома #ОтделкаПомещений #СтроительныеРаботы #КачественныйРемонт #СтроительныеТовары #профессиональная_грунтовка #грунтовка_концентрат_премиум;"Премиум грунтовка глубокого проникновения для подготовки поверхностей перед финишной отделкой...";"{""content"": [{""widgetName"": ""raTextBlock"", ""title"": {""items"": [{""type"": ""text"", ""content"": ""Грунтовка глубокого проникновения PRIMER 1:4 концентрат ImPasto 1кг""}], ""size"": ""size5"", ""color"": ""color1""}, ""theme"": ""primary"", ""padding"": ""type2"", ""gapSize"": ""m"", ""text"": {""size"": ""size2"", ""align"": ""left"", ""color"": ""color1"", ""items"": [{""type"": ""text"", ""content"": ""Премиум грунтовка глубокого проникновения для подготовки поверхностей перед финишной отделкой...""}]}}], ""version"": 0.3}";экокрас;https://cdn1.ozone.ru/s3/multimedia-1-q/7218190898.jpg;;2 года;Россия;Грунтовка глубокого проникновения ЭкоКрас 1кг- 1шт;;990;1;;1;24;1;0,05;"Глубокого проникновения;Обеспыливающая;Пропиточная;Укрепляющая;Универсальная";;;"По бетону;Для фасадов;Для хобби и творчества;Для швов;По кирпичу;По штукатурке;Универсальная";;;;"Бетон;Газобетон;Кирпич;Пенобетон;Штукатурка";;Акриловая;"Валик;Кисть;Краскопульт;Пистолет";Готовый раствор;;"С повышенной влажностью;С умеренной влажностью;Сухое";;;;;;;;;;;;


### 🔧 ОБРАБОТКА НЕОБЯЗАТЕЛЬНЫХ ПОЛЕЙ

**ВАЖНО:** Система должна обрабатывать ЛЮБЫЕ необязательные поля, даже с неизвестными именами. Все дополнительные поля сохраняются в атрибуты товара.

coffee

Пример обработки произвольных полей

extractAllAttributes: (csvRow) -> attributes = {}

# Обязательные технические поля requiredFields = [

'Вес в упаковке, г*', 'Ширина упаковки, мм*', 'Высота упаковки, мм*', 
'Длина упаковки, мм*', 'Объем, л', 'Время высыхания, часов'

]

# Обрабатываем все поля CSV for key, value of csvRow

if value and value.toString().trim() != ''
  # Сохраняем все поля, кроме системных
  if not key in ['№', 'Артикул*', 'Название товара', 'Цена, руб.*', 'Ссылка на главное фото*', 'Ссылки на дополнительные фото']
    attributes[key] = value.toString().trim()

log "Извлечено атрибутов: "+Object.keys(attributes).length return attributes


## 🎯 ПОДРОБНОЕ ЛОГГИРОВАНИЕ

### 📝 ТРЕБОВАНИЯ К ЛОГГИРОВАНИЮ

**Обязательно логировать:**
- Начало и завершение всех основных операций
- Ошибки с полным стектрейсом
- Прогресс длительных операций (импорт, синхронизация)
- Пользовательские действия (добавление в корзину, авторизация)
- Изменения состояния приложения
- Запросы к базе данных

**Пример детального логгирования:**

coffee importFromCSV: (file, domain, onProgress) -> log "🚀 Начало импорта CSV файла: "+file.name log "📊 Домен для импорта: "+domain log "📁 Размер файла: "+file.size+" байт"

return new Promise (resolve, reject) =>

reader = new FileReader()

reader.onload = (e) =>
  try
    log "✅ Файл успешно прочитан, начало парсинга CSV"
    results = Papa.parse(e.target.result, {
      header: true
      delimiter: ';'
      skipEmptyLines: true
      encoding: 'UTF-8'
    })

    log "📈 CSV распарсен: #{results.data.length} строк найдено"

    validProducts = results.data.filter (row) => 
      isValid = row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*']
      if not isValid
        log "⚠️ Пропущена строка из-за отсутствия обязательных полей: "+JSON.stringify(row)
      return isValid

    log "✅ Валидных товаров для импорта: "+validProducts.length

    this.processProductsInBatches(validProducts, domain, onProgress)
      .then (results) =>
        log "🎉 Импорт успешно завершен: "+results.length+" товаров обработано"
        resolve(results)
      .catch (error) =>
        log "❌ Ошибка в процессе импорта: "+error.message
        reject(error)

  catch error
    log "💥 Критическая ошибка парсинга CSV: "+error.message
    log "🔍 Stack trace:", error.stack
    reject(new Error("Ошибка парсинга CSV: "+error.message))

reader.onerror = (error) ->
  log "❌ Ошибка чтения файла: "+error
  reject(new Error('Ошибка чтения файла'))

reader.readAsText(file, 'UTF-8')

```

🚨 КРИТИЧЕСКИЕ ПРАВИЛА РАЗРАБОТКИ

ЗАПРЕЩЕНО:

  1. Использование rgba() функций в стилях
  2. Прямое использование console.log (только глобальный log)
  3. Многострочные атрибуты в Pug
  4. @import в стилях компонентов
  5. JavaScript операторы в CoffeeScript
  6. Объявление debug в компонентах

ОБЯЗАТЕЛЬНО:

  1. Полные листинги файлов компонентов
  2. Адаптивная верстка (mobile-first)
  3. BEM методология именования классов
  4. CSS переменные для цветов и прозрачностей
  5. Подробное логгирование всех операций
  6. Обработка всех необязательных полей CSV

📅 ПОЭТАПНЫЙ ПЛАН РАЗРАБОТКИ

🎯 ЭТАП 1: БАЗОВАЯ АРХИТЕКТУРА ✅

  • Настройка сборки и конфигурации
  • Базовая структура Vue приложения
  • PouchDB сервис и дизайн-документы
  • Главный layout и система стилей
  • Система типов и интерфейсов

🎯 ЭТАП 2: АДМИН-ПАНЕЛЬ ⚠️ В РАБОТЕ

  • Layout админ-панели
  • Компонент управления товарами
  • ТЕКУЩАЯ ЗАДАЧА: Система импорта товаров
  • Редактор категорий с загрузкой изображений
  • Управление слайдами главной страницы
  • Управление статьями блога
  • Настройки доменов и мультиязычности

🎯 ЭТАП 3: КЛИЕНТСКАЯ ЧАСТЬ

  • Главная страница с адаптивным дизайном
  • Каталог товаров с фильтрацией
  • Страница товара с галереей
  • Корзина и оформление заказа
  • Блог и система статей

🎯 ЭТАП 4: ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ

  • Поиск по товарам
  • Система отзывов и рейтингов
  • Личный кабинет пользователя
  • Система скидок и промокодов
  • Интеграция с платежными системами

🎯 ЭТАП 5: ОПТИМИЗАЦИЯ И ТЕСТИРОВАНИЕ

  • PWA функциональность (офлайн-режим)
  • Оптимизация производительности
  • Тестирование на разных устройствах
  • Деплой и настройка продакшн-окружения

📊 ТЕКУЩОЕ СОСТОЯНИЕ ПРОЕКТА

✅ ВЫПОЛНЕНО

  1. Базовая архитектура
    • Напиши базовые файлы системы
    • Система роутинга
    • Глобальная конфигурация Добавлено подключение компонентов во все объекты где они используются:

Главное приложение подключает все UI и Domain компоненты

Страницы подключают используемые компоненты через components

Роутер подключает все страницы приложения

Создана полная структура компонентов:

UI компоненты: Button, Notification, AppLoader, ThemeToggle, LanguageToggle

Domain компоненты: MultilevelMenu, CartWidget, ProductGrid, ProductCard

Страницы: Home, Catalog, Product, Cart, Admin, NotFound

Настроена правильная иерархия зависимостей:

Каждый компонент самостоятельно подключает свои стили

Родительские компоненты регистрируют дочерние через components

Главное приложение регистрирует глобальные компоненты

Созданы все необходимые заглушки для полнофункционального приложения Полноценный PouchDB сервис:

Подключение к CouchDB с системой аутентификации

Двусторонняя синхронизация с фильтрацией по доменам

Обработка ошибок и автоматические повторы

Локальное кэширование и офлайн-работа

Дизайн-документы CouchDB:

Views для товаров, категорий, заказов с индексацией

Validate_doc_update функции для валидации данных

Система поисковых индексов для быстрого поиска

Сервисы данных:

ProductService: работа с товарами, поиск, фильтрация

CategoryService: управление категориями, иерархия

DomainService: мультидоменность, настройки доменов

Интеграция в приложение:

Правильная инициализация всех сервисов

Обработка состояний загрузки и ошибок

Подробное логгирование всех операций Layout администратора с навигацией:

Боковая панель с навигационным меню

Хедер с хлебными крошками и информацией пользователя

Адаптивный дизайн для мобильных устройств

Компонент DataTable:

Сортировка по колонкам

Поиск и фильтрация данных

Пагинация

Выбор элементов

Слоты для кастомных действий

Компонент загрузки файлов:

Drag & drop интерфейс

Валидация типов и размера файлов

Индикатор прогресса загрузки

Поддержка множественного выбора

Настроена полная маршрутизация для админ-панели:

Родительский маршрут /admin с проверкой авторизации

Дочерние маршруты через children:

/admin/dashboard - Дашборд

/admin/products - Управление товарами

/admin/categories - Управление категориями

/admin/import - Импорт товаров

/admin/media - Медиа-менеджер

Реализована система breadcrumbs:

Динамическое обновление хлебных крошек

Поддержка мета-данных в маршрутах

Созданы все необходимые страницы:

Дашборд со статистикой и быстрыми действиями

Заглушки для остальных разделов

Полная навигация между разделами

Улучшена безопасность:

Проверка роли пользователя (только admin)

Редирект при отсутствии прав доступа

🎯 БЛИЖАЙШИЕ ЗАДАЧИ

  1. Работа с данными

    • PouchDB сервис с синхронизацией
    • Дизайн-документы для CouchDB
    • Система типов данных
  2. Базовые компоненты

    • Главный layout приложения
    • Layout админ-панели
    • Система уведомлений
  3. Стили и дизайн

    • CSS переменные и дизайн-система
    • Адаптивная верстка
    • Темная/светлая темы
  4. Система импорта товаров

    • Компонент загрузки CSV файлов
    • Парсинг и валидация данных
    • Пакетная обработка товаров
    • Создание категорий на лету

    5.1. Завершить компонент импорта товаров 5.2. Реализовать редактор категорий 5.3. Создать компонент управления слайдами 5.4. Разработать главную страницу магазина

  5. Админ-панель

    • Управление товарами
    • Система прогресса операций
    • Обработка ошибок импорта

🔄 В РАБОТЕ СЕЙЧАС

отвечай на русском Анализировать реализованный код, по git репозитарию https://gogs.osvoj.ru/oleg/s5l.ru-crm Проверяй промт и изменения в нём по адресу https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md после выполнения задачи, напиши

  • что сделано.
  • Напиши следующую задачу на выполнение (после завершения отладки, созданных файлов) напиши файлы реализующие следующую задачу. ⚠️ ПРИОРИТЕТ

ЭТАП 1.8: РЕАЛИЗАЦИЯ РЕДАКТОРА КАТЕГОРИЙ И МЕДИА-МЕНЕДЖЕРА

Редактор категорий:

Древовидное отображение с drag&drop

Загрузка изображений категорий

Редактирование метаданных и SEO

Медиа-менеджер:

Загрузка изображений с превью

Прикрепление файлов к документам CouchDB

Система кэширования и оптимизации

Интеграция:

Связь медиа-файлов с товарами и категориями

Управление версиями файлов

Оптимизация производительности

Приоритет: Критический 🚨 (завершает базовый функционал админ-панели)

Приоритет: Высокий ⚠️ (необходимо для наполнения магазина товарами)