# 📊 ПРАВИЛА СТАТИСТИЧЕСКИХ КОМПОНЕНТОВ > **Цель:** Обеспечить корректную работу, синхронизацию и консистентность статистических компонентов в системе 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) => ( ))}
) } ``` **Следование этим правилам обеспечит надёжную работу статистических компонентов!** 🚀