From 121a4dece1fc781cf8b13faf78dec2814107e9b8 Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Wed, 27 Aug 2025 12:29:00 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85,=20layout=20=D0=B8=20=D1=81=D1=82=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D1=85=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DATA_SYNCHRONIZATION_RULES.md - правила синхронизации между компонентами - GRAPHQL_CACHE_RULES.md - настройки кеширования и fetchPolicy - CSS_LAYOUT_SCROLL_RULES.md - решение проблем с overflow и scroll - STATISTICAL_COMPONENTS_RULES.md - правила Master-Detail архитектуры Документация основана на исправлениях в кабинете фулфилмента 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/api-layer/GRAPHQL_CACHE_RULES.md | 390 ++++++++++++ .../CSS_LAYOUT_SCROLL_RULES.md | 489 ++++++++++++++ .../DATA_SYNCHRONIZATION_RULES.md | 286 +++++++++ .../STATISTICAL_COMPONENTS_RULES.md | 598 ++++++++++++++++++ 4 files changed, 1763 insertions(+) create mode 100644 docs/api-layer/GRAPHQL_CACHE_RULES.md create mode 100644 docs/presentation-layer/CSS_LAYOUT_SCROLL_RULES.md create mode 100644 docs/presentation-layer/DATA_SYNCHRONIZATION_RULES.md create mode 100644 docs/presentation-layer/STATISTICAL_COMPONENTS_RULES.md diff --git a/docs/api-layer/GRAPHQL_CACHE_RULES.md b/docs/api-layer/GRAPHQL_CACHE_RULES.md new file mode 100644 index 0000000..cbc82f3 --- /dev/null +++ b/docs/api-layer/GRAPHQL_CACHE_RULES.md @@ -0,0 +1,390 @@ +# 🔄 ПРАВИЛА КЕШИРОВАНИЯ GRAPHQL И FETCHPOLICY + +> **Цель:** Обеспечить корректное кеширование GraphQL данных и предотвратить проблемы синхронизации между компонентами + +## 📋 **ОСНОВНЫЕ ПРИНЦИПЫ КЕШИРОВАНИЯ** + +### 1. **СИНХРОНИЗАЦИЯ СВЯЗАННЫХ КОМПОНЕНТОВ** + +```typescript +// ✅ ПРАВИЛЬНО - одинаковые настройки для связанных данных +const masterComponent = useQuery(GET_WAREHOUSE_STATS, { + fetchPolicy: 'cache-and-network', + pollInterval: 30000, + errorPolicy: 'all', +}) + +const detailComponent = useQuery(GET_SUPPLIES_DETAILS, { + fetchPolicy: 'cache-and-network', // ← ОБЯЗАТЕЛЬНО то же самое! + pollInterval: 30000, // ← ОБЯЗАТЕЛЬНО то же самое! + errorPolicy: 'all', // ← ОБЯЗАТЕЛЬНО то же самое! +}) +``` + +```typescript +// ❌ НЕПРАВИЛЬНО - разные настройки создают рассинхронизацию +const masterComponent = useQuery(GET_WAREHOUSE_STATS, { + fetchPolicy: 'cache-and-network', +}) + +const detailComponent = useQuery(GET_SUPPLIES_DETAILS, { + // default fetchPolicy: 'cache-first' - СОЗДАЁТ ПРОБЛЕМУ! +}) +``` + +### 2. **ОБЯЗАТЕЛЬНЫЕ FETCH POLICIES ПО ТИПАМ КОМПОНЕНТОВ** + +#### **Dashboard и Статистика** + +```typescript +useQuery(DASHBOARD_QUERY, { + fetchPolicy: 'cache-and-network', // Всегда актуальные данные + pollInterval: 30000, // Обновление каждые 30 сек + errorPolicy: 'all', // Показывать частичные данные при ошибках +}) +``` + +#### **Связанные компоненты (Master-Detail)** + +```typescript +// Все компоненты одной функциональности ДОЛЖНЫ иметь идентичные настройки +const sharedQueryOptions = { + fetchPolicy: 'cache-and-network' as const, + pollInterval: 30000, + errorPolicy: 'all' as const, +} + +// Применение во всех связанных компонентах +useQuery(MASTER_QUERY, sharedQueryOptions) +useQuery(DETAIL_QUERY, sharedQueryOptions) +useQuery(STATS_QUERY, sharedQueryOptions) +``` + +#### **Списки и таблицы** + +```typescript +useQuery(LIST_QUERY, { + fetchPolicy: 'cache-and-network', // Актуальные данные + pollInterval: 60000, // 1 минута для списков + notifyOnNetworkStatusChange: true, // Показывать статус загрузки +}) +``` + +#### **Редко изменяющиеся данные** + +```typescript +useQuery(STATIC_DATA_QUERY, { + fetchPolicy: 'cache-first', // Кеш приоритетен + pollInterval: 300000, // 5 минут + errorPolicy: 'ignore', // Не показывать ошибки для статичных данных +}) +``` + +--- + +## 🏢 **СПЕЦИФИЧЕСКИЕ ПРАВИЛА ДЛЯ ФУЛФИЛМЕНТА** + +### **ГРУППЫ СИНХРОНИЗИРОВАННЫХ КОМПОНЕНТОВ** + +#### **Группа 1: Складская статистика** + +```typescript +// Все эти компоненты ДОЛЖНЫ использовать одинаковые настройки: +const warehouseStatsOptions = { + fetchPolicy: 'cache-and-network' as const, + pollInterval: 30000, + errorPolicy: 'all' as const, +} + +// 1. Главный dashboard (/fulfillment-warehouse) +useQuery(GET_WAREHOUSE_STATS, warehouseStatsOptions) + +// 2. Подраздел расходников (/fulfillment-warehouse/supplies) +useQuery(GET_SUPPLIES_STATS, warehouseStatsOptions) + +// 3. Раздел услуг (/services) - вкладка "Расходники" +useQuery(GET_SERVICE_SUPPLIES, warehouseStatsOptions) +``` + +#### **Группа 2: История поставок** + +```typescript +const suppliesHistoryOptions = { + fetchPolicy: 'cache-and-network' as const, + pollInterval: 30000, + errorPolicy: 'all' as const, +} + +// 1. Основные поставки (главная таблица) +useQuery(GET_MY_FULFILLMENT_SUPPLIES, suppliesHistoryOptions) + +// 2. Детали поставок (раскрывающиеся строки) +useQuery(GET_MY_FULFILLMENT_CONSUMABLE_SUPPLIES, suppliesHistoryOptions) +``` + +--- + +## 🚨 **КРИТИЧЕСКИЕ АНТИ-ПАТТЕРНЫ** + +### **❌ НИКОГДА НЕ ДЕЛАЙТЕ:** + +#### **1. Разные fetchPolicy для связанных данных** + +```typescript +// ❌ СОЗДАЁТ РАССИНХРОНИЗАЦИЮ +const warehouse = useQuery(GET_WAREHOUSE, { fetchPolicy: 'cache-and-network' }) +const supplies = useQuery(GET_SUPPLIES, {}) // default cache-first + +// Результат: warehouse показывает новые данные, supplies - старые +``` + +#### **2. Разные pollInterval для одной функциональности** + +```typescript +// ❌ СОЗДАЁТ НЕСОГЛАСОВАННОСТЬ +const stats = useQuery(GET_STATS, { pollInterval: 30000 }) +const details = useQuery(GET_DETAILS, { pollInterval: 60000 }) + +// Результат: stats обновляется чаще details - значения не совпадают +``` + +#### **3. Смешивание cache-first и cache-and-network** + +```typescript +// ❌ КЛАССИЧЕСКАЯ ОШИБКА +const mainData = useQuery(MAIN_QUERY, { fetchPolicy: 'cache-and-network' }) +const relatedData = useQuery(RELATED_QUERY, { fetchPolicy: 'cache-first' }) + +// Результат: mainData актуальные, relatedData устаревшие +``` + +#### **4. Игнорирование ошибок в критических компонентах** + +```typescript +// ❌ СКРЫВАЕТ ПРОБЛЕМЫ +useQuery(CRITICAL_DATA_QUERY, { + errorPolicy: 'ignore', // Не показывает ошибки когда нужно! +}) +``` + +--- + +## 📊 **ПРАВИЛА ПО ТИПАМ ДАННЫХ** + +### **СТАТИСТИКА И DASHBOARD** + +```typescript +const dashboardOptions = { + fetchPolicy: 'cache-and-network', // Всегда актуальные данные + pollInterval: 30000, // 30 сек - критическая актуальность + errorPolicy: 'all', // Показывать частичные данные + notifyOnNetworkStatusChange: true, // Показывать индикатор обновления +} +``` + +### **ИНВЕНТАРЬ И ОСТАТКИ** + +```typescript +const inventoryOptions = { + fetchPolicy: 'cache-and-network', // Остатки должны быть точными + pollInterval: 30000, // Частое обновление + errorPolicy: 'all', // Критические данные +} +``` + +### **ИСТОРИЯ И ЛОГИ** + +```typescript +const historyOptions = { + fetchPolicy: 'cache-first', // История не меняется часто + pollInterval: 120000, // 2 минуты достаточно + errorPolicy: 'all', // Показывать что есть +} +``` + +### **СПРАВОЧНИКИ И КАТАЛОГИ** + +```typescript +const catalogOptions = { + fetchPolicy: 'cache-first', // Справочники стабильны + pollInterval: 300000, // 5 минут + errorPolicy: 'ignore', // Не критично +} +``` + +--- + +## 🔧 **ПРАКТИЧЕСКИЕ ПРИМЕРЫ ИСПРАВЛЕНИЙ** + +### **ПРИМЕР 1: Рассинхронизация карточек статистики** + +**❌ Проблема:** + +```typescript +// Главный dashboard +const warehouseStats = useQuery(GET_WAREHOUSE_STATS, { + fetchPolicy: 'cache-and-network', +}) + +// Подраздел supplies +const suppliesStats = useQuery(GET_SUPPLIES_STATS, { + // default fetchPolicy: 'cache-first' - ПРОБЛЕМА! +}) + +// Результат: карточка "РАСХОДНИКИ ФУЛФИЛМЕНТА" = 130, карточка "ОСТАТОК" = 160 +``` + +**✅ Решение:** + +```typescript +// Общие настройки для всех статистических компонентов +const statsOptions = { + fetchPolicy: 'cache-and-network' as const, + pollInterval: 30000, + errorPolicy: 'all' as const, +} + +// Применяем везде одинаково +const warehouseStats = useQuery(GET_WAREHOUSE_STATS, statsOptions) +const suppliesStats = useQuery(GET_SUPPLIES_STATS, statsOptions) +``` + +### **ПРИМЕР 2: История поставок не синхронизируется с основной таблицей** + +**❌ Проблема:** + +```typescript +// Основная таблица +const supplies = useQuery(GET_SUPPLIES, { fetchPolicy: 'cache-and-network' }) + +// История в раскрывающихся строках +const history = useQuery(GET_SUPPLY_HISTORY, {}) // default cache-first + +// Результат: основная таблица обновилась, история показывает старые данные +``` + +**✅ Решение:** + +```typescript +const syncedOptions = { + fetchPolicy: 'cache-and-network', + pollInterval: 30000, +} + +const supplies = useQuery(GET_SUPPLIES, syncedOptions) +const history = useQuery(GET_SUPPLY_HISTORY, syncedOptions) +``` + +--- + +## 🎯 **ЧЕКЛИСТ НАСТРОЙКИ КЕШИРОВАНИЯ** + +### **Перед релизом компонента:** + +- [ ] Определили группу связанных компонентов +- [ ] Выбрали единую fetchPolicy для группы +- [ ] Установили одинаковые pollInterval значения +- [ ] Настроили errorPolicy в соответствии с критичностью +- [ ] Протестировали синхронизацию данных + +### **При обнаружении рассинхронизации:** + +- [ ] Проверили fetchPolicy всех связанных запросов +- [ ] Сравнили pollInterval значения +- [ ] Убедились в использовании одних таблиц БД +- [ ] Проверили последовательность обновления данных + +--- + +## 📈 **МОНИТОРИНГ И ОТЛАДКА** + +### **DEBUG ЛОГИ ДЛЯ КЕШИРОВАНИЯ** + +```typescript +// Добавляйте в компоненты для контроля кеширования +useQuery(QUERY, { + fetchPolicy: 'cache-and-network', + onCompleted: (data) => { + console.log('CACHE DEBUG:', { + query: 'QUERY_NAME', + timestamp: new Date().toISOString(), + dataLength: data?.items?.length, + source: 'network/cache', + }) + }, + onError: (error) => { + console.error('CACHE ERROR:', { + query: 'QUERY_NAME', + error: error.message, + networkError: error.networkError?.message, + }) + }, +}) +``` + +### **ПРОВЕРКА СИНХРОНИЗАЦИИ** + +```typescript +// Контрольные точки для проверки консистентности +useEffect(() => { + if (masterData && detailData) { + const masterValue = masterData.total + const detailValue = detailData.reduce((sum, item) => sum + item.value, 0) + + if (Math.abs(masterValue - detailValue) > 0) { + console.error('🚨 CACHE SYNC ERROR:', { + component: 'ComponentName', + master: masterValue, + detail: detailValue, + diff: masterValue - detailValue, + }) + } + } +}, [masterData, detailData]) +``` + +--- + +## 🚀 **РЕКОМЕНДАЦИИ ПО ПРОИЗВОДИТЕЛЬНОСТИ** + +### **ОПТИМИЗАЦИЯ POLLING** + +```typescript +// Адаптивные интервалы в зависимости от активности пользователя +const useAdaptivePolling = () => { + const [isActive, setIsActive] = useState(true) + + useEffect(() => { + const handleVisibilityChange = () => { + setIsActive(!document.hidden) + } + + document.addEventListener('visibilitychange', handleVisibilityChange) + return () => document.removeEventListener('visibilitychange', handleVisibilityChange) + }, []) + + return { + pollInterval: isActive ? 30000 : 120000, // Реже обновляем неактивные вкладки + } +} +``` + +### **УСЛОВНОЕ КЕШИРОВАНИЕ** + +```typescript +// Разные стратегии для разных сценариев +const getCachePolicy = (dataType: string) => { + switch (dataType) { + case 'critical-stats': + return { fetchPolicy: 'cache-and-network', pollInterval: 30000 } + case 'user-lists': + return { fetchPolicy: 'cache-and-network', pollInterval: 60000 } + case 'reference-data': + return { fetchPolicy: 'cache-first', pollInterval: 300000 } + default: + return { fetchPolicy: 'cache-first' } + } +} +``` + +**Следование этим правилам обеспечит стабильное кеширование и синхронизацию GraphQL данных!** 🚀 diff --git a/docs/presentation-layer/CSS_LAYOUT_SCROLL_RULES.md b/docs/presentation-layer/CSS_LAYOUT_SCROLL_RULES.md new file mode 100644 index 0000000..3ad6112 --- /dev/null +++ b/docs/presentation-layer/CSS_LAYOUT_SCROLL_RULES.md @@ -0,0 +1,489 @@ +# 🎨 ПРАВИЛА CSS LAYOUT И СКРОЛЛА + +> **Цель:** Предотвратить проблемы с overflow, scroll и позиционированием в Next.js 15 + React 19 приложении + +## 📋 **ОСНОВНЫЕ ПРИНЦИПЫ LAYOUT** + +### 1. **БАЗОВАЯ АРХИТЕКТУРА LAYOUT** + +```typescript +// ✅ ПРАВИЛЬНАЯ структура для всех страниц приложения +
{/* Контейнер полной высоты */} + {/* Фиксированная боковая панель */} +
+
{/* ЕДИНСТВЕННАЯ зона скролла */} + {/* Весь контент здесь */} +
+
+
+``` + +### 2. **КРИТИЧЕСКИЕ ПРАВИЛА OVERFLOW** + +#### **✅ ПРАВИЛЬНО: Один уровень overflow** + +```css +/* Родительский контейнер */ +.container { + height: 100vh; + overflow: hidden; /* Запрещаем скролл на уровне экрана */ +} + +/* Дочерний скроллируемый контейнер */ +.scrollable-content { + flex: 1; + overflow-y: auto; /* ЕДИНСТВЕННАЯ зона скролла */ +} +``` + +#### **❌ НЕПРАВИЛЬНО: Множественные overflow зоны** + +```css +/* Создаёт конфликты скролла */ +.parent { + overflow-y: auto; /* Первый скролл */ +} +.child { + overflow-y: auto; /* Второй скролл - ПРОБЛЕМА! */ +} +``` + +--- + +## 🚨 **ТИПИЧНЫЕ ПРОБЛЕМЫ И РЕШЕНИЯ** + +### **ПРОБЛЕМА 1: Контент смещается вправо** + +**❌ Причина:** + +```jsx +
+ {' '} + {/* Принудительная высота */} +
+ {' '} + {/* Скрывает контент */} + {/* Контент не помещается и смещается */} +
+
+``` + +**✅ Решение:** + +```jsx +
+ {' '} + {/* Простая вертикальная компоновка */} + {/* Контент автоматически размещается правильно */} +
+``` + +### **ПРОБЛЕМА 2: Скролл не работает** + +**❌ Причина:** + +```jsx +
+ {' '} + {/* Блокирует скролл */} +
+ {' '} + {/* Дублирует блокировку */} +
+ {' '} + {/* Создаёт контент больше экрана */} + {/* Контент не скроллится */} +
+
+
+``` + +**✅ Решение:** + +```jsx +
+
+ {' '} + {/* ЕДИНСТВЕННАЯ зона скролла */} + {/* Контент свободно скроллится */} +
+
+``` + +### **ПРОБЛЕМА 3: Двойной sidebar в layout** + +**❌ Причина:** + +```jsx +// В page.tsx +
+ {/* Первый sidebar */} +
+
+ {/* Второй sidebar - ДУБЛИРОВАНИЕ! */} + +
+
+
+``` + +**✅ Решение:** + +```jsx +// В page.tsx - УБРАТЬ дублирование +
+ {/* ЕДИНСТВЕННЫЙ sidebar */} +
+
{/* Весь контент */}
+
+
+``` + +--- + +## 🏗️ **АРХИТЕКТУРНЫЕ ПАТТЕРНЫ** + +### **ПАТТЕРН 1: Dashboard с статистикой** + +```jsx +export function DashboardPage() { + return ( +
+ +
+
+ {/* Заголовок */} +
+ + {/* Статистические карточки */} + + + {/* Основное содержимое */} + +
+
+
+ ) +} +``` + +### **ПАТТЕРН 2: Таблица с фильтрами** + +```jsx +export function TablePage() { + return ( +
+ +
+
+ {/* Фильтры (фиксированные) */} + + + {/* Таблица (скроллируемая) */} +
+ +
+
+
+
+ ) +} +``` + +### **ПАТТЕРН 3: Модальные окна** + +```jsx +// ✅ Модалки не должны влиять на основной скролл + +
+ {' '} + {/* Скролл только внутри модалки */} + +
+
+``` + +--- + +## 📱 **RESPONSIVE DESIGN ПРАВИЛА** + +### **АДАПТИВНЫЕ КОНТЕЙНЕРЫ** + +```jsx +
+ {/* Карточки адаптивно размещаются */} +
+``` + +### **АДАПТИВНЫЕ ОТСТУПЫ** + +```jsx +
+``` + +--- + +## 🎯 **СПЕЦИФИЧЕСКИЕ ПРАВИЛА ДЛЯ SFERA** + +### **ПРАВИЛА ДЛЯ ФУЛФИЛМЕНТ КОМПОНЕНТОВ** + +#### **1. Главная страница склада** + +```jsx +// src/app/fulfillment-warehouse/page.tsx +
+ +
+
+ + {/* Статистика - НЕ скроллится */} + {/* Контент - скроллится */} +
+
+
+``` + +#### **2. Таблицы поставок** + +```jsx +// src/components/fulfillment-warehouse/fulfillment-supplies-page.tsx +
+ +
+
+ {' '} + {/* ← КЛЮЧЕВОЙ ЭЛЕМЕНТ */} + {/* Фильтры */} + {/* Статистика */} + {/* Основная таблица - скроллируется естественно */} +
+
+
+``` + +#### **3. Раскрывающиеся детали (Master-Detail)** + +```jsx +// Детали поставок в раскрывающихся строках + + +
+ {' '} + {/* Локальный скролл */} + +
+ + +``` + +--- + +## ⚡ **ПРОИЗВОДИТЕЛЬНОСТЬ И ОПТИМИЗАЦИЯ** + +### **ВИРТУАЛИЗАЦИЯ ДЛЯ БОЛЬШИХ СПИСКОВ** + +```jsx +// Для таблиц с >100 строк +import { FixedSizeList as List } from 'react-window' + +;
+ {' '} + {/* Контейнер фиксированной высоты */} + + {({ index, style }) => ( +
+ +
+ )} +
+
+``` + +### **LAZY LOADING ДЛЯ КОНТЕНТА** + +```jsx +const LazyTableSection = lazy(() => import('./TableSection')) + +// В компоненте +
+ {/* Загружается сразу */} + Загрузка...
}> + {/* Загружается по требованию */} + + +``` + +--- + +## 🚨 **АНТИ-ПАТТЕРНЫ И ЗАПРЕТЫ** + +### **❌ НИКОГДА НЕ ДЕЛАЙТЕ:** + +#### **1. Принудительные размеры** + +```jsx +// ❌ Создаёт проблемы с layout +
+
+``` + +#### **2. Множественные overflow зоны** + +```jsx +// ❌ Конфликты скролла +
+
+
+``` + +#### **3. Смешивание fixed и sticky позиционирования** + +```jsx +// ❌ Непредсказуемое поведение +
+
+``` + +#### **4. Игнорирование responsive дизайна** + +```jsx +// ❌ Не адаптируется к мобильным устройствам +
{/* Сломается на мобильных */} +``` + +--- + +## 🔧 **ИНСТРУМЕНТЫ ОТЛАДКИ** + +### **CSS DEBUG КЛАССЫ** + +```css +/* Добавьте для визуализации проблем */ +.debug-borders * { + border: 1px solid red !important; +} + +.debug-overflow { + overflow: visible !important; + background: rgba(255, 0, 0, 0.1) !important; +} + +.debug-scroll { + scrollbar-color: red transparent !important; + scrollbar-width: thick !important; +} +``` + +### **REACT DevTools** + +```jsx +// Добавьте data-атрибуты для отладки +
+``` + +### **КОНСОЛЬНЫЕ ЛОГИ ДЛЯ РАЗМЕРОВ** + +```jsx +useEffect(() => { + const element = ref.current + if (element) { + console.log('LAYOUT DEBUG:', { + scrollHeight: element.scrollHeight, + clientHeight: element.clientHeight, + offsetHeight: element.offsetHeight, + hasOverflow: element.scrollHeight > element.clientHeight, + }) + } +}, []) +``` + +--- + +## 📊 **ЧЕКЛИСТ ПРОВЕРКИ LAYOUT** + +### **Перед релизом:** + +- [ ] Единственная зона скролла на странице +- [ ] Нет принудительных высот (`minHeight: '200vh'`) +- [ ] Правильная структура с `h-screen` и `overflow-hidden` +- [ ] Responsive дизайн для мобильных устройств +- [ ] Sidebar не дублируется в компонентах +- [ ] Модальные окна не влияют на основной скролл + +### **При проблемах со скроллом:** + +- [ ] Проверить количество `overflow-y-auto` в иерархии +- [ ] Убедиться в отсутствии `overflow-hidden` на скроллируемом контейнере +- [ ] Проверить наличие `flex-1` у родительского контейнера +- [ ] Убрать принудительные размеры (`min-height`, `height: 200vh`) + +--- + +## 🎨 **КАСТОМНЫЕ СКРОЛЛБАРЫ** + +### **TAILWIND CLASSES** + +```jsx +
+``` + +### **CUSTOM CSS** + +```css +/* Кастомные скроллбары для SFERA */ +.custom-scrollbar::-webkit-scrollbar { + width: 8px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} +``` + +**Следование этим правилам обеспечит корректную работу layout и скролла во всех компонентах!** 🚀 diff --git a/docs/presentation-layer/DATA_SYNCHRONIZATION_RULES.md b/docs/presentation-layer/DATA_SYNCHRONIZATION_RULES.md new file mode 100644 index 0000000..015ee46 --- /dev/null +++ b/docs/presentation-layer/DATA_SYNCHRONIZATION_RULES.md @@ -0,0 +1,286 @@ +# 🔄 ПРАВИЛА СИНХРОНИЗАЦИИ ДАННЫХ МЕЖДУ КОМПОНЕНТАМИ + +> **Цель:** Обеспечить консистентность данных между различными компонентами системы SFERA + +## 📋 **ОСНОВНЫЕ ПРИНЦИПЫ** + +### 1. **ЕДИНЫЙ ИСТОЧНИК ИСТИНЫ (Single Source of Truth)** + +- ✅ **Один резолвер = одна таблица БД** для одного типа данных +- ✅ **V2 система** - приоритет над legacy кодом +- ❌ **Никогда не дублируйте** одни данные в разных резолверах + +### 2. **КОНСИСТЕНТНАЯ CACHE POLICY** + +```typescript +// ✅ ПРАВИЛЬНО - одинаковая policy для связанных данных +const query1 = useQuery(QUERY_A, { fetchPolicy: 'cache-and-network' }) +const query2 = useQuery(QUERY_B, { fetchPolicy: 'cache-and-network' }) + +// ❌ НЕПРАВИЛЬНО - разные policies создают рассинхронизацию +const query1 = useQuery(QUERY_A, { fetchPolicy: 'cache-and-network' }) +const query2 = useQuery(QUERY_B, {}) // default cache-first policy +``` + +### 3. **ОБЯЗАТЕЛЬНЫЕ FETCH POLICIES** + +#### **Для связанных компонентов:** + +```typescript +fetchPolicy: 'cache-and-network', // Всегда актуальные данные +pollInterval: 30000, // Автообновление каждые 30 сек +``` + +#### **Для dashboard/статистики:** + +```typescript +fetchPolicy: 'cache-and-network', // Синхронизация с основными данными +pollInterval: 30000, // Регулярное обновление +errorPolicy: 'all', // Показывать частичные данные при ошибках +``` + +--- + +## 🏢 **ПРАВИЛА ДЛЯ ФУЛФИЛМЕНТА** + +### **ТАБЛИЦЫ V2 СИСТЕМЫ** + +- **`fulfillmentConsumableInventory`** - складские остатки расходников +- **`fulfillmentConsumableSupplyOrder`** - поставки расходников +- **`fulfillmentInventoryV2`** - общий инвентарь фулфилмента + +### **СВЯЗАННЫЕ КОМПОНЕНТЫ** + +```typescript +// Все эти компоненты должны использовать одинаковые данные: + +1. Главный dashboard склада (/fulfillment-warehouse) + - Карточка "РАСХОДНИКИ ФУЛФИЛМЕНТА" + +2. Подраздел расходников (/fulfillment-warehouse/supplies) + - Карточка "ОСТАТОК" + - Главная таблица поставок + +3. Раздел услуг (/services) + - Вкладка "Расходники" + +4. История поставок + - Раскрывающиеся детали каждого товара +``` + +### **СИНХРОНИЗИРОВАННЫЕ ПОЛЯ** + +```typescript +interface SynchronizedData { + currentStock: number // Текущий остаток (одинаковый везде) + totalReceived: number // Общее количество поступлений + productId: string // ID для группировки истории поставок + fulfillmentCenterId: string // ID фулфилмент-центра +} +``` + +--- + +## 📊 **ПРАВИЛА ДЛЯ СТАТИСТИЧЕСКИХ КОМПОНЕНТОВ** + +### **1. АРХИТЕКТУРА MASTER-DETAIL** + +```typescript +// MASTER компонент (главный раздел) +const masterStats = useQuery(GET_WAREHOUSE_STATS, { + fetchPolicy: 'cache-and-network', + pollInterval: 30000, +}) + +// DETAIL компонент (подраздел) +const detailStats = useQuery(GET_DETAILED_SUPPLIES, { + fetchPolicy: 'cache-and-network', // ← ОБЯЗАТЕЛЬНО то же самое! + pollInterval: 30000, // ← ОБЯЗАТЕЛЬНО то же самое! +}) + +// Вычисление должно давать одинаковые результаты +const masterValue = masterStats.data?.fulfillmentSupplies?.current +const detailValue = detailStats.data?.supplies?.reduce((sum, s) => sum + s.currentStock, 0) +// masterValue === detailValue ← ОБЯЗАТЕЛЬНО! +``` + +### **2. ПРАВИЛА АГРЕГАЦИИ** + +```typescript +// ✅ ПРАВИЛЬНО - агрегация из одного источника данных +const totalStock = inventoryItems.reduce((sum, item) => sum + item.currentStock, 0) + +// ❌ НЕПРАВИЛЬНО - агрегация из разных источников +const totalStock1 = supplies.reduce((sum, s) => sum + s.currentStock, 0) // источник A +const totalStock2 = inventory.reduce((sum, i) => sum + i.remainingStock, 0) // источник B +``` + +--- + +## 🔗 **ПРАВИЛА СВЯЗЫВАНИЯ ДАННЫХ** + +### **ОБЯЗАТЕЛЬНЫЕ ПОЛЯ ДЛЯ СВЯЗИ** + +```typescript +// В GraphQL схемах ОБЯЗАТЕЛЬНО указывать связующие поля: + +type Supply { + id: ID! + productId: ID! // ← Для фильтрации истории поставок + fulfillmentCenterId: ID // ← Для привязки к фулфилменту +} + +type SupplyOrder { + id: ID! + fulfillmentCenterId: ID! // ← Для фильтрации по фулфилменту + items: [SupplyOrderItem!]! +} + +type SupplyOrderItem { + productId: ID! // ← Для связи с inventory + requestedQuantity: Int! + receivedQuantity: Int! // ← Для подсчёта остатков +} +``` + +### **ФИЛЬТРАЦИЯ ПО СВЯЗАННЫМ ДАННЫМ** + +```typescript +// ✅ ПРАВИЛЬНО - фильтрация по ID +const getSupplyHistory = (supply: Supply) => { + return allDeliveries.filter((delivery) => delivery.items?.some((item) => item.productId === supply.productId)) +} + +// ❌ НЕПРАВИЛЬНО - фильтрация по названию (может быть дубли) +const getSupplyHistory = (supply: Supply) => { + return allDeliveries.filter((delivery) => delivery.name === supply.name && delivery.category === supply.category) +} +``` + +--- + +## ⚡ **ПРАВИЛА REAL-TIME ОБНОВЛЕНИЙ** + +### **АВТОМАТИЧЕСКАЯ СИНХРОНИЗАЦИЯ** + +```typescript +// Компоненты должны обновляться автоматически при изменениях + +// 1. При приёме поставки на склад +prisma.fulfillmentConsumableInventory.update({ + where: { id: itemId }, + data: { currentStock: newStock }, +}) +// → Автоматически обновятся: статистика, таблица, услуги + +// 2. При отгрузке товаров +prisma.fulfillmentConsumableInventory.update({ + where: { id: itemId }, + data: { currentStock: { decrement: shippedQuantity } }, +}) +// → Автоматически обновятся все связанные компоненты +``` + +### **POLLING ИНТЕРВАЛЫ** + +```typescript +pollInterval: 30000 // 30 сек - для статистики и dashboard +pollInterval: 60000 // 1 мин - для списков и таблиц +pollInterval: 120000 // 2 мин - для отчётов и архивных данных +``` + +--- + +## 🚨 **АНТИ-ПАТТЕРНЫ И ТИПИЧНЫЕ ОШИБКИ** + +### **❌ НИКОГДА НЕ ДЕЛАЙТЕ:** + +#### **1. Разные источники для одних данных** + +```typescript +// ❌ ПЛОХО +const stats = useQuery(GET_OLD_SUPPLIES) // legacy таблица +const details = useQuery(GET_NEW_SUPPLIES) // V2 таблица +``` + +#### **2. Разные cache policies для связанных данных** + +```typescript +// ❌ ПЛОХО +const master = useQuery(QUERY_A, { fetchPolicy: 'cache-and-network' }) +const detail = useQuery(QUERY_B, {}) // default cache-first +``` + +#### **3. Ручная синхронизация через состояние** + +```typescript +// ❌ ПЛОХО +const [manualSync, setManualSync] = useState(0) +useEffect(() => { + // Попытка ручной синхронизации через state +}, [manualSync]) +``` + +#### **4. Группировка по нестабильным полям** + +```typescript +// ❌ ПЛОХО - названия могут изменяться +supplies.filter((s) => s.name === targetName) + +// ✅ ХОРОШО - ID стабильны +supplies.filter((s) => s.productId === targetProductId) +``` + +--- + +## 🎯 **ЧЕКЛИСТ СИНХРОНИЗАЦИИ** + +### **При создании связанных компонентов:** + +- [ ] Используют одну таблицу БД как источник данных +- [ ] Одинаковые `fetchPolicy` настройки +- [ ] Одинаковые `pollInterval` значения +- [ ] Связь через стабильные ID поля +- [ ] Одинаковая логика агрегации/фильтрации +- [ ] Обработка ошибок и loading состояний + +### **При тестировании синхронизации:** + +- [ ] Изменение данных обновляет все связанные компоненты +- [ ] Значения в master и detail компонентах совпадают +- [ ] Нет задержек между обновлениями (< 30 сек) +- [ ] Ошибки в одном компоненте не ломают другие + +--- + +## 📈 **МОНИТОРИНГ СИНХРОНИЗАЦИИ** + +### **DEBUG ЛОГИ** + +```typescript +// Добавляйте логи для контроля синхронизации +console.warn('SYNC CHECK:', { + masterValue: masterData?.totalStock, + detailValue: detailData?.reduce(sum, 0), + timestamp: new Date().toISOString(), + source: 'fulfillmentInventorySync', +}) +``` + +### **УВЕДОМЛЕНИЯ О РАССИНХРОНИЗАЦИИ** + +```typescript +// Проверка консистентности данных +if (Math.abs(masterValue - detailValue) > 0) { + console.error('🚨 DATA SYNCHRONIZATION ERROR', { + master: masterValue, + detail: detailValue, + component: 'FulfillmentStats', + }) + + // Отправка уведомления разработчикам + toast.error('Обнаружена рассинхронизация данных') +} +``` + +**Следование этим правилам обеспечит надёжную синхронизацию данных между всеми компонентами системы!** 🚀 diff --git a/docs/presentation-layer/STATISTICAL_COMPONENTS_RULES.md b/docs/presentation-layer/STATISTICAL_COMPONENTS_RULES.md new file mode 100644 index 0000000..d30a220 --- /dev/null +++ b/docs/presentation-layer/STATISTICAL_COMPONENTS_RULES.md @@ -0,0 +1,598 @@ +# 📊 ПРАВИЛА СТАТИСТИЧЕСКИХ КОМПОНЕНТОВ + +> **Цель:** Обеспечить корректную работу, синхронизацию и консистентность статистических компонентов в системе SFERA + +## 📋 **ОСНОВНЫЕ ПРИНЦИПЫ СТАТИСТИЧЕСКИХ КОМПОНЕНТОВ** + +### 1. **АРХИТЕКТУРА MASTER-DETAIL** + +```typescript +// ✅ ПРАВИЛЬНАЯ архитектура связанных статистических компонентов +interface StatisticsHierarchy { + masterComponent: { + location: '/fulfillment-warehouse' + cards: ['РАСХОДНИКИ ФУЛФИЛМЕНТА', 'ТОВАРЫ СЕЛЛЕРОВ', ...] + role: 'Обзор всего склада' + } + + detailComponent: { + location: '/fulfillment-warehouse/supplies' + cards: ['ВСЕГО ПОЗИЦИЙ', 'ДОСТУПНО', 'ОСТАТОК', 'НЕТ В НАЛИЧИИ', ...] + role: 'Детализация расходников' + } + + // ОБЯЗАТЕЛЬНО: masterComponent.cards[X] === sum(detailComponent.items) + consistency: 'Значения master должны равняться агрегации detail' +} +``` + +### 2. **ЕДИНЫЕ ИСТОЧНИКИ ДАННЫХ** + +```typescript +// ✅ ПРАВИЛЬНО - один источник данных для связанных статистик +const warehouseQuery = useQuery(GET_WAREHOUSE_STATS, { + fetchPolicy: 'cache-and-network', + pollInterval: 30000, +}) + +const suppliesQuery = useQuery(GET_SUPPLIES_STATS, { + fetchPolicy: 'cache-and-network', // ← ОБЯЗАТЕЛЬНО то же самое! + pollInterval: 30000, // ← ОБЯЗАТЕЛЬНО то же самое! +}) + +// ОБЯЗАТЕЛЬНАЯ проверка консистентности +useEffect(() => { + const masterValue = warehouseQuery.data?.fulfillmentSupplies?.current + const detailValue = suppliesQuery.data?.supplies?.reduce((sum, s) => sum + s.currentStock, 0) + + if (masterValue !== detailValue) { + console.error('🚨 STATISTICS SYNC ERROR:', { masterValue, detailValue }) + } +}, [warehouseQuery.data, suppliesQuery.data]) +``` + +--- + +## 🎯 **ТИПЫ СТАТИСТИЧЕСКИХ КАРТОЧЕК** + +### **ТИП 1: Счётчики (Counters)** + +```jsx +// Подсчёт количества элементов +; + +// Логика расчёта +const totalItems = supplies.length +const availableItems = supplies.filter((s) => s.currentStock > 0).length +const outOfStockItems = supplies.filter((s) => s.currentStock === 0).length +``` + +### **ТИП 2: Агрегаторы (Aggregators)** + +```jsx +// Суммирование значений +; sum + s.currentStock, 0)} + unit="шт" + icon={Package} + color="blue" + format="number" +/> + +// Логика расчёта +const totalStock = supplies.reduce((sum, supply) => sum + supply.currentStock, 0) +const totalValue = supplies.reduce((sum, supply) => sum + supply.price * supply.currentStock, 0) +``` + +### **ТИП 3: Индикаторы состояния (Status Indicators)** + +```jsx +// Показатели состояния системы + +``` + +### **ТИП 4: Финансовые показатели (Financial)** + +```jsx +// Денежные значения + +``` + +--- + +## 🏢 **СПЕЦИФИЧЕСКИЕ ПРАВИЛА ДЛЯ ФУЛФИЛМЕНТА** + +### **КАРТОЧКИ ГЛАВНОГО DASHBOARD (/fulfillment-warehouse)** + +```typescript +interface WarehouseMasterStats { + 'РАСХОДНИКИ ФУЛФИЛМЕНТА': { + source: 'fulfillmentConsumableInventory' + calculation: 'SUM(currentStock)' + syncWith: '/fulfillment-warehouse/supplies - карточка ОСТАТОК' + } + + 'ТОВАРЫ СЕЛЛЕРОВ': { + source: 'sellerInventoryOnWarehouse' + calculation: 'SUM(currentStock)' + syncWith: 'Таблица товаров селлеров' + } + + 'АКТИВНЫЕ ЗАКАЗЫ': { + source: 'supplyOrders' + calculation: 'COUNT where status IN [PENDING, SHIPPED]' + syncWith: 'Раздел входящих заказов' + } +} +``` + +### **КАРТОЧКИ ПОДРАЗДЕЛА РАСХОДНИКОВ (/fulfillment-warehouse/supplies)** + +```typescript +interface SuppliesDetailStats { + 'ВСЕГО ПОЗИЦИЙ': { + calculation: 'COUNT(DISTINCT productId)' + description: 'Количество уникальных товаров' + } + + ДОСТУПНО: { + calculation: 'COUNT WHERE currentStock > 0' + description: 'Товары в наличии' + } + + ОСТАТОК: { + calculation: 'SUM(currentStock)' + description: 'Общее количество единиц товара' + syncWith: 'Master: РАСХОДНИКИ ФУЛФИЛМЕНТА' // ← КРИТИЧЕСКАЯ СИНХРОНИЗАЦИЯ + } + + 'НЕТ В НАЛИЧИИ': { + calculation: 'COUNT WHERE currentStock = 0' + description: 'Товары с нулевыми остатками' + } + + 'ОБЩАЯ СТОИМОСТЬ': { + calculation: 'SUM(price * currentStock)' + format: 'currency' + } + + 'В ПУТИ': { + calculation: 'COUNT WHERE status = "shipped"' + description: 'Товары в транспортировке' + } +} +``` + +--- + +## 🔄 **ПРАВИЛА СИНХРОНИЗАЦИИ СТАТИСТИК** + +### **1. ОБЯЗАТЕЛЬНАЯ КОНСИСТЕНТНОСТЬ MASTER-DETAIL** + +```typescript +// ✅ ПРАВИЛЬНАЯ реализация проверки синхронизации +const validateStatisticsConsistency = (masterData: any, detailData: any[]) => { + const masterValue = masterData?.fulfillmentSupplies?.current || 0 + const detailValue = detailData.reduce((sum, item) => sum + item.currentStock, 0) + + const tolerance = 0 // Для inventory данных - точное соответствие! + const isConsistent = Math.abs(masterValue - detailValue) <= tolerance + + if (!isConsistent) { + console.error('🚨 CRITICAL STATISTICS INCONSISTENCY:', { + component: 'FulfillmentStatistics', + masterValue, + detailValue, + difference: masterValue - detailValue, + timestamp: new Date().toISOString(), + requiresAttention: true, + }) + + // Уведомление пользователя о проблеме + toast.error('Обнаружена несогласованность данных статистики') + + // Попытка принудительного обновления + refetchMasterData() + refetchDetailData() + } + + return isConsistent +} + +// Использование в компонентах +useEffect(() => { + validateStatisticsConsistency(warehouseMasterData, suppliesDetailData) +}, [warehouseMasterData, suppliesDetailData]) +``` + +### **2. АВТОМАТИЧЕСКОЕ ОБНОВЛЕНИЕ СВЯЗАННЫХ СТАТИСТИК** + +```typescript +// При изменении данных - все связанные статистики обновляются +const handleInventoryUpdate = async (inventoryChange: InventoryChange) => { + // 1. Обновляем основные данные + await updateInventoryRecord(inventoryChange) + + // 2. Принудительное обновление всех связанных статистик + await Promise.all([ + refetchWarehouseStats(), // Master dashboard + refetchSuppliesStats(), // Detail dashboard + refetchServiceStats(), // Services section + ]) + + // 3. Проверяем консистентность после обновления + setTimeout(() => validateStatisticsConsistency(), 1000) +} +``` + +--- + +## 🎨 **ДИЗАЙН И UI ПРАВИЛА** + +### **СТАНДАРТНАЯ СТРУКТУРА КАРТОЧКИ** + +```jsx + +
+
+ {/* Основная информация */} +

{title}

+

+ {formattedValue} {unit && {unit}} +

+ + {/* Дополнительная информация */} + {subtitle &&

{subtitle}

} +
+ + {/* Иконка */} +
+ +
+
+
+``` + +### **ЦВЕТОВЫЕ СХЕМЫ ПО ТИПАМ** + +```typescript +const StatCardColors = { + // Позитивные показатели (наличие, доступность) + positive: { + background: 'bg-green-500/20', + text: 'text-green-300', + icon: 'text-green-300', + }, + + // Нейтральные показатели (общие счётчики) + neutral: { + background: 'bg-blue-500/20', + text: 'text-blue-300', + icon: 'text-blue-300', + }, + + // Внимание требующие (мало на складе) + warning: { + background: 'bg-yellow-500/20', + text: 'text-yellow-300', + icon: 'text-yellow-300', + }, + + // Негативные показатели (отсутствие, проблемы) + negative: { + background: 'bg-red-500/20', + text: 'text-red-300', + icon: 'text-red-300', + }, + + // Финансовые показатели + financial: { + background: 'bg-purple-500/20', + text: 'text-purple-300', + icon: 'text-purple-300', + }, + + // Процессы и активность + process: { + background: 'bg-orange-500/20', + text: 'text-orange-300', + icon: 'text-orange-300', + }, +} +``` + +--- + +## 📈 **ФОРМАТИРОВАНИЕ ДАННЫХ** + +### **ЧИСЛОВЫЕ ФОРМАТЫ** + +```typescript +const formatStatValue = (value: number, type: StatType) => { + switch (type) { + case 'currency': + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + minimumFractionDigits: 0, + }).format(value) + + case 'number': + return new Intl.NumberFormat('ru-RU').format(value) + + case 'percentage': + return new Intl.NumberFormat('ru-RU', { + style: 'percent', + minimumFractionDigits: 1, + }).format(value / 100) + + case 'compact': + return new Intl.NumberFormat('ru-RU', { + notation: 'compact', + compactDisplay: 'short', + }).format(value) + + default: + return String(value) + } +} + +// Примеры использования +formatStatValue(1500, 'number') // "1 500" +formatStatValue(25000, 'currency') // "25 000 ₽" +formatStatValue(1500000, 'compact') // "1,5 млн" +``` + +### **ЕДИНИЦЫ ИЗМЕРЕНИЯ** + +```typescript +const StatUnits = { + quantity: 'шт', + weight: 'кг', + volume: 'л', + currency: '₽', + percentage: '%', + count: '', // без единицы для простых счётчиков + days: 'дн.', + hours: 'ч', +} +``` + +--- + +## 🚨 **АНТИ-ПАТТЕРНЫ И ТИПИЧНЫЕ ОШИБКИ** + +### **❌ НИКОГДА НЕ ДЕЛАЙТЕ:** + +#### **1. Разные источники данных для связанных статистик** + +```typescript +// ❌ ПЛОХО - создаёт несогласованность +const masterStats = useQuery(GET_OLD_WAREHOUSE_DATA) // legacy таблица +const detailStats = useQuery(GET_NEW_SUPPLIES_DATA) // V2 таблица + +// Результат: разные значения в связанных карточках +``` + +#### **2. Игнорирование ошибок в статистике** + +```typescript +// ❌ ПЛОХО - скрывает проблемы с данными +const { data, error } = useQuery(STATS_QUERY, { + errorPolicy: 'ignore', // Ошибки не видны! +}) + +// ✅ ПРАВИЛЬНО +const { data, error } = useQuery(STATS_QUERY, { + errorPolicy: 'all', // Показываем ошибки + onError: (error) => { + console.error('Stats error:', error) + toast.error('Ошибка загрузки статистики') + }, +}) +``` + +#### **3. Вычисления на фронтенде вместо БД** + +```typescript +// ❌ ПЛОХО - медленно и неточно +const totalValue = supplies.map(s => s.price * s.quantity).reduce((a, b) => a + b, 0) + +// ✅ ПРАВИЛЬНО - вычисления в GraphQL resolver +query GetSuppliesStats { + suppliesStats { + totalValue # Рассчитано на сервере + totalStock # Рассчитано на сервере + } +} +``` + +#### **4. Отсутствие проверки синхронизации** + +```typescript +// ❌ ПЛОХО - не контролируем консистентность +const Stats = () => { + const master = useQuery(MASTER_QUERY) + const detail = useQuery(DETAIL_QUERY) + + return
{master.value} vs {detail.sum}
// Может не совпадать! +} + +// ✅ ПРАВИЛЬНО - с проверкой синхронизации +const Stats = () => { + const master = useQuery(MASTER_QUERY) + const detail = useQuery(DETAIL_QUERY) + + useEffect(() => { + validateConsistency(master.data, detail.data) + }, [master.data, detail.data]) + + return +} +``` + +--- + +## 🔧 **ИНСТРУМЕНТЫ ОТЛАДКИ И МОНИТОРИНГА** + +### **DEBUG КОМПОНЕНТ ДЛЯ СТАТИСТИКИ** + +```jsx +const StatisticsDebugPanel = ({ masterData, detailData }) => { + if (process.env.NODE_ENV !== 'development') return null + + return ( +
+

Statistics Debug

+
Master: {masterData?.value}
+
Detail Sum: {detailData?.reduce((sum, item) => sum + item.value, 0)}
+
Sync: {masterData?.value === detailSum ? '✅' : '❌'}
+
Last Update: {new Date().toLocaleTimeString()}
+
+ ) +} +``` + +### **АВТОМАТИЧЕСКИЕ ТЕСТЫ СИНХРОНИЗАЦИИ** + +```typescript +// Тест для проверки синхронизации статистик +describe('Statistics Synchronization', () => { + it('должен синхронизировать master и detail значения', async () => { + const { masterValue } = await fetchMasterStats() + const detailItems = await fetchDetailItems() + const detailSum = detailItems.reduce((sum, item) => sum + item.value, 0) + + expect(masterValue).toBe(detailSum) + }) + + it('должен обновлять связанные статистики при изменении данных', async () => { + const initialStats = await fetchStats() + await updateInventory({ productId: '1', change: +10 }) + + // Ждём обновления + await new Promise((resolve) => setTimeout(resolve, 1000)) + + const updatedStats = await fetchStats() + expect(updatedStats.totalStock).toBe(initialStats.totalStock + 10) + }) +}) +``` + +--- + +## 📊 **ШАБЛОНЫ КОМПОНЕНТОВ** + +### **БАЗОВЫЙ СТАТИСТИЧЕСКИЙ КОМПОНЕНТ** + +```tsx +interface StatCardProps { + title: string + value: number | string + icon: React.ComponentType + color: 'blue' | 'green' | 'red' | 'yellow' | 'purple' | 'orange' + unit?: string + subtitle?: string + trend?: { value: number; period: string } + format?: 'number' | 'currency' | 'percentage' | 'compact' + loading?: boolean + error?: string +} + +export const StatCard: React.FC = ({ + title, + value, + icon: Icon, + color, + unit, + subtitle, + trend, + format = 'number', + loading, + error, +}) => { + const formattedValue = useMemo(() => { + if (loading) return '...' + if (error) return 'Ошибка' + return formatStatValue(Number(value), format) + }, [value, format, loading, error]) + + return ( + +
+
+

{title}

+

+ {formattedValue} + {unit && {unit}} +

+ {subtitle &&

{subtitle}

} + {trend && ( +

+ {trend.value > 0 ? '+' : ''} + {trend.value} {trend.period} +

+ )} +
+
+ +
+
+
+ ) +} +``` + +### **КОМПОНЕНТ ГРУППЫ СТАТИСТИК** + +```tsx +interface StatsGridProps { + stats: Array + columns?: 1 | 2 | 3 | 4 | 6 + loading?: boolean + error?: string +} + +export const StatsGrid: React.FC = ({ stats, columns = 6, loading, error }) => { + if (error) { + return ( + +
Ошибка загрузки статистики: {error}
+
+ ) + } + + return ( +
+ {stats.map((stat) => ( + + ))} +
+ ) +} +``` + +**Следование этим правилам обеспечит надёжную работу статистических компонентов!** 🚀