
- Добавить fetchPolicy: 'cache-and-network' в раздел услуг - Добавить pollInterval: 30000 для автоматического обновления - Обеспечить синхронизацию данных между складом и услугами 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1324 lines
62 KiB
TypeScript
1324 lines
62 KiB
TypeScript
'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"> </span>
|
||
<div className="flex items-center space-x-2">
|
||
<div className="w-3 h-3"> </div> {/* Пустое место под стрелку */}
|
||
<div className="w-6 h-6"> </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>
|
||
)
|
||
} |