Files
sfera-new/docs/presentation-layer/STATISTICAL_COMPONENTS_RULES.md
Veronika Smirnova 121a4dece1 docs: создать правила для синхронизации данных, layout и статистических компонентов
- 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 <noreply@anthropic.com>
2025-08-27 12:29:00 +03:00

599 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📊 ПРАВИЛА СТАТИСТИЧЕСКИХ КОМПОНЕНТОВ
> **Цель:** Обеспечить корректную работу, синхронизацию и консистентность статистических компонентов в системе 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
// Подсчёт количества элементов
;<StatCard title="Всего позиций" value={supplies.length} icon={Package} color="blue" format="number" />
// Логика расчёта
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
// Суммирование значений
;<StatCard
title="Остаток"
value={supplies.reduce((sum, s) => 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
// Показатели состояния системы
<StatCard
title="Доступно"
value={availableSupplies.length}
icon={CheckCircle}
color="green"
subtitle={`из ${totalSupplies} позиций`}
trend={{ value: +5, period: 'за неделю' }}
/>
```
### **ТИП 4: Финансовые показатели (Financial)**
```jsx
// Денежные значения
<StatCard
title="Общая стоимость"
value={totalInventoryValue}
icon={DollarSign}
color="purple"
format="currency"
currency="RUB"
/>
```
---
## 🏢 **СПЕЦИФИЧЕСКИЕ ПРАВИЛА ДЛЯ ФУЛФИЛМЕНТА**
### **КАРТОЧКИ ГЛАВНОГО 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
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
{/* Основная информация */}
<p className="text-xs font-medium text-white/60 uppercase tracking-wider">{title}</p>
<p className="text-2xl font-bold text-{color}-300 mt-1">
{formattedValue} {unit && <span className="text-sm">{unit}</span>}
</p>
{/* Дополнительная информация */}
{subtitle && <p className="text-xs text-white/40 mt-1">{subtitle}</p>}
</div>
{/* Иконка */}
<div className="p-2 bg-{color}-500/20 rounded-lg">
<Icon className="h-5 w-5 text-{color}-300" />
</div>
</div>
</Card>
```
### **ЦВЕТОВЫЕ СХЕМЫ ПО ТИПАМ**
```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 <div>{master.value} vs {detail.sum}</div> // Может не совпадать!
}
// ✅ ПРАВИЛЬНО - с проверкой синхронизации
const Stats = () => {
const master = useQuery(MASTER_QUERY)
const detail = useQuery(DETAIL_QUERY)
useEffect(() => {
validateConsistency(master.data, detail.data)
}, [master.data, detail.data])
return <ValidatedStatsView />
}
```
---
## 🔧 **ИНСТРУМЕНТЫ ОТЛАДКИ И МОНИТОРИНГА**
### **DEBUG КОМПОНЕНТ ДЛЯ СТАТИСТИКИ**
```jsx
const StatisticsDebugPanel = ({ masterData, detailData }) => {
if (process.env.NODE_ENV !== 'development') return null
return (
<div className="fixed bottom-4 right-4 bg-black/80 p-4 rounded-lg text-xs text-white">
<h3>Statistics Debug</h3>
<div>Master: {masterData?.value}</div>
<div>Detail Sum: {detailData?.reduce((sum, item) => sum + item.value, 0)}</div>
<div>Sync: {masterData?.value === detailSum ? '✅' : '❌'}</div>
<div>Last Update: {new Date().toLocaleTimeString()}</div>
</div>
)
}
```
### **АВТОМАТИЧЕСКИЕ ТЕСТЫ СИНХРОНИЗАЦИИ**
```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<StatCardProps> = ({
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 (
<Card className="glass-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium text-white/60 uppercase tracking-wider">{title}</p>
<p className={`text-2xl font-bold text-${color}-300 mt-1`}>
{formattedValue}
{unit && <span className="text-sm ml-1">{unit}</span>}
</p>
{subtitle && <p className="text-xs text-white/40 mt-1">{subtitle}</p>}
{trend && (
<p className="text-xs text-white/50 mt-1">
{trend.value > 0 ? '+' : ''}
{trend.value} {trend.period}
</p>
)}
</div>
<div className={`p-2 bg-${color}-500/20 rounded-lg`}>
<Icon className={`h-5 w-5 text-${color}-300`} />
</div>
</div>
</Card>
)
}
```
### **КОМПОНЕНТ ГРУППЫ СТАТИСТИК**
```tsx
interface StatsGridProps {
stats: Array<StatCardProps & { id: string }>
columns?: 1 | 2 | 3 | 4 | 6
loading?: boolean
error?: string
}
export const StatsGrid: React.FC<StatsGridProps> = ({ stats, columns = 6, loading, error }) => {
if (error) {
return (
<Card className="glass-card p-4">
<div className="text-red-300 text-center">Ошибка загрузки статистики: {error}</div>
</Card>
)
}
return (
<div
className={`
grid grid-cols-1
md:grid-cols-2
lg:grid-cols-${Math.min(columns, 4)}
xl:grid-cols-${columns}
gap-4
`}
>
{stats.map((stat) => (
<StatCard key={stat.id} {...stat} loading={loading} />
))}
</div>
)
}
```
**Следование этим правилам обеспечит надёжную работу статистических компонентов!** 🚀