Files
sfera-new/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard/index.tsx
Veronika Smirnova 2790fa9b98 fix(components): синхронизировать кеширование между связанными компонентами
- Добавить fetchPolicy: 'cache-and-network' в раздел услуг
- Добавить pollInterval: 30000 для автоматического обновления
- Обеспечить синхронизацию данных между складом и услугами

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-27 12:28:36 +03:00

1324 lines
62 KiB
TypeScript
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.

'use client'
import { useQuery } from '@apollo/client'
import {
Package,
TrendingUp,
TrendingDown,
AlertTriangle,
RotateCcw,
Wrench,
Users,
Box,
Search,
ArrowUpDown,
Store,
Package2,
Eye,
EyeOff,
ChevronRight,
ChevronDown,
Layers,
Truck,
Clock,
CheckCircle,
Settings,
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState, useMemo } from 'react'
import { toast } from 'sonner'
import { Sidebar } from '@/components/dashboard/sidebar'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import {
GET_MY_COUNTERPARTIES,
GET_SUPPLY_ORDERS,
GET_WAREHOUSE_PRODUCTS,
GET_WAREHOUSE_DATA, // Новый запрос данных склада с партнерами
GET_MY_SUPPLIES, // Расходники селлеров (старые данные заказов)
GET_SELLER_SUPPLIES_ON_WAREHOUSE, // Расходники селлеров на складе (новый API)
GET_MY_FULFILLMENT_SUPPLIES, // Расходники фулфилмента
GET_FULFILLMENT_WAREHOUSE_STATS, // Статистика склада с изменениями за сутки
GET_SUPPLY_MOVEMENTS, // Движения товаров (прибыло/убыло)
} from '@/graphql/queries'
import { useAuth } from '@/hooks/useAuth'
import { useRealtime } from '@/hooks/useRealtime'
import { useSidebar } from '@/hooks/useSidebar'
import { WbReturnClaims } from '../wb-return-claims'
import { StatCard } from './blocks/StatCard'
// Типы данных для 3-уровневой иерархии
interface ProductVariant { // 🟠 УРОВЕНЬ 3: Варианты товаров
id: string
name: string // Размер, характеристика, вариант упаковки
// Места и количества для каждого типа на уровне варианта
productPlace?: string
productQuantity: number
goodsPlace?: string
goodsQuantity: number
defectsPlace?: string
defectsQuantity: number
sellerSuppliesPlace?: string
sellerSuppliesQuantity: number
sellerSuppliesOwners?: string[] // Владельцы расходников
pvzReturnsPlace?: string
pvzReturnsQuantity: number
}
interface ProductItem { // 🟢 УРОВЕНЬ 2: Товары
id: string
name: string
article: string
// Места и количества для каждого типа
productPlace?: string
productQuantity: number
goodsPlace?: string
goodsQuantity: number
defectsPlace?: string
defectsQuantity: number
sellerSuppliesPlace?: string
sellerSuppliesQuantity: number
sellerSuppliesOwners?: string[] // Владельцы расходников
pvzReturnsPlace?: string
pvzReturnsQuantity: number
// Третий уровень - варианты товара
variants?: ProductVariant[]
}
interface StoreData { // 🔵 УРОВЕНЬ 1: Магазины
id: string
name: string
logo?: string
avatar?: string // Аватар пользователя организации
products: number
goods: number
defects: number
sellerSupplies: number
pvzReturns: number
// Изменения за сутки
productsChange: number
goodsChange: number
defectsChange: number
sellerSuppliesChange: number
pvzReturnsChange: number
// Детализация по товарам
items: ProductItem[]
}
interface WarehouseStats {
products: { current: number; change: number; arrived: number; departed: number }
goods: { current: number; change: number; arrived: number; departed: number }
defects: { current: number; change: number; arrived: number; departed: number }
pvzReturns: { current: number; change: number; arrived: number; departed: number }
fulfillmentSupplies: { current: number; change: number; arrived: number; departed: number }
sellerSupplies: { current: number; change: number; arrived: number; departed: number }
}
export function FulfillmentWarehouseDashboard() {
const router = useRouter()
const { getSidebarMargin } = useSidebar()
const { user } = useAuth()
// Состояния для поиска и фильтрации
const [searchTerm, setSearchTerm] = useState('')
const [sortField, setSortField] = useState<keyof StoreData>('name')
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
// Состояния для 3-уровневой иерархии
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set()) // 🔵 Раскрытые магазины
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set()) // 🟢 Раскрытые товары
const [showReturnClaims, setShowReturnClaims] = useState(false)
const [showAdditionalValues, setShowAdditionalValues] = useState(true)
// Загружаем данные из GraphQL
const {
data: counterpartiesData,
loading: counterpartiesLoading,
error: counterpartiesError,
refetch: refetchCounterparties,
} = useQuery(GET_MY_COUNTERPARTIES, {
pollInterval: 30000,
errorPolicy: 'all',
onError: (error) => {
console.warn('Ошибка загрузки контрагентов:', error)
},
})
const {
data: ordersData,
loading: ordersLoading,
error: ordersError,
refetch: refetchOrders,
} = useQuery(GET_SUPPLY_ORDERS, {
pollInterval: 30000,
errorPolicy: 'all',
onError: (error) => {
console.warn('Ошибка загрузки заказов:', error)
},
})
const {
data: warehouseData,
loading: warehouseLoading,
error: warehouseError,
refetch: refetchWarehouse,
} = useQuery(GET_WAREHOUSE_PRODUCTS, {
pollInterval: 30000,
errorPolicy: 'all',
onError: (error) => {
console.warn('Ошибка загрузки товаров склада:', error)
},
})
const {
data: sellerSuppliesData,
loading: sellerSuppliesLoading,
error: sellerSuppliesError,
refetch: refetchSellerSupplies,
} = useQuery(GET_SELLER_SUPPLIES_ON_WAREHOUSE, {
pollInterval: 30000,
errorPolicy: 'all',
onError: (error) => {
console.warn('Ошибка загрузки расходников селлеров:', error)
},
})
const {
data: fulfillmentSuppliesData,
loading: fulfillmentSuppliesLoading,
error: fulfillmentSuppliesError,
refetch: refetchFulfillmentSupplies,
} = useQuery(GET_MY_FULFILLMENT_SUPPLIES, {
pollInterval: 30000,
errorPolicy: 'all',
onError: (error) => {
console.warn('Ошибка загрузки расходников фулфилмента:', error)
},
})
const {
data: warehouseStatsData,
loading: warehouseStatsLoading,
error: warehouseStatsError,
refetch: refetchWarehouseStats,
} = useQuery(GET_FULFILLMENT_WAREHOUSE_STATS, {
fetchPolicy: 'cache-and-network', // Синхронизация с карточкой "ОСТАТОК"
pollInterval: 30000,
errorPolicy: 'all',
onError: (error) => {
console.warn('Ошибка загрузки статистики склада:', error)
},
})
// Новый запрос данных склада с партнерами
const {
data: partnerWarehouseData,
loading: partnerWarehouseLoading,
error: partnerWarehouseError,
refetch: refetchPartnerWarehouse,
} = useQuery(GET_WAREHOUSE_DATA, {
pollInterval: 60000, // Реже обновляем данные партнеров
errorPolicy: 'all',
onError: (error) => {
console.warn('Ошибка загрузки данных склада с партнерами:', error)
},
})
// Запрос движений товаров (прибыло/убыло)
const {
data: supplyMovementsData,
loading: supplyMovementsLoading,
error: supplyMovementsError,
refetch: refetchSupplyMovements,
} = useQuery(GET_SUPPLY_MOVEMENTS, {
variables: { period: '24h' },
pollInterval: 30000, // Обновляем каждые 30 секунд
errorPolicy: 'all',
onError: (error) => {
console.warn('Ошибка загрузки движений товаров:', error)
},
})
// Real-time обновления
useRealtime(() => {
refetchCounterparties()
refetchOrders()
refetchWarehouse()
refetchSellerSupplies()
refetchFulfillmentSupplies()
refetchWarehouseStats()
refetchSupplyMovements()
})
// Общий статус загрузки
const loading =
counterpartiesLoading ||
ordersLoading ||
warehouseLoading ||
sellerSuppliesLoading ||
fulfillmentSuppliesLoading ||
warehouseStatsLoading ||
supplyMovementsLoading
// === КРИТИЧЕСКАЯ БИЗНЕС-ЛОГИКА ОБРАБОТКИ ДАННЫХ ===
const formatNumber = (num: number): string => {
if (num === 0) return '0'
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
}
// Функция для расчета статистики склада
const warehouseStats: WarehouseStats = useMemo(() => {
const stats = warehouseStatsData?.fulfillmentWarehouseStats
const movements = supplyMovementsData?.supplyMovements
if (stats) {
return {
products: {
current: stats.products?.current || 0,
change: stats.products?.change || 0,
arrived: movements?.arrived?.products || 0,
departed: movements?.departed?.products || 0,
},
goods: {
current: stats.goods?.current || 0,
change: stats.goods?.change || 0,
arrived: movements?.arrived?.goods || 0,
departed: movements?.departed?.goods || 0,
},
defects: {
current: stats.defects?.current || 0,
change: stats.defects?.change || 0,
arrived: movements?.arrived?.defects || 0,
departed: movements?.departed?.defects || 0,
},
pvzReturns: {
current: stats.pvzReturns?.current || 0,
change: stats.pvzReturns?.change || 0,
arrived: movements?.arrived?.pvzReturns || 0,
departed: movements?.departed?.pvzReturns || 0,
},
fulfillmentSupplies: {
current: stats.fulfillmentSupplies?.current || 0,
change: stats.fulfillmentSupplies?.change || 0,
arrived: movements?.arrived?.fulfillmentSupplies || 0,
departed: movements?.departed?.fulfillmentSupplies || 0,
},
sellerSupplies: {
current: stats.sellerSupplies?.current || 0,
change: stats.sellerSupplies?.change || 0,
arrived: movements?.arrived?.sellerSupplies || 0,
departed: movements?.departed?.sellerSupplies || 0,
},
}
}
// Fallback: считаем из загруженных данных
const warehouseProducts = warehouseData?.warehouseProducts || []
const sellerSupplies = sellerSuppliesData?.sellerSuppliesOnWarehouse || []
const fulfillmentSupplies = fulfillmentSuppliesData?.myFulfillmentSupplies || []
return {
products: {
current: warehouseProducts.filter((p: any) => p.type === 'PRODUCT').length,
change: 0,
arrived: movements?.arrived?.products || 0,
departed: movements?.departed?.products || 0,
},
goods: {
current: warehouseProducts.filter((p: any) => p.type === 'GOODS').length,
change: 0,
arrived: movements?.arrived?.goods || 0,
departed: movements?.departed?.goods || 0,
},
defects: {
current: warehouseProducts.filter((p: any) => p.type === 'DEFECTS').length,
change: 0,
arrived: movements?.arrived?.defects || 0,
departed: movements?.departed?.defects || 0,
},
pvzReturns: {
current: warehouseProducts.filter((p: any) => p.type === 'PVZ_RETURNS').length,
change: 0,
arrived: movements?.arrived?.pvzReturns || 0,
departed: movements?.departed?.pvzReturns || 0,
},
fulfillmentSupplies: {
current: fulfillmentSupplies.length,
change: 0,
arrived: movements?.arrived?.fulfillmentSupplies || 0,
departed: movements?.departed?.fulfillmentSupplies || 0,
},
sellerSupplies: {
current: sellerSupplies.length,
change: 0,
arrived: movements?.arrived?.sellerSupplies || 0,
departed: movements?.departed?.sellerSupplies || 0,
},
}
}, [warehouseStatsData, warehouseData, sellerSuppliesData, fulfillmentSuppliesData, supplyMovementsData])
// === КРИТИЧЕСКАЯ ЛОГИКА ГРУППИРОВКИ ДАННЫХ ПО МАГАЗИНАМ ===
const storeData: StoreData[] = useMemo(() => {
console.warn('🔄 Пересчитываем storeData...')
// НОВАЯ ЛОГИКА: Используем данные из GET_WAREHOUSE_DATA если доступны
const partnerStores = partnerWarehouseData?.warehouseData?.stores || []
const sellerCounterparties = counterpartiesData?.getMyCounterparties?.filter((c: any) => c.type === 'SELLER') || []
const warehouseProducts = warehouseData?.getWarehouseProducts || []
const sellerSupplies = sellerSuppliesData?.getSellerSuppliesOnWarehouse || []
console.warn('📊 Исходные данные для группировки:', {
partnerStores: partnerStores.length,
sellers: sellerCounterparties.length,
warehouseProducts: warehouseProducts.length,
sellerSupplies: sellerSupplies.length,
})
// Если есть данные от нового API - используем их
if (partnerStores.length > 0) {
console.warn('✨ ИСПОЛЬЗУЕМ НОВУЮ ЛОГИКУ С ПАРТНЕРАМИ')
return partnerStores.map((store: any) => ({
id: store.id,
name: store.storeName,
logo: store.storeImage,
avatar: null,
products: store.storeQuantity,
goods: 0,
defects: 0,
sellerSupplies: 0,
pvzReturns: 0,
// Движения товаров (прибыло/убыло) - по умолчанию 0
productsArrived: 0, // TODO: считать из реальных поставок на фулфилмент
productsDeparted: 0, // TODO: считать из реальных поставок на маркетплейсы
goodsArrived: 0,
goodsDeparted: 0,
defectsArrived: 0,
defectsDeparted: 0,
sellerSuppliesArrived: 0,
sellerSuppliesDeparted: 0,
pvzReturnsArrived: 0,
pvzReturnsDeparted: 0,
items: store.products?.map((product: any) => ({
id: product.id,
name: product.productName,
article: '',
productQuantity: product.productQuantity,
productPlace: product.productPlace,
goodsQuantity: 0,
defectsQuantity: 0,
sellerSuppliesQuantity: 0,
pvzReturnsQuantity: 0,
variants: product.variants?.map((variant: any) => ({
id: variant.id,
name: variant.variantName,
quantity: variant.variantQuantity,
place: variant.variantPlace,
})) || [],
})) || [],
}))
}
// Fallback: используем старую логику
return sellerCounterparties.map((seller: any) => {
const sellerId = seller.id
const sellerName = seller.organization?.name || seller.name || 'Неизвестный селлер'
// КРИТИЧНО: Группировка товаров/продуктов по названию с суммированием
const sellerProducts = warehouseProducts.filter((p: any) => p.sellerId === sellerId)
// Группируем по названию товара
const productGroups = sellerProducts.reduce((acc: any, product: any) => {
const key = product.name || 'Без названия'
if (!acc[key]) {
acc[key] = {
id: `${sellerId}-${key}`,
name: key,
article: product.article || '',
productQuantity: 0,
goodsQuantity: 0,
defectsQuantity: 0,
sellerSuppliesQuantity: 0,
pvzReturnsQuantity: 0,
sellerSuppliesOwners: [],
variants: [],
}
}
// Суммируем количества
acc[key].productQuantity += product.productQuantity || 0
acc[key].goodsQuantity += product.goodsQuantity || 0
acc[key].defectsQuantity += product.defectsQuantity || 0
acc[key].pvzReturnsQuantity += product.pvzReturnsQuantity || 0
return acc
}, {})
// КРИТИЧНО: Группировка расходников селлера по ВЛАДЕЛЬЦУ (не по названию!)
const sellerSuppliesForThisSeller = sellerSupplies.filter((supply: any) =>
supply.type === 'SELLER_CONSUMABLES' &&
supply.sellerId === sellerId,
)
console.warn(`📦 Расходники для селлера ${sellerName}:`, sellerSuppliesForThisSeller.length)
// Группируем расходники по владельцу
const suppliesGroups = sellerSuppliesForThisSeller.reduce((acc: any, supply: any) => {
const ownerKey = supply.ownerName || supply.sellerName || 'Неизвестный владелец'
if (!acc[ownerKey]) {
acc[ownerKey] = {
id: `${sellerId}-supply-${ownerKey}`,
name: `Расходники ${ownerKey}`,
article: '',
productQuantity: 0,
goodsQuantity: 0,
defectsQuantity: 0,
sellerSuppliesQuantity: 0,
pvzReturnsQuantity: 0,
sellerSuppliesOwners: [ownerKey],
variants: [],
}
}
acc[ownerKey].sellerSuppliesQuantity += supply.quantity || 0
return acc
}, {})
const allItems = [...Object.values(productGroups), ...Object.values(suppliesGroups)] as ProductItem[]
// Подсчет итогов для магазина
const totals = allItems.reduce(
(acc, item) => ({
products: acc.products + (item.productQuantity || 0),
goods: acc.goods + (item.goodsQuantity || 0),
defects: acc.defects + (item.defectsQuantity || 0),
sellerSupplies: acc.sellerSupplies + (item.sellerSuppliesQuantity || 0),
pvzReturns: acc.pvzReturns + (item.pvzReturnsQuantity || 0),
}),
{ products: 0, goods: 0, defects: 0, sellerSupplies: 0, pvzReturns: 0 },
)
console.warn(`📊 Итоги для ${sellerName}:`, totals)
return {
id: sellerId,
name: sellerName,
logo: seller.organization?.logo,
avatar: seller.organization?.user?.avatar,
products: totals.products,
goods: totals.goods,
defects: totals.defects,
sellerSupplies: totals.sellerSupplies,
pvzReturns: totals.pvzReturns,
// Движения товаров (прибыло/убыло) - по умолчанию 0
productsArrived: 0, // TODO: считать из реальных поставок на фулфилмент
productsDeparted: 0, // TODO: считать из реальных поставок на маркетплейсы
goodsArrived: 0,
goodsDeparted: 0,
defectsArrived: 0,
defectsDeparted: 0,
sellerSuppliesArrived: 0,
sellerSuppliesDeparted: 0,
pvzReturnsArrived: 0,
pvzReturnsDeparted: 0,
items: allItems,
}
})
}, [partnerWarehouseData, counterpartiesData, warehouseData, sellerSuppliesData])
// Фильтрация и сортировка данных
const filteredAndSortedStores = useMemo(() => {
let filtered = storeData
if (searchTerm) {
filtered = filtered.filter((store) =>
store.name.toLowerCase().includes(searchTerm.toLowerCase()),
)
}
return filtered.sort((a, b) => {
const aValue = a[sortField]
const bValue = b[sortField]
if (typeof aValue === 'string' && typeof bValue === 'string') {
return sortOrder === 'asc'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue)
}
if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortOrder === 'asc' ? aValue - bValue : bValue - aValue
}
return 0
})
}, [storeData, searchTerm, sortField, sortOrder])
// Подсчет общих итогов
const totals = useMemo(() => {
return filteredAndSortedStores.reduce(
(acc, store) => ({
products: acc.products + store.products,
goods: acc.goods + store.goods,
defects: acc.defects + store.defects,
sellerSupplies: acc.sellerSupplies + store.sellerSupplies,
pvzReturns: acc.pvzReturns + store.pvzReturns,
}),
{ products: 0, goods: 0, defects: 0, sellerSupplies: 0, pvzReturns: 0 },
)
}, [filteredAndSortedStores])
// Вспомогательные функции для UI
const handleSort = (field: keyof StoreData) => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field)
setSortOrder('asc')
}
}
// Функции управления 3-уровневой иерархией
const toggleStoreExpansion = (storeId: string) => {
setExpandedStores(prev => {
const newSet = new Set(prev)
if (newSet.has(storeId)) {
newSet.delete(storeId)
} else {
newSet.add(storeId)
}
return newSet
})
}
const toggleItemExpansion = (itemId: string) => {
setExpandedItems(prev => {
const newSet = new Set(prev)
if (newSet.has(itemId)) {
newSet.delete(itemId)
} else {
newSet.add(itemId)
}
return newSet
})
}
// Компонент заголовка таблицы с сортировкой
const TableHeader = ({
field,
children,
sortable = false,
}: {
field?: keyof StoreData
children: React.ReactNode
sortable?: boolean
}) => (
<div
className={`px-3 py-2 text-left text-xs font-medium text-blue-100 uppercase tracking-wider ${
sortable ? 'cursor-pointer hover:text-white hover:bg-blue-500/10' : ''
} flex items-center space-x-1`}
onClick={sortable && field ? () => handleSort(field) : undefined}
>
<span>{children}</span>
{sortable && field === sortField && (
<ArrowUpDown className="w-3 h-3" />
)}
</div>
)
// === ОБРАБОТКА СОСТОЯНИЙ ===
if (loading && storeData.length === 0) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex items-center justify-center`}>
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
<span className="text-white/60">Загрузка данных склада...</span>
</div>
</main>
</div>
)
}
// === РЕНДЕР ИНТЕРФЕЙСА ===
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300`}>
<div className="flex-1 overflow-y-auto space-y-4">
{/* Статистические карты склада */}
<div className="flex-shrink-0 mb-4">
<div className="glass-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-semibold text-blue-400">Статистика склада</h2>
<div className="flex items-center space-x-2">
<button
onClick={() => {
refetchCounterparties()
refetchOrders()
refetchWarehouse()
refetchSellerSupplies()
refetchFulfillmentSupplies()
refetchWarehouseStats()
refetchSupplyMovements()
}}
disabled={loading}
className="h-7 text-xs bg-white/10 border-white/20 text-white hover:bg-white/20 px-3 rounded"
>
{loading ? 'Обновляется...' : 'Обновить'}
</button>
</div>
</div>
{/* Блок статистических карт */}
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
{/* ЭТАП 1: Добавлены прибыло/убыло в карточки */}
{/* ЭТАП 3: Добавлен индикатор загрузки */}
<StatCard
title="Продукты"
icon={Box}
current={totals.products}
change={warehouseStats.products.change}
description="Готовые к отправке"
showMovements={true}
arrived={warehouseStats.products.arrived}
departed={warehouseStats.products.departed}
isLoading={loading}
/>
<StatCard
title="Товары"
icon={Package}
current={totals.goods}
change={warehouseStats.goods.change}
description="На складе и в обработке"
showMovements={true}
arrived={warehouseStats.goods.arrived}
departed={warehouseStats.goods.departed}
isLoading={loading}
/>
<StatCard
title="Брак"
icon={AlertTriangle}
current={totals.defects}
change={warehouseStats.defects.change}
description="Требует утилизации"
showMovements={true}
arrived={warehouseStats.defects.arrived}
departed={warehouseStats.defects.departed}
isLoading={loading}
/>
<StatCard
title="Возвраты с ПВЗ"
icon={RotateCcw}
current={totals.pvzReturns}
change={warehouseStats.pvzReturns.change}
description="К обработке"
showMovements={true}
arrived={warehouseStats.pvzReturns.arrived}
departed={warehouseStats.pvzReturns.departed}
isLoading={loading}
/>
<StatCard
title="Расходники селлеров"
icon={Users}
current={totals.sellerSupplies}
change={warehouseStats.sellerSupplies.change}
description="Материалы клиентов"
showMovements={true}
arrived={warehouseStats.sellerSupplies.arrived}
departed={warehouseStats.sellerSupplies.departed}
isLoading={loading}
/>
<StatCard
title="Расходники фулфилмента"
icon={Wrench}
current={warehouseStats.fulfillmentSupplies.current}
change={warehouseStats.fulfillmentSupplies.change}
description="Операционные материалы"
onClick={() => router.push('/fulfillment-warehouse/supplies')}
showMovements={true}
arrived={warehouseStats.fulfillmentSupplies.arrived}
departed={warehouseStats.fulfillmentSupplies.departed}
isLoading={loading}
/>
{/* ОТКАТ ВСЕХ ЭТАПОВ: Вернуться к исходным карточкам */}
{/*
<StatCard
title="Продукты"
icon={Box}
current={totals.products}
change={warehouseStats.products.change}
description="Готовые к отправке"
/>
<StatCard
title="Товары"
icon={Package}
current={totals.goods}
change={warehouseStats.goods.change}
description="На складе и в обработке"
/>
<StatCard
title="Брак"
icon={AlertTriangle}
current={totals.defects}
change={warehouseStats.defects.change}
description="Требует утилизации"
/>
<StatCard
title="Возвраты с ПВЗ"
icon={RotateCcw}
current={totals.pvzReturns}
change={warehouseStats.pvzReturns.change}
description="К обработке"
/>
<StatCard
title="Расходники селлеров"
icon={Users}
current={totals.sellerSupplies}
change={warehouseStats.sellerSupplies.change}
description="Материалы клиентов"
/>
<StatCard
title="Расходники фулфилмента"
icon={Wrench}
current={warehouseStats.fulfillmentSupplies.current}
change={warehouseStats.fulfillmentSupplies.change}
description="Операционные материалы"
onClick={() => router.push('/fulfillment-warehouse/supplies')}
/>
*/}
</div>
</div>
</div>
{/* Таблица данных */}
<div className="flex-1 min-h-0">
<div className="glass-card p-4 h-full flex flex-col">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-semibold text-blue-400">Детализация по магазинам</h2>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/40" />
<Input
placeholder="Поиск магазина..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 h-8 text-xs bg-white/10 border-white/20 text-white placeholder-white/40"
/>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowAdditionalValues(!showAdditionalValues)}
className="h-8 text-xs text-white/60 hover:text-white hover:bg-white/10"
>
{showAdditionalValues ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
{showAdditionalValues ? 'Скрыть изменения' : 'Показать изменения'}
</Button>
</div>
</div>
{/* УРОВЕНЬ 1: Заголовки таблицы магазинов */}
<div className="flex-shrink-0 bg-blue-500/20 border-b border-blue-500/40">
<div className="grid grid-cols-6 gap-0">
<TableHeader field="name" sortable>
/ Магазин
</TableHeader>
<TableHeader field="products" sortable>
Продукты
</TableHeader>
<TableHeader field="goods" sortable>
Товары
</TableHeader>
<TableHeader field="defects" sortable>
Брак
</TableHeader>
<TableHeader field="sellerSupplies" sortable>
Расходники селлеров
</TableHeader>
<TableHeader field="pvzReturns" sortable>
Возвраты с ПВЗ
</TableHeader>
</div>
</div>
{/* Строка итогов */}
<div className="flex-shrink-0 bg-blue-500/25 border-b border-blue-500/50">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-2.5 flex items-center space-x-2">
<span className="text-white/60 text-xs">&nbsp;</span>
<div className="flex items-center space-x-2">
<div className="w-3 h-3">&nbsp;</div> {/* Пустое место под стрелку */}
<div className="w-6 h-6">&nbsp;</div> {/* Пустое место под аватар */}
<span className="text-blue-300 font-bold text-xs">ИТОГО ({filteredAndSortedStores.length})</span>
</div>
</div>
<div className="px-3 py-2.5 text-xs font-bold text-white">
{formatNumber(totals.products)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{warehouseStats.products.arrived}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{warehouseStats.products.departed}</span>
</span>
</div>
<div className="px-3 py-2.5 text-xs font-bold text-white">
{formatNumber(totals.goods)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{warehouseStats.goods.arrived}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{warehouseStats.goods.departed}</span>
</span>
</div>
<div className="px-3 py-2.5 text-xs font-bold text-white">
{formatNumber(totals.defects)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{warehouseStats.defects.arrived}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{warehouseStats.defects.departed}</span>
</span>
</div>
<div className="px-3 py-2.5 text-xs font-bold text-white">
{formatNumber(totals.sellerSupplies)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{warehouseStats.sellerSupplies.arrived}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{warehouseStats.sellerSupplies.departed}</span>
</span>
</div>
<div className="px-3 py-2.5 text-xs font-bold text-white">
{formatNumber(totals.pvzReturns)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{warehouseStats.pvzReturns.arrived}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{warehouseStats.pvzReturns.departed}</span>
</span>
</div>
</div>
</div>
{/* ОСНОВНЫЕ ДАННЫЕ: 3-уровневая иерархия */}
<div className="flex-1 overflow-y-auto">
{filteredAndSortedStores.map((store, index) => (
<div key={store.id} className="border-b border-blue-500/30 hover:bg-blue-500/5 transition-colors border-l-8 border-l-blue-400 bg-blue-500/5 shadow-sm hover:shadow-md">
{/* 🔵 УРОВЕНЬ 1: Основная строка магазина */}
<div
className="grid grid-cols-6 gap-0 cursor-pointer"
onClick={() => toggleStoreExpansion(store.id)}
>
<div className="px-3 py-2.5 flex items-center space-x-2">
<span className="text-white/60 text-xs">{filteredAndSortedStores.length - index}</span>
<div className="flex items-center space-x-2">
{expandedStores.has(store.id) ? (
<ChevronDown className="w-3 h-3 text-white/60" />
) : (
<ChevronRight className="w-3 h-3 text-white/60" />
)}
<Avatar className="w-6 h-6">
{store.avatar && <AvatarImage src={store.avatar} alt={store.name} />}
<AvatarFallback className="bg-blue-500 text-white font-medium text-xs">
{store.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
<span className="text-white font-medium text-sm truncate">{store.name}</span>
</div>
</div>
<div className="px-3 py-2.5 text-white font-medium text-sm">
{formatNumber(store.products)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{store.productsArrived || 0}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{store.productsDeparted || 0}</span>
</span>
</div>
<div className="px-3 py-2.5 text-white font-medium text-sm">
{formatNumber(store.goods)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{store.goodsArrived || 0}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{store.goodsDeparted || 0}</span>
</span>
</div>
<div className="px-3 py-2.5 text-white font-medium text-sm">
{formatNumber(store.defects)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{store.defectsArrived || 0}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{store.defectsDeparted || 0}</span>
</span>
</div>
<div className="px-3 py-2.5 text-white font-medium text-sm">
{formatNumber(store.sellerSupplies)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{store.sellerSuppliesArrived || 0}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{store.sellerSuppliesDeparted || 0}</span>
</span>
</div>
<div className="px-3 py-2.5 text-white font-medium text-sm">
{formatNumber(store.pvzReturns)}
<span className="ml-1 text-[10px]">
<span className="text-green-400">+{store.pvzReturnsArrived || 0}</span>
<span className="text-white/40 mx-1">|</span>
<span className="text-red-400">-{store.pvzReturnsDeparted || 0}</span>
</span>
</div>
</div>
{/* 🟢 УРОВЕНЬ 2: Развернутые товары */}
{expandedStores.has(store.id) && (
<div className="bg-green-500/5 border-t border-green-500/20">
{/* Заголовки второго уровня */}
<div className="border-b border-green-500/20 bg-green-500/10">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider">
Наименование
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-left">
Место
</div>
</div>
</div>
</div>
{/* Данные товаров */}
<div className="max-h-64 overflow-y-auto scrollbar-thin scrollbar-thumb-green-500/30 scrollbar-track-transparent">
{store.items?.map((item) => (
<div key={item.id}>
{/* Основная строка товара */}
<div
className="border-b border-green-500/15 hover:bg-green-500/10 transition-colors cursor-pointer border-l-4 border-l-green-500/40 ml-4"
onClick={() => toggleItemExpansion(item.id)}
>
<div className="grid grid-cols-6 gap-0">
{/* Наименование */}
<div className="px-3 py-2 flex items-center">
<div className="flex-1">
<div className="text-white font-medium text-xs flex items-center space-x-2">
{expandedItems.has(item.id) ? (
<ChevronDown className="w-3 h-3 text-green-400" />
) : (
<ChevronRight className="w-3 h-3 text-green-400" />
)}
<div className="w-2 h-2 bg-green-500 rounded flex-shrink-0"></div>
<span>{item.name}</span>
{item.variants && item.variants.length > 0 && (
<Badge className="bg-orange-500/20 text-orange-300 text-[10px] px-1 py-0">
{item.variants.length} вар.
</Badge>
)}
</div>
{item.article && (
<div className="text-white/60 text-[10px] mt-0.5">
Артикул: {item.article}
</div>
)}
</div>
</div>
{/* Продукты */}
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-left text-xs text-white font-medium">
{formatNumber(item.productQuantity)}
</div>
<div className="px-3 py-2 text-left text-xs text-white/60">
{item.productPlace || '-'}
</div>
</div>
{/* Товары */}
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-left text-xs text-white font-medium">
{formatNumber(item.goodsQuantity)}
</div>
<div className="px-3 py-2 text-left text-xs text-white/60">
{item.goodsPlace || '-'}
</div>
</div>
{/* Брак */}
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-left text-xs text-white font-medium">
{formatNumber(item.defectsQuantity)}
</div>
<div className="px-3 py-2 text-left text-xs text-white/60">
{item.defectsPlace || '-'}
</div>
</div>
{/* Расходники селлера */}
<div className="grid grid-cols-2 gap-0">
<Popover>
<PopoverTrigger asChild>
<div className="px-3 py-2 text-left text-xs text-white font-medium cursor-help hover:bg-white/10 rounded">
{formatNumber(item.sellerSuppliesQuantity)}
</div>
</PopoverTrigger>
<PopoverContent className="w-64 glass-card">
<div className="text-xs">
<div className="font-medium mb-2 text-white">Расходники селлеров:</div>
{item.sellerSuppliesOwners && item.sellerSuppliesOwners.length > 0 ? (
item.sellerSuppliesOwners.map((owner, index) => (
<div key={index} className="py-1 text-white/80">
{owner}
</div>
))
) : (
<div className="text-white/60">Нет данных о владельцах</div>
)}
</div>
</PopoverContent>
</Popover>
<div className="px-3 py-2 text-left text-xs text-white/60">
{item.sellerSuppliesPlace || '-'}
</div>
</div>
{/* Возвраты с ПВЗ */}
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-2 text-left text-xs text-white font-medium">
{formatNumber(item.pvzReturnsQuantity)}
</div>
<div className="px-3 py-2 text-left text-xs text-white/60">
{item.pvzReturnsPlace || '-'}
</div>
</div>
</div>
</div>
{/* 🟠 УРОВЕНЬ 3: Варианты товара */}
{expandedItems.has(item.id) && item.variants && item.variants.length > 0 && (
<div className="bg-orange-500/5 border-t border-orange-500/20">
{/* Заголовки для вариантов */}
<div className="border-b border-orange-500/20 bg-orange-500/10">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider">
Вариант
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Кол-во
</div>
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-left">
Место
</div>
</div>
</div>
</div>
{/* Данные по вариантам */}
<div className="max-h-32 overflow-y-auto scrollbar-thin scrollbar-thumb-orange-500/30 scrollbar-track-transparent">
{item.variants.map((variant) => (
<div
key={variant.id}
className="border-b border-orange-500/15 hover:bg-orange-500/10 transition-colors border-l-4 border-l-orange-500/50 ml-8"
>
<div className="grid grid-cols-6 gap-0">
{/* Название варианта */}
<div className="px-3 py-1.5">
<div className="text-white font-medium text-[10px] flex items-center space-x-2">
<div className="w-1.5 h-1.5 bg-orange-500 rounded flex-shrink-0"></div>
<span>{variant.name}</span>
</div>
</div>
{/* Продукты */}
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-left text-[10px] text-white font-medium">
{formatNumber(variant.productQuantity)}
</div>
<div className="px-3 py-1.5 text-left text-[10px] text-white/60">
{variant.productPlace || '-'}
</div>
</div>
{/* Товары */}
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-left text-[10px] text-white font-medium">
{formatNumber(variant.goodsQuantity)}
</div>
<div className="px-3 py-1.5 text-left text-[10px] text-white/60">
{variant.goodsPlace || '-'}
</div>
</div>
{/* Брак */}
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-left text-[10px] text-white font-medium">
{formatNumber(variant.defectsQuantity)}
</div>
<div className="px-3 py-1.5 text-left text-[10px] text-white/60">
{variant.defectsPlace || '-'}
</div>
</div>
{/* Расходники селлера */}
<div className="grid grid-cols-2 gap-0">
<Popover>
<PopoverTrigger asChild>
<div className="px-3 py-1.5 text-left text-[10px] text-white font-medium cursor-help hover:bg-white/10 rounded">
{formatNumber(variant.sellerSuppliesQuantity)}
</div>
</PopoverTrigger>
<PopoverContent className="w-64 glass-card">
<div className="text-xs">
<div className="font-medium mb-2 text-white">
Расходники селлеров:
</div>
{variant.sellerSuppliesOwners && variant.sellerSuppliesOwners.length > 0 ? (
variant.sellerSuppliesOwners.map((owner, index) => (
<div key={index} className="py-1 text-white/80">
{owner}
</div>
))
) : (
<div className="text-white/60">Нет данных о владельцах</div>
)}
</div>
</PopoverContent>
</Popover>
<div className="px-3 py-1.5 text-left text-[10px] text-white/60">
{variant.sellerSuppliesPlace || '-'}
</div>
</div>
{/* Возвраты с ПВЗ */}
<div className="grid grid-cols-2 gap-0">
<div className="px-3 py-1.5 text-left text-[10px] text-white font-medium">
{formatNumber(variant.pvzReturnsQuantity)}
</div>
<div className="px-3 py-1.5 text-left text-[10px] text-white/60">
{variant.pvzReturnsPlace || '-'}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
{/* Блок возвратов WB */}
{showReturnClaims && (
<div className="flex-shrink-0">
<WbReturnClaims onBack={() => setShowReturnClaims(false)} />
</div>
)}
{/* Информация об отсутствии результатов */}
{filteredAndSortedStores.length === 0 && searchTerm && (
<div className="text-center py-8">
<p className="text-white/60">
По запросу "{searchTerm}" ничего не найдено.{' '}
<button
onClick={() => setSearchTerm('')}
className="text-blue-400 hover:underline"
>
Очистить поиск
</button>
</p>
</div>
)}
{/* Отладочная информация */}
{process.env.NODE_ENV === 'development' && (
<div className="mt-8 p-4 bg-white/5 rounded text-xs text-white/40">
<p>🔧 Debug Info:</p>
<p> Загрузка: {loading ? 'да' : 'нет'}</p>
<p> Всего магазинов: {storeData.length}</p>
<p> Отфильтровано: {filteredAndSortedStores.length}</p>
<p> Поиск: {searchTerm || 'нет'}</p>
<p> Сортировка: {sortField} ({sortOrder})</p>
</div>
)}
</div>
</main>
</div>
)
}