Gogs 3 долоо хоног өмнө
parent
commit
d28dd68b4d

+ 46 - 12
README.md

@@ -1311,7 +1311,35 @@ DomainService: мультидоменность, настройки домено
 Обработка состояний загрузки и ошибок
 Обработка состояний загрузки и ошибок
 
 
 Подробное логгирование всех операций
 Подробное логгирование всех операций
+Layout администратора с навигацией:
 
 
+Боковая панель с навигационным меню
+
+Хедер с хлебными крошками и информацией пользователя
+
+Адаптивный дизайн для мобильных устройств
+
+Компонент DataTable:
+
+Сортировка по колонкам
+
+Поиск и фильтрация данных
+
+Пагинация
+
+Выбор элементов
+
+Слоты для кастомных действий
+
+Компонент загрузки файлов:
+
+Drag & drop интерфейс
+
+Валидация типов и размера файлов
+
+Индикатор прогресса загрузки
+
+Поддержка множественного выбора
 ### 🎯 БЛИЖАЙШИЕ ЗАДАЧИ
 ### 🎯 БЛИЖАЙШИЕ ЗАДАЧИ
 
 
 2. **Работа с данными**
 2. **Работа с данными**
@@ -1359,33 +1387,39 @@ DomainService: мультидоменность, настройки домено
 напиши файлы реализующие следующую задачу.
 напиши файлы реализующие следующую задачу.
  ⚠️ ПРИОРИТЕТ
  ⚠️ ПРИОРИТЕТ
 
 
-ЭТАП 1.5: РАЗРАБОТКА АДМИН-ПАНЕЛИ И СИСТЕМЫ ИМПОРТА
+ЭТАП 1.6: РЕАЛИЗАЦИЯ СИСТЕМЫ ИМПОРТА И МЕДИА-МЕНЕДЖЕРА
+
+Страница импорта товаров:
 
 
-Создание админ-панели:
+Интеграция компонента FileUpload
 
 
-Layout администратора с навигацией
+Парсинг CSV файлов с валидацией
 
 
-Компонент управления товарами (DataTable)
+Преобразование данных в структуру PouchDB
 
 
-Редактор категорий с загрузкой изображений
+Пакетная обработка и прогресс-бар
 
 
-Система импорта товаров:
+Редактор категорий:
 
 
-Компонент загрузки CSV файлов
+Древовидная структура категорий
 
 
-Парсинг и валидация данных CSV
+Загрузка изображений для категорий
 
 
-Пакетная обработка товаров
+Drag & drop для изменения иерархии
 
 
-Создание категорий на лету
+Редактирование метаданных
 
 
-Управление медиа-файлами:
+Медиа-менеджер:
 
 
 Загрузка изображений товаров
 Загрузка изображений товаров
 
 
 Прикрепление файлов к документам CouchDB
 Прикрепление файлов к документам CouchDB
 
 
-Система кэширования медиа
+Система кэширования и оптимизации
+
+Управление версиями файлов
+
+Приоритет: Критический 🚨 (необходимо для наполнения магазина товарами и контентом)
 
 
 Приоритет: Высокий ⚠️ (необходимо для наполнения магазина товарами)
 Приоритет: Высокий ⚠️ (необходимо для наполнения магазина товарами)
 
 

+ 99 - 0
app/components/Admin/DataTable/index.coffee

@@ -0,0 +1,99 @@
+
+if globalThis.stylFns and globalThis.stylFns['app/components/Admin/DataTable/index.styl']
+  styleElement = document.createElement('style')
+  styleElement.type = 'text/css'
+  styleElement.textContent = globalThis.stylFns['app/components/Admin/DataTable/index.styl']
+  document.head.appendChild(styleElement)
+
+module.exports = 
+  name: 'data-table'
+  props:
+    data:
+      type: Array
+      default: -> []
+    columns:
+      type: Array
+      default: -> []
+    loading:
+      type: Boolean
+      default: false
+    selectable:
+      type: Boolean
+      default: false
+
+  data: ->
+    {
+      selectedItems: []
+      sortField: ''
+      sortDirection: 'asc'
+      currentPage: 1
+      pageSize: 20
+      searchQuery: ''
+    }
+
+  computed:
+    filteredData: ->
+      if !@searchQuery
+        return @data
+      
+      query = @searchQuery.toLowerCase()
+      return @data.filter (item) =>
+        @columns.some (col) =>
+          value = item[col.key]
+          return false unless value?
+          String(value).toLowerCase().includes(query)
+
+    sortedData: ->
+      if !@sortField
+        return @filteredData
+      
+      return @filteredData.sort (a, b) =>
+        aVal = a[@sortField]
+        bVal = b[@sortField]
+        
+        if aVal < bVal
+          return @sortDirection == 'asc' ? -1 : 1
+        if aVal > bVal
+          return @sortDirection == 'asc' ? 1 : -1
+        return 0
+
+    paginatedData: ->
+      startIndex = (@currentPage - 1) * @pageSize
+      endIndex = startIndex + @pageSize
+      return @sortedData.slice(startIndex, endIndex)
+
+    totalPages: ->
+      Math.ceil(@sortedData.length / @pageSize)
+
+  methods:
+    sort: (field) ->
+      if @sortField == field
+        @sortDirection = if @sortDirection == 'asc' then 'desc' else 'asc'
+      else
+        @sortField = field
+        @sortDirection = 'asc'
+
+    selectAll: (checked) ->
+      if checked
+        @selectedItems = [...@paginatedData]
+      else
+        @selectedItems = []
+
+    selectItem: (item, checked) ->
+      if checked
+        @selectedItems.push(item)
+      else
+        @selectedItems = @selectedItems.filter (i) -> i != item
+
+    isSelected: (item) ->
+      @selectedItems.includes(item)
+
+    editItem: (item) ->
+      @$emit 'edit', item
+
+    deleteItem: (item) ->
+      if confirm('Вы уверены, что хотите удалить этот элемент?')
+        @$emit 'delete', item
+
+  render: (new Function '_ctx', '_cache', globalThis.renderFns['app/components/Admin/DataTable/index.pug'])()
+

+ 85 - 0
app/components/Admin/DataTable/index.pug

@@ -0,0 +1,85 @@
+div(class="data-table")
+  div(class="data-table__toolbar")
+    div(class="data-table__search")
+      input(
+        type="text"
+        v-model="searchQuery"
+        placeholder="Поиск..."
+        class="data-table__search-input"
+      )
+    
+    div(class="data-table__actions")
+      slot(name="toolbar")
+  
+  div(v-if="loading" class="data-table__loading")
+    div(class="data-table__spinner")
+    span Загрузка данных...
+  
+  div(v-else class="data-table__container")
+    table(class="data-table__table")
+      thead
+        tr
+          th(v-if="selectable" class="data-table__checkbox-column")
+            input(
+              type="checkbox"
+              :checked="selectedItems.length === paginatedData.length && paginatedData.length > 0"
+              @change="selectAll($event.target.checked)"
+            )
+          
+          th(
+            v-for="column in columns"
+            :key="column.key"
+            @click="sort(column.key)"
+            :class="{ 'data-table__sortable': column.sortable !== false }"
+            class="data-table__header"
+          )
+            span {{ column.title }}
+            span(
+              v-if="sortField === column.key"
+              class="data-table__sort-indicator"
+            ) {{ sortDirection === 'asc' ? '↑' : '↓' }}
+      
+      tbody
+        tr(
+          v-for="item in paginatedData"
+          :key="item._id"
+          class="data-table__row"
+        )
+          td(v-if="selectable" class="data-table__checkbox-column")
+            input(
+              type="checkbox"
+              :checked="isSelected(item)"
+              @change="selectItem(item, $event.target.checked)"
+            )
+          
+          td(
+            v-for="column in columns"
+            :key="column.key"
+            class="data-table__cell"
+          )
+            template(v-if="column.slot")
+              slot(:name="column.slot" :item="item")
+            template(v-else)
+              span {{ item[column.key] }}
+          
+          td(v-if="$slots.actions" class="data-table__actions-cell")
+            slot(name="actions" :item="item")
+  
+  div(v-if="!loading && paginatedData.length === 0" class="data-table__empty")
+    span Нет данных для отображения
+  
+  div(v-if="!loading && totalPages > 1" class="data-table__pagination")
+    button(
+      @click="currentPage = currentPage - 1"
+      :disabled="currentPage === 1"
+      class="data-table__pagination-btn"
+    ) ←
+    
+    span(class="data-table__pagination-info")
+      | Страница {{ currentPage }} из {{ totalPages }}
+    
+    button(
+      @click="currentPage = currentPage + 1"
+      :disabled="currentPage === totalPages"
+      class="data-table__pagination-btn"
+    ) →

+ 160 - 0
app/components/Admin/DataTable/index.styl

@@ -0,0 +1,160 @@
+.data-table
+  background: var(--color-white)
+  border-radius: var(--border-radius)
+  box-shadow: var(--shadow-sm)
+  overflow: hidden
+  
+  .theme-dark &
+    background: var(--color-dark)
+    color: var(--color-light)
+
+.data-table__toolbar
+  padding: var(--spacing-lg)
+  border-bottom: 1px solid var(--border-color)
+  display: flex
+  justify-content: space-between
+  align-items: center
+  gap: var(--spacing-md)
+
+.data-table__search
+  flex: 1
+  max-width: 300px
+
+.data-table__search-input
+  width: 100%
+  padding: var(--spacing-sm) var(--spacing-md)
+  border: 1px solid var(--border-color)
+  border-radius: var(--border-radius)
+  font-family: var(--font-family)
+  
+  .theme-dark &
+    background: var(--color-dark-50)
+    color: var(--color-light)
+    border-color: var(--color-light-10)
+
+.data-table__actions
+  display: flex
+  gap: var(--spacing-sm)
+
+.data-table__loading
+  padding: var(--spacing-2xl)
+  text-align: center
+  color: var(--color-secondary)
+
+.data-table__spinner
+  width: 40px
+  height: 40px
+  border: 3px solid var(--color-primary-10)
+  border-top: 3px solid var(--color-primary)
+  border-radius: 50%
+  animation: data-table-spin 1s linear infinite
+  margin: 0 auto var(--spacing-md)
+
+@keyframes data-table-spin
+  0%
+    transform: rotate(0deg)
+  100%
+    transform: rotate(360deg)
+
+.data-table__container
+  overflow-x: auto
+
+.data-table__table
+  width: 100%
+  border-collapse: collapse
+
+.data-table__header
+  padding: var(--spacing-md)
+  text-align: left
+  font-weight: var(--font-weight-semibold)
+  border-bottom: 1px solid var(--border-color)
+  background: var(--color-light-10)
+  
+  .theme-dark &
+    background: var(--color-dark-50)
+
+.data-table__sortable
+  cursor: pointer
+  user-select: none
+  
+  &:hover
+    background: var(--color-primary-10)
+
+.data-table__sort-indicator
+  margin-left: var(--spacing-xs)
+  color: var(--color-primary)
+
+.data-table__checkbox-column
+  width: 40px
+  text-align: center
+
+.data-table__row
+  border-bottom: 1px solid var(--border-color)
+  
+  &:hover
+    background: var(--color-primary-10)
+
+.data-table__cell
+  padding: var(--spacing-md)
+  border-bottom: 1px solid var(--border-color)
+
+.data-table__actions-cell
+  padding: var(--spacing-md)
+  border-bottom: 1px solid var(--border-color)
+  
+  .data-table__actions
+    display: flex
+    gap: var(--spacing-xs)
+    justify-content: flex-end
+
+.data-table__empty
+  padding: var(--spacing-2xl)
+  text-align: center
+  color: var(--color-secondary)
+
+.data-table__pagination
+  padding: var(--spacing-lg)
+  display: flex
+  justify-content: center
+  align-items: center
+  gap: var(--spacing-md)
+  border-top: 1px solid var(--border-color)
+
+.data-table__pagination-btn
+  padding: var(--spacing-sm) var(--spacing-md)
+  border: 1px solid var(--border-color)
+  border-radius: var(--border-radius)
+  background: var(--color-white)
+  cursor: pointer
+  transition: var(--transition-fast)
+  
+  &:hover:not(:disabled)
+    background: var(--color-primary-10)
+    border-color: var(--color-primary)
+  
+  &:disabled
+    opacity: 0.5
+    cursor: not-allowed
+  
+  .theme-dark &
+    background: var(--color-dark)
+    color: var(--color-light)
+
+.data-table__pagination-info
+  color: var(--color-secondary)
+
+// Адаптивность
+@media (max-width: 768px)
+  .data-table__toolbar
+    flex-direction: column
+    align-items: stretch
+  
+  .data-table__search
+    max-width: none
+  
+  .data-table__actions
+    justify-content: flex-start
+  
+  .data-table__cell
+    padding: var(--spacing-sm)
+    font-size: var(--font-size-sm)

+ 107 - 0
app/components/Admin/FileUpload/index.coffee

@@ -0,0 +1,107 @@
+
+
+if globalThis.stylFns and globalThis.stylFns['app/components/Admin/FileUpload/index.styl']
+  styleElement = document.createElement('style')
+  styleElement.type = 'text/css'
+  styleElement.textContent = globalThis.stylFns['app/components/Admin/FileUpload/index.styl']
+  document.head.appendChild(styleElement)
+
+module.exports = 
+  name: 'file-upload'
+  props:
+    accept:
+      type: String
+      default: '.csv,.xlsx,.xls'
+    multiple:
+      type: Boolean
+      default: false
+    maxSize:
+      type: Number
+      default: 10485760 # 10MB
+
+  data: ->
+    {
+      isDragging: false
+      selectedFiles: []
+      uploadProgress: 0
+      isUploading: false
+    }
+
+  methods:
+    onDragOver: (event) ->
+      event.preventDefault()
+      @isDragging = true
+
+    onDragLeave: (event) ->
+      event.preventDefault()
+      @isDragging = false
+
+    onDrop: (event) ->
+      event.preventDefault()
+      @isDragging = false
+      files = Array.from(event.dataTransfer.files)
+      @handleFiles(files)
+
+    onFileSelect: (event) ->
+      files = Array.from(event.target.files)
+      @handleFiles(files)
+
+    handleFiles: (files) ->
+      validFiles = files.filter (file) =>
+        if file.size > @maxSize
+          @$emit('error', `Файл ${file.name} слишком большой. Максимальный размер: ${@maxSize / 1048576}MB`)
+          return false
+        
+        if @accept and !@isFileTypeAccepted(file)
+          @$emit('error', `Файл ${file.name} имеет недопустимый тип`)
+          return false
+        
+        return true
+
+      if @multiple
+        @selectedFiles = [...@selectedFiles, ...validFiles]
+      else
+        @selectedFiles = validFiles.slice(0, 1)
+
+      @$emit('select', @selectedFiles)
+
+    isFileTypeAccepted: (file) ->
+      acceptTypes = @accept.split(',').map (type) -> type.trim()
+      return acceptTypes.some (type) =>
+        if type.startsWith('.')
+          return file.name.toLowerCase().endsWith(type.toLowerCase())
+        else
+          return file.type.match(type)
+
+    removeFile: (index) ->
+      @selectedFiles.splice(index, 1)
+      @$emit('select', @selectedFiles)
+
+    uploadFiles: ->
+      if @selectedFiles.length == 0
+        @$emit('error', 'Нет файлов для загрузки')
+        return
+
+      @isUploading = true
+      @uploadProgress = 0
+
+      # Имитация загрузки
+      interval = setInterval (=>
+        @uploadProgress += 10
+        if @uploadProgress >= 100
+          clearInterval(interval)
+          @isUploading = false
+          @$emit('upload', @selectedFiles)
+          @selectedFiles = []
+      ), 100
+
+    getFileIcon: (file) ->
+      if file.type.includes('spreadsheet') or file.name.includes('.csv')
+        return '📊'
+      else if file.type.includes('image')
+        return '🖼️'
+      else
+        return '📄'
+
+  render: (new Function '_ctx', '_cache', globalThis.renderFns['app/components/Admin/FileUpload/index.pug'])()
+

+ 55 - 0
app/components/Admin/FileUpload/index.pug

@@ -0,0 +1,55 @@
+div(class="file-upload")
+  div(
+    class="file-upload__dropzone"
+    :class="{ 'file-upload__dropzone--dragging': isDragging }"
+    @dragover="onDragOver"
+    @dragleave="onDragLeave"
+    @drop="onDrop"
+  )
+    div(class="file-upload__content")
+      div(class="file-upload__icon") 📁
+      h3(class="file-upload__title") Перетащите файлы сюда
+      p(class="file-upload__subtitle") или
+      label(class="file-upload__browse")
+        input(
+          type="file"
+          :accept="accept"
+          :multiple="multiple"
+          @change="onFileSelect"
+          class="file-upload__input"
+        )
+        span(class="file-upload__browse-text") выберите файлы
+      p(class="file-upload__hint") Поддерживаемые форматы: {{ accept }}
+  
+  div(v-if="selectedFiles.length > 0" class="file-upload__files")
+    h4(class="file-upload__files-title") Выбранные файлы:
+    
+    div(
+      v-for="(file, index) in selectedFiles"
+      :key="index"
+      class="file-upload__file"
+    )
+      span(class="file-upload__file-icon") {{ getFileIcon(file) }}
+      div(class="file-upload__file-info")
+        span(class="file-upload__file-name") {{ file.name }}
+        span(class="file-upload__file-size") {{ (file.size / 1024 / 1024).toFixed(2) }} MB
+      button(
+        @click="removeFile(index)"
+        class="file-upload__remove"
+        title="Удалить файл"
+      ) ×
+  
+  div(v-if="isUploading" class="file-upload__progress")
+    div(class="file-upload__progress-bar")
+      div(
+        class="file-upload__progress-fill"
+        :style="{ width: uploadProgress + '%' }"
+      )
+    span(class="file-upload__progress-text") {{ uploadProgress }}%
+  
+  div(v-if="selectedFiles.length > 0 && !isUploading" class="file-upload__actions")
+    ui-button(
+      @click="uploadFiles"
+      type="primary"
+      :disabled="isUploading"
+    ) Загрузить файлы

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

@@ -1,4 +1,9 @@
-# app/pages/Admin/index.coffee
+
+if globalThis.stylFns and globalThis.stylFns['app/pages/Admin/index.styl']
+  styleElement = document.createElement('style')
+  styleElement.type = 'text/css'
+  styleElement.textContent = globalThis.stylFns['app/pages/Admin/index.styl']
+  document.head.appendChild(styleElement)
 
 
 module.exports = {
 module.exports = {
   props:
   props:
@@ -11,8 +16,53 @@ module.exports = {
 
 
   data: ->
   data: ->
     {
     {
-      pageTitle: 'Админ-панель'
+      user: null
+      breadcrumbs: []
     }
     }
 
 
+  computed:
+    currentRoute: -> @$route
+
+  methods:
+    logout: ->
+      localStorage.removeItem('user')
+      @$router.push('/')
+      @$emit('show-notification', 'Вы вышли из админ-панели', 'info')
+
+    updateBreadcrumbs: ->
+      crumbs = []
+      pathArray = @$route.path.split('/').filter (x) -> x
+      
+      pathArray.forEach (path, index) =>
+        path = "/#{pathArray.slice(0, index + 1).join('/')}"
+        name = @getBreadcrumbName(path, pathArray[index])
+        crumbs.push({ path, name })
+      
+      @breadcrumbs = crumbs
+
+    getBreadcrumbName: (path, segment) ->
+      names =
+        '/admin': 'Дашборд'
+        '/admin/products': 'Товары'
+        '/admin/categories': 'Категории'
+        '/admin/import': 'Импорт'
+        '/admin/media': 'Медиафайлы'
+      
+      return names[path] || segment.charAt(0).toUpperCase() + segment.slice(1)
+
+  watch:
+    '$route.path': ->
+      @updateBreadcrumbs()
+
+  mounted: ->
+    userData = localStorage.getItem('user')
+    if userData
+      try
+        @user = JSON.parse(userData)
+      catch
+        @user = null
+    
+    @updateBreadcrumbs()
+
   render: (new Function '_ctx', '_cache', globalThis.renderFns['app/pages/Admin/index.pug'])()
   render: (new Function '_ctx', '_cache', globalThis.renderFns['app/pages/Admin/index.pug'])()
 }
 }

+ 39 - 4
app/pages/Admin/index.pug

@@ -1,4 +1,39 @@
-div(class="admin-page")
-  h1 {{ pageTitle }}
-  p Админ-панель в разработке
-  ui-button(@click="$router.push('/')") На главную
+div(class="admin-layout")
+  aside(class="admin-sidebar")
+    div(class="admin-sidebar__header")
+      h2 {{ domainSettings?.companyName || 'Админ-панель' }}
+      p Версия 1.0.0
+    
+    nav(class="admin-nav")
+      ul(class="admin-nav__list")
+        li(class="admin-nav__item")
+          router-link(to="/admin" class="admin-nav__link" exact) 📊 Дашборд
+        li(class="admin-nav__item")
+          router-link(to="/admin/products" class="admin-nav__link") 🛍️ Товары
+        li(class="admin-nav__item")
+          router-link(to="/admin/categories" class="admin-nav__link") 📂 Категории
+        li(class="admin-nav__item")
+          router-link(to="/admin/import" class="admin-nav__link") 📥 Импорт
+        li(class="admin-nav__item")
+          router-link(to="/admin/media" class="admin-nav__link") 🖼️ Медиафайлы
+        li(class="admin-nav__item")
+          router-link(to="/" class="admin-nav__link") ← На сайт
+  
+  main(class="admin-main")
+    header(class="admin-header")
+      div(class="admin-header__user")
+        span {{ user?.username || 'Администратор' }}
+        ui-button(@click="logout" size="small") Выйти
+      
+      div(class="admin-header__breadcrumbs")
+        span(v-for="(crumb, index) in breadcrumbs" :key="index")
+          router-link(v-if="index < breadcrumbs.length - 1" :to="crumb.path") {{ crumb.name }}
+          span(v-else) {{ crumb.name }}
+          span(v-if="index < breadcrumbs.length - 1") /
+    
+    div(class="admin-content")
+      router-view(
+        :domain-settings="domainSettings"
+        :language="language"
+        @show-notification="$emit('show-notification', $event)"
+      )

+ 100 - 548
app/pages/Admin/index.styl

@@ -1,576 +1,128 @@
-// Admin panel responsive styles
-.admin
+.admin-layout
   display: flex
   display: flex
   min-height: 100vh
   min-height: 100vh
-  background-color: #f9fafb
-  position: relative
+  background: var(--color-light-10)
 
 
-  &.dark
-    background-color: #111827
-
-// Mobile toggle button
-.admin__mobile-toggle
-  display: none
-  position: fixed
-  top: 1rem
-  left: 1rem
-  z-index: 1000
-  padding: 0.5rem
-  background-color: #ef4444
-  border-radius: 0.375rem
-  cursor: pointer
-  border: none
-
-  @media (max-width: 768px)
-    display: block
-
-.admin__burger
-  width: 1.5rem
-  height: 1.125rem
-  position: relative
-  transition: all 0.3s ease-in-out
-
-  span
-    display: block
-    height: 2px
-    width: 100%
-    background-color: #ffffff
-    border-radius: 1px
-    transition: all 0.3s ease-in-out
-    transform-origin: center
-
-    &:nth-child(2)
-      margin: 0.25rem 0
-
-.admin__burger--active
-  span
-    &:nth-child(1)
-      transform: rotate(45deg) translate(6px, 6px)
-    &:nth-child(2)
-      opacity: 0
-    &:nth-child(3)
-      transform: rotate(-45deg) translate(6px, -6px)
-
-// Sidebar styles
-.admin__sidebar
-  width: 16rem
-  background-color: #ffffff
-  box-shadow: 0 0 20px rgba(0, 0, 0, 0.1)
-  transition: all 0.3s ease-in-out
-  display: flex
-  flex-direction: column
-  position: fixed
-  left: 0
-  top: 0
-  height: 100vh
-  z-index: 100
-  overflow-y: auto
-
-  &.dark
-    background-color: #1f2937
-
-  @media (max-width: 768px)
-    transform: translateX(-100%)
-    
-    &.admin__sidebar--mobile-open
-      transform: translateX(0)
-
-  &.admin__sidebar--collapsed
-    width: 5rem
-
-.admin__sidebar-header
-  padding: 1.5rem 1rem
-  border-bottom: 1px solid #e5e7eb
-  display: flex
-  align-items: center
-  justify-content: space-between
-
-  &.dark
-    border-bottom-color: #374151
-
-.admin__brand
-  display: flex
-  align-items: center
-  gap: 0.75rem
-
-.admin__logo
-  width: 2rem
-  height: 2rem
-  object-fit: contain
-
-.admin__company-name
-  font-size: 1.125rem
-  font-weight: bold
-  color: #111827
-  white-space: nowrap
-  overflow: hidden
-  text-overflow: ellipsis
-
-  &.dark
-    color: #ffffff
-
-.admin__sidebar-toggle
-  padding: 0.5rem
-  border-radius: 0.375rem
-  cursor: pointer
-  border: none
-  background: none
-  color: #6b7280
-  transition: all 0.2s ease-in-out
-
-  &:hover
-    background-color: #f3f4f6
-    color: #111827
-
-  &.dark
-    color: #9ca3af
-
-    &:hover
-      background-color: #374151
-      color: #ffffff
-
-.admin__toggle-icon
-  width: 1rem
-  height: 1rem
-  transition: transform 0.3s ease-in-out
-
-  &.admin__toggle-icon--collapsed
-    transform: rotate(180deg)
-
-// Navigation styles
-.admin__nav
-  flex: 1
-  padding: 1rem 0
+.admin-sidebar
+  width: 280px
+  background: var(--color-white)
+  box-shadow: var(--shadow-md)
+  border-right: 1px solid var(--border-color)
   display: flex
   display: flex
   flex-direction: column
   flex-direction: column
-  gap: 1.5rem
-
-.admin__nav-section
-  padding: 0 1rem
-
-.admin__nav-section-title
-  font-size: 0.75rem
-  font-weight: 600
-  color: #6b7280
-  text-transform: uppercase
-  letter-spacing: 0.05em
-  margin-bottom: 0.75rem
-
-  &.dark
-    color: #9ca3af
-
-.admin__nav-list
+  
+  .theme-dark &
+    background: var(--color-dark)
+    color: var(--color-light)
+
+.admin-sidebar__header
+  padding: var(--spacing-xl)
+  border-bottom: 1px solid var(--border-color)
+  
+  h2
+    font-size: var(--font-size-lg)
+    font-weight: var(--font-weight-bold)
+    margin-bottom: var(--spacing-xs)
+    color: var(--color-primary)
+  
+  p
+    font-size: var(--font-size-sm)
+    color: var(--color-secondary)
+
+.admin-nav__list
   list-style: none
   list-style: none
-  margin: 0
-  padding: 0
-  display: flex
-  flex-direction: column
-  gap: 0.25rem
-
-.admin__nav-item
-  margin: 0
-
-.admin__nav-link
-  display: flex
-  align-items: center
-  gap: 0.75rem
-  padding: 0.75rem 1rem
-  border-radius: 0.5rem
-  text-decoration: none
-  transition: all 0.2s ease-in-out
-  color: #374151
-  position: relative
-
-  &:hover
-    background-color: #f3f4f6
-    color: #111827
-
-  &.dark
-    color: #d1d5db
-
-    &:hover
-      background-color: #374151
-      color: #ffffff
-
-  &.admin__nav-item--active
-    background-color: #fef2f2
-    color: #dc2626
-    font-weight: 500
-
-    &.dark
-      background-color: #7f1d1d
-      color: #fca5a5
-
-.admin__nav-icon
-  width: 1.25rem
-  height: 1.25rem
-  flex-shrink: 0
-
-.admin__nav-text
-  white-space: nowrap
-  overflow: hidden
-  text-overflow: ellipsis
+  padding: var(--spacing-md) 0
   flex: 1
   flex: 1
 
 
-.admin__sidebar--collapsed .admin__nav-text
-  display: none
-
-.admin__nav-badge
-  background-color: #ef4444
-  color: #ffffff
-  font-size: 0.75rem
-  padding: 0.125rem 0.5rem
-  border-radius: 9999px
-  font-weight: 500
-
-// Sidebar footer
-.admin__sidebar-footer
-  padding: 1rem
-  border-top: 1px solid #e5e7eb
-  margin-top: auto
-
-  &.dark
-    border-top-color: #374151
-
-.admin__user-info
-  display: flex
-  align-items: center
-  gap: 0.75rem
-  margin-bottom: 1rem
-
-.admin__user-avatar
-  width: 2.5rem
-  height: 2.5rem
-  border-radius: 50%
-  background-color: #ef4444
-  color: #ffffff
-  display: flex
-  align-items: center
-  justify-content: center
-  font-weight: 600
-  font-size: 0.875rem
-
-.admin__user-details
-  display: flex
-  flex-direction: column
-  flex: 1
-  min-width: 0
-
-.admin__user-name
-  font-weight: 600
-  color: #111827
-  font-size: 0.875rem
-  white-space: nowrap
-  overflow: hidden
-  text-overflow: ellipsis
-
-  &.dark
-    color: #ffffff
-
-.admin__user-role
-  font-size: 0.75rem
-  color: #6b7280
-
-  &.dark
-    color: #9ca3af
-
-.admin__logout-btn
-  width: 100%
-  display: flex
-  align-items: center
-  gap: 0.75rem
-  padding: 0.75rem 1rem
-  border-radius: 0.5rem
-  background: none
-  border: 1px solid #e5e7eb
-  color: #374151
-  cursor: pointer
-  transition: all 0.2s ease-in-out
-  font-size: 0.875rem
-
-  &:hover
-    background-color: #f9fafb
-    border-color: #d1d5db
+.admin-nav__item
+  margin-bottom: var(--spacing-xs)
 
 
-  &.dark
-    border-color: #4b5563
-    color: #d1d5db
-
-    &:hover
-      background-color: #374151
-      border-color: #6b7280
+.admin-nav__link
+  display: block
+  padding: var(--spacing-md) var(--spacing-xl)
+  text-decoration: none
+  color: var(--color-dark)
+  transition: var(--transition-fast)
+  border-left: 3px solid transparent
+  
+  &:hover, &.router-link-active
+    background: var(--color-primary-10)
+    border-left-color: var(--color-primary)
+    color: var(--color-primary)
+  
+  .theme-dark &
+    color: var(--color-light)
+    
+    &:hover, &.router-link-active
+      background: var(--color-primary-20)
+      color: var(--color-light)
 
 
-// Main content area
-.admin__main
+.admin-main
   flex: 1
   flex: 1
-  margin-left: 16rem
-  transition: all 0.3s ease-in-out
-  min-height: 100vh
   display: flex
   display: flex
   flex-direction: column
   flex-direction: column
 
 
-  &.admin__main--expanded
-    margin-left: 5rem
-
-  @media (max-width: 768px)
-    margin-left: 0
-    
-    &.admin__main--mobile-open
-      transform: translateX(16rem)
-
-.admin__topbar
-  background-color: #ffffff
-  border-bottom: 1px solid #e5e7eb
-  padding: 1rem 1.5rem
+.admin-header
+  background: var(--color-white)
+  padding: var(--spacing-lg) var(--spacing-xl)
+  border-bottom: 1px solid var(--border-color)
   display: flex
   display: flex
-  align-items: center
   justify-content: space-between
   justify-content: space-between
-
-  &.dark
-    background-color: #1f2937
-    border-bottom-color: #374151
-
-.admin__breadcrumbs
-  display: flex
-  align-items: center
-  gap: 0.5rem
-  font-size: 0.875rem
-
-.admin__breadcrumb-link
-  color: #6b7280
-  text-decoration: none
-  transition: color 0.2s ease-in-out
-
-  &:hover
-    color: #ef4444
-
-  &.dark
-    color: #9ca3af
-
-.admin__breadcrumb-current
-  color: #111827
-  font-weight: 500
-
-  &.dark
-    color: #ffffff
-
-.admin__breadcrumb-separator
-  color: #9ca3af
-  margin-left: 0.5rem
-
-.admin__actions
-  display: flex
   align-items: center
   align-items: center
-  gap: 0.5rem
-
-.admin__action-btn
-  padding: 0.5rem
-  border-radius: 0.375rem
-  background: none
-  border: none
-  color: #6b7280
-  cursor: pointer
-  transition: all 0.2s ease-in-out
-  position: relative
-
-  &:hover
-    background-color: #f3f4f6
-    color: #111827
-
-  &.dark
-    color: #9ca3af
-
-    &:hover
-      background-color: #374151
-      color: #ffffff
-
-// Notifications
-.admin__notifications
-  position: relative
-
-.admin__notifications-btn
-  position: relative
+  
+  .theme-dark &
+    background: var(--color-dark)
+    color: var(--color-light)
 
 
-.admin__notification-badge
-  position: absolute
-  top: -0.25rem
-  right: -0.25rem
-  background-color: #ef4444
-  color: #ffffff
-  font-size: 0.75rem
-  padding: 0.125rem 0.375rem
-  border-radius: 9999px
-  min-width: 1.25rem
-  text-align: center
-
-.admin__notifications-dropdown
-  position: absolute
-  top: 100%
-  right: 0
-  width: 20rem
-  background-color: #ffffff
-  border: 1px solid #e5e7eb
-  border-radius: 0.5rem
-  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1)
-  z-index: 50
-  margin-top: 0.5rem
-
-  &.dark
-    background-color: #1f2937
-    border-color: #374151
-
-.admin__notifications-header
-  padding: 1rem
-  border-bottom: 1px solid #e5e7eb
+.admin-header__user
   display: flex
   display: flex
   align-items: center
   align-items: center
-  justify-content: space-between
-
-  &.dark
-    border-bottom-color: #374151
-
-.admin__notifications-count
-  font-size: 0.875rem
-  color: #ef4444
-  font-weight: 500
-
-.admin__notifications-list
-  max-height: 20rem
-  overflow-y: auto
-
-.admin__notification-item
-  padding: 1rem
-  border-bottom: 1px solid #f3f4f6
-  display: flex
-  align-items: flex-start
-  gap: 0.75rem
-  transition: background-color 0.2s ease-in-out
-
-  &:hover
-    background-color: #f9fafb
-
-  &.dark
-    border-bottom-color: #374151
-
-    &:hover
-      background-color: #374151
-
-  &.admin__notification-item--unread
-    background-color: #fef2f2
-
-    &.dark
-      background-color: #7f1d1d
-
-.admin__notification-icon
-  width: 1.5rem
-  height: 1.5rem
-  flex-shrink: 0
-  color: #ef4444
-
-.admin__notification-content
-  flex: 1
-  min-width: 0
-
-.admin__notification-title
-  font-size: 0.875rem
-  color: #111827
-  margin: 0 0 0.25rem 0
-  line-height: 1.4
-
-  &.dark
-    color: #ffffff
-
-.admin__notification-time
-  font-size: 0.75rem
-  color: #6b7280
-
-.admin__notification-action
-  padding: 0.25rem
-  background: none
-  border: none
-  color: #9ca3af
-  cursor: pointer
-  border-radius: 0.25rem
-  font-size: 1.125rem
-  line-height: 1
-
-  &:hover
-    color: #6b7280
-    background-color: #f3f4f6
-
-  &.dark
+  gap: var(--spacing-md)
+
+.admin-header__breadcrumbs
+  color: var(--color-secondary)
+  font-size: var(--font-size-sm)
+  
+  a
+    color: var(--color-primary)
+    text-decoration: none
+    
     &:hover
     &:hover
-      background-color: #4b5563
-      color: #d1d5db
+      text-decoration: underline
 
 
-// Content area
-.admin__content
+.admin-content
   flex: 1
   flex: 1
-  padding: 1.5rem
+  padding: var(--spacing-xl)
   overflow-y: auto
   overflow-y: auto
 
 
-// Overlay for mobile menu
-.admin__overlay
-  position: fixed
-  inset: 0
-  background-color: rgba(0, 0, 0, 0.5)
-  z-index: 90
-  display: none
-
-  @media (max-width: 768px)
-    display: block
-
-// Responsive adjustments
-@media (max-width: 1024px)
-  .admin__sidebar
-    width: 14rem
-
-  .admin__main
-    margin-left: 14rem
-
-  .admin__main--expanded
-    margin-left: 4rem
-
-  .admin__sidebar--collapsed
-    width: 4rem
-
+// Адаптивность
 @media (max-width: 768px)
 @media (max-width: 768px)
-  .admin__sidebar
-    width: 16rem
-
-  .admin__main
-    margin-left: 0
-
-  .admin__main--mobile-open
-    transform: translateX(16rem)
-
-  .admin__topbar
-    padding: 1rem
-
-  .admin__content
-    padding: 1rem
-
-  .admin__breadcrumbs
-    font-size: 0.8rem
-
-  .admin__notifications-dropdown
-    width: 18rem
-    right: -1rem
-
-@media (max-width: 480px)
-  .admin__mobile-toggle
-    top: 0.5rem
-    left: 0.5rem
-
-  .admin__topbar
+  .admin-layout
     flex-direction: column
     flex-direction: column
-    gap: 1rem
-    align-items: flex-start
-
-  .admin__actions
+  
+  .admin-sidebar
     width: 100%
     width: 100%
-    justify-content: flex-end
-
-  .admin__notifications-dropdown
-    width: 16rem
-    right: -2rem
+    height: auto
+  
+  .admin-nav__list
+    display: flex
+    overflow-x: auto
+    padding: var(--spacing-sm)
+  
+  .admin-nav__item
+    margin-bottom: 0
+    margin-right: var(--spacing-sm)
+  
+  .admin-nav__link
+    white-space: nowrap
+    border-left: none
+    border-bottom: 3px solid transparent
+    
+    &:hover, &.router-link-active
+      border-left: none
+      border-bottom-color: var(--color-primary)
+  
+  .admin-header
+    flex-direction: column
+    gap: var(--spacing-md)
+    align-items: flex-start