This commit is contained in:
Veronika Smirnova
2025-07-24 11:52:59 +03:00
2 changed files with 737 additions and 485 deletions

View File

@ -78,6 +78,7 @@ export function WBWarehouseDashboard() {
const [totalProducts, setTotalProducts] = useState(0)
const [totalStocks, setTotalStocks] = useState(0)
const [totalReserved, setTotalReserved] = useState(0)
const [totalFromClient, setTotalFromClient] = useState(0)
const [activeWarehouses, setActiveWarehouses] = useState(0)
// Загрузка данных
@ -106,53 +107,63 @@ export function WBWarehouseDashboard() {
const wbService = new WildberriesService(apiToken)
console.log('WB Warehouse: Starting data load with Analytics API...')
console.log('WB Warehouse: Starting data load...')
// Сначала получаем карточки товаров для передачи в Analytics API
console.log('WB Warehouse: Getting cards for Analytics API...')
// Сначала получаем карточки товаров - это основа для всего
console.log('WB Warehouse: Getting cards...')
const cards = await WildberriesService.getAllCards(apiToken).catch(() => [])
const nmIds = cards.map(card => card.nmID).filter(id => id > 0)
console.log('WB Warehouse: Found cards for Analytics API:', nmIds)
console.log('WB Warehouse: Found cards:', cards.length)
console.log('WB Warehouse: Card IDs for analytics:', nmIds)
// Загружаем склады, основные данные и Analytics API для движения товаров
const [warehousesData, stocksData, rawAnalyticsData] = await Promise.all([
wbService.getWarehouses().catch((error) => {
console.error('WB Warehouse: Error loading warehouses:', error)
return []
}),
wbService.getStocks().catch((error) => {
console.error('WB Warehouse: Error loading stocks:', error)
return []
}),
wbService.getStocksReportByOffices({
nmIds: nmIds.length > 0 ? nmIds : undefined, // Передаем ID твоих товаров
stockType: '' // все склады - покажем все данные
}).catch((error) => {
console.error('WB Warehouse: Error loading analytics data:', error)
return []
})
])
if (cards.length === 0) {
console.log('WB Warehouse: No cards found, cannot proceed with analytics')
setStocks([])
setWarehouses([])
return
}
console.log('WB Warehouse: Warehouses loaded:', warehousesData.length)
console.log('WB Warehouse: Basic stocks loaded:', stocksData.length)
console.log('WB Warehouse: Analytics data loaded:', rawAnalyticsData.length)
// Получаем данные по складам для каждого товара отдельно
console.log('WB Warehouse: Getting stocks analytics for each card separately...')
const analyticsResults = []
setWarehouses(warehousesData)
for (const nmId of nmIds) {
console.log(`WB Warehouse: Getting analytics for nmId: ${nmId}`)
try {
const result = await wbService.getStocksReportByOffices({
nmIds: [nmId], // Один товар за раз
stockType: '' // все склады
})
analyticsResults.push({ nmId, data: result })
console.log(`WB Warehouse: Got analytics for ${nmId}:`, result)
// Пауза между запросами чтобы не превысить лимиты API
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (error) {
console.error(`WB Warehouse: Error loading analytics for ${nmId}:`, error)
analyticsResults.push({ nmId, data: { data: { regions: [] } } })
}
}
console.log('WB Warehouse: Cards loaded:', cards.length)
console.log('WB Warehouse: Analytics data received')
// Analytics API создает записи с другой структурой - изучаем что пришло
console.log('WB Warehouse: Raw analytics data structure:', rawAnalyticsData)
console.log('WB Warehouse: Sample analytics item:', rawAnalyticsData[0])
// Объединяем карточки товаров с данными Analytics API
const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults)
console.log('WB Warehouse: Combined stocks:', combinedStocks.length)
// Отключаем общую аналитику - будем показывать детализацию по товарам в карточках
setAnalyticsData([])
// Объединяем основные данные со склады и данные Analytics API по складам WB
const combinedStocks = [...stocksData, ...rawAnalyticsData]
const processedStocks = processStocksData(combinedStocks, warehousesData, rawAnalyticsData)
setStocks(processedStocks)
// Используем объединенные данные
setStocks(combinedStocks)
// Извлекаем информацию о складах из данных Analytics API
const warehousesFromAnalytics = extractWarehousesFromStocks(combinedStocks)
setWarehouses(warehousesFromAnalytics)
// Обновляем статистику
updateStatistics(processedStocks, warehousesData)
updateStatistics(combinedStocks, warehousesFromAnalytics)
if (showToast) {
toast.success('Данные обновлены')
@ -167,102 +178,175 @@ export function WBWarehouseDashboard() {
}
}
// Обработка данных остатков с дополнением данными из Analytics API
const processStocksData = (stocksData: unknown[], warehousesData: WBWarehouse[], analyticsData: WBStock[] = []): WBStock[] => {
// Объединение карточек товаров с индивидуальными данными Analytics API
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
const stocksMap = new Map<number, WBStock>()
// Создаем карту данных Analytics API по складам для быстрого поиска
const analyticsMap = new Map<number, { toClientCount: number, fromClientCount: number }>()
analyticsData.forEach(item => {
item.stocks.forEach(stock => {
analyticsMap.set(stock.warehouseId, {
toClientCount: stock.inWayToClient,
fromClientCount: stock.inWayFromClient
console.log('WB Warehouse: Combining cards with individual analytics...')
console.log('WB Warehouse: Cards count:', cards.length)
console.log('WB Warehouse: Analytics results count:', analyticsResults.length)
// Создаем карту Analytics результатов по nmId
const analyticsMap = new Map()
analyticsResults.forEach(result => {
analyticsMap.set(result.nmId, result.data)
})
// Создаем записи для каждой карточки товара
cards.forEach(card => {
const stock: WBStock = {
nmId: card.nmID,
vendorCode: String(card.vendorCode || card.supplierVendorCode || ''),
title: String(card.title || card.object || `Товар ${card.nmID}`),
brand: String(card.brand || ''),
price: 0, // Цена будет из размеров, если есть
stocks: [], // Заполним из Analytics API
totalQuantity: 0,
totalReserved: 0,
photos: Array.isArray(card.photos) ? card.photos : [],
mediaFiles: Array.isArray(card.mediaFiles) ? card.mediaFiles : [],
characteristics: Array.isArray(card.characteristics) ? card.characteristics : [],
subjectName: String(card.subjectName || card.object || ''),
description: String(card.description || '')
}
// Берем цену из первого размера если есть
if (card.sizes && card.sizes.length > 0) {
stock.price = Number(card.sizes[0].price || card.sizes[0].discountedPrice) || 0
}
// Получаем данные Analytics для этого конкретного товара
const analyticsData = analyticsMap.get(card.nmID)
console.log(`WB Warehouse: Processing analytics for card ${card.nmID}:`, analyticsData)
if (analyticsData?.data?.regions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
analyticsData.data.regions.forEach((region: any) => {
if (region.offices && region.offices.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
region.offices.forEach((office: any) => {
console.log(`WB Warehouse: Adding office ${office.officeName} for card ${card.nmID}`)
console.log(`WB Warehouse: Office metrics:`, office.metrics)
stock.stocks.push({
warehouseId: office.officeID,
warehouseName: office.officeName,
quantity: office.metrics?.stockCount || 0,
quantityFull: office.metrics?.stockCount || 0,
inWayToClient: office.metrics?.toClientCount || 0,
inWayFromClient: office.metrics?.fromClientCount || 0
})
stock.totalQuantity += office.metrics?.stockCount || 0
stock.totalReserved += office.metrics?.toClientCount || 0
})
}
})
} else {
console.log(`WB Warehouse: No analytics data found for card ${card.nmID}`)
}
stocksMap.set(card.nmID, stock)
})
console.log('WB Warehouse: Final stocks after combining:', stocksMap.size)
return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity)
}
// Объединение карточек товаров с данными Analytics API (старая функция)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const combineCardsWithAnalytics = (cards: any[], analyticsResponse: any): WBStock[] => {
const stocksMap = new Map<number, WBStock>()
console.log('WB Warehouse: Combining cards with analytics...')
console.log('WB Warehouse: Cards count:', cards.length)
console.log('WB Warehouse: Analytics response:', analyticsResponse)
// Сначала создаем записи для всех карточек товаров
cards.forEach(card => {
const stock: WBStock = {
nmId: card.nmID,
vendorCode: String(card.vendorCode || card.supplierVendorCode || ''),
title: String(card.title || card.object || `Товар ${card.nmID}`),
brand: String(card.brand || ''),
price: 0, // Цена будет из размеров, если есть
stocks: [], // Заполним из Analytics API
totalQuantity: 0,
totalReserved: 0,
photos: Array.isArray(card.photos) ? card.photos : [],
mediaFiles: Array.isArray(card.mediaFiles) ? card.mediaFiles : [],
characteristics: Array.isArray(card.characteristics) ? card.characteristics : [],
subjectName: String(card.subjectName || card.object || ''),
description: String(card.description || '')
}
// Берем цену из первого размера если есть
if (card.sizes && card.sizes.length > 0) {
stock.price = Number(card.sizes[0].price || card.sizes[0].discountedPrice) || 0
}
stocksMap.set(card.nmID, stock)
})
console.log('WB Warehouse: Created stocks from cards:', stocksMap.size)
// Теперь дополняем данными из Analytics API
if (analyticsResponse?.data?.regions) {
console.log('WB Warehouse: Processing analytics regions:', analyticsResponse.data.regions.length)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
analyticsResponse.data.regions.forEach((region: any) => {
if (region.offices && region.offices.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
region.offices.forEach((office: any) => {
console.log(`WB Warehouse: Processing office ${office.officeName} (${office.officeID})`)
console.log('WB Warehouse: Office metrics:', office.metrics)
// Пока что добавляем данные склада ко всем товарам
// TODO: нужно понять как Analytics API связывает товары со складами
stocksMap.forEach((stock, nmId) => {
stock.stocks.push({
warehouseId: office.officeID,
warehouseName: office.officeName,
quantity: office.metrics?.stockCount || 0,
quantityFull: office.metrics?.stockCount || 0,
inWayToClient: office.metrics?.toClientCount || 0,
inWayFromClient: office.metrics?.fromClientCount || 0
})
stock.totalQuantity += office.metrics?.stockCount || 0
stock.totalReserved += office.metrics?.toClientCount || 0
})
})
}
})
}
console.log('WB Warehouse: Final stocks after combining:', stocksMap.size)
return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity)
}
// Извлечение информации о складах из данных Analytics API
const extractWarehousesFromStocks = (stocksData: WBStock[]): WBWarehouse[] => {
const warehousesMap = new Map<number, WBWarehouse>()
stocksData.forEach(stock => {
stock.stocks.forEach(stockInfo => {
if (!warehousesMap.has(stockInfo.warehouseId)) {
warehousesMap.set(stockInfo.warehouseId, {
id: stockInfo.warehouseId,
name: stockInfo.warehouseName,
cargoType: 1, // По умолчанию
deliveryType: 1 // По умолчанию
})
}
})
})
console.log('WB Warehouse: Analytics map created with', analyticsMap.size, 'warehouse entries')
stocksData.forEach((stockItem: unknown) => {
const stock = stockItem as Record<string, unknown>
const nmId = Number(stock.nmId) || 0
if (!stocksMap.has(nmId)) {
console.log(`WB Warehouse: Processing stock for nmId ${nmId}`)
console.log(`WB Warehouse: Stock item:`, stock)
stocksMap.set(nmId, {
nmId,
vendorCode: String(stock.vendorCode || stock.supplierArticle || ''),
title: String(stock.title || stock.subject || `Товар ${nmId}`),
brand: String(stock.brand || ''),
price: Number(stock.price || stock.Price) || 0,
stocks: [],
totalQuantity: 0,
totalReserved: 0,
photos: Array.isArray(stock.photos) ? stock.photos as Array<{big?: string; c246x328?: string; c516x688?: string; square?: string; tm?: string}> : [],
mediaFiles: Array.isArray(stock.mediaFiles) ? stock.mediaFiles as string[] : [],
characteristics: Array.isArray(stock.characteristics) ? stock.characteristics as Array<{id: number; name: string; value: string[] | string}> : [],
subjectName: String(stock.subjectName || stock.subject || ''),
description: String(stock.description || '')
})
}
const item = stocksMap.get(nmId)!
// Для Analytics API данных берем warehouseId из первого stock в массиве stocks
let warehouseId = Number(stock.warehouseId || stock.warehouse) || 0
let warehouseName = String(stock.warehouseName || '')
// Если это данные Analytics API (есть массив stocks)
if (Array.isArray(stock.stocks) && stock.stocks.length > 0) {
const firstStock = stock.stocks[0]
warehouseId = Number(firstStock.warehouseId) || 0
warehouseName = String(firstStock.warehouseName || `Склад ${warehouseId}`)
console.log(`WB Warehouse: Analytics stock - warehouseId: ${warehouseId}, name: ${warehouseName}`)
} else {
// Обычные данные
warehouseName = warehouseName || warehousesData.find(w => w.id === warehouseId)?.name || `Склад ${warehouseId}`
}
let quantity = Number(stock.quantity) || 0
let quantityFull = Number(stock.quantityFull) || 0
let inWayToClient = 0
let inWayFromClient = 0
// Если это данные Analytics API
if (Array.isArray(stock.stocks) && stock.stocks.length > 0) {
const firstStock = stock.stocks[0]
quantity = Number(firstStock.quantity) || 0
quantityFull = Number(firstStock.quantityFull) || 0
inWayToClient = Number(firstStock.inWayToClient) || 0
inWayFromClient = Number(firstStock.inWayFromClient) || 0
} else {
// Обычные данные - используем Analytics API если доступны
const analyticsInfo = analyticsMap.get(warehouseId)
inWayToClient = analyticsInfo?.toClientCount ?? (Number(stock.inWayToClient) || 0)
inWayFromClient = analyticsInfo?.fromClientCount ?? (Number(stock.inWayFromClient) || 0)
}
const hasAnalytics = Array.isArray(stock.stocks) && stock.stocks.length > 0
console.log(`WB Warehouse: Warehouse ${warehouseId} - Analytics: ${hasAnalytics ? 'YES' : 'NO'}, toClient: ${inWayToClient}, fromClient: ${inWayFromClient}`)
const warehouseStock = {
warehouseId,
warehouseName,
quantity,
quantityFull,
inWayToClient,
inWayFromClient
}
item.stocks.push(warehouseStock)
item.totalQuantity += quantity
item.totalReserved += inWayToClient
})
return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity)
return Array.from(warehousesMap.values())
}
// Обновление статистики
@ -271,6 +355,12 @@ export function WBWarehouseDashboard() {
setTotalStocks(stocksData.reduce((sum, item) => sum + item.totalQuantity, 0))
setTotalReserved(stocksData.reduce((sum, item) => sum + item.totalReserved, 0))
// Считаем общее количество товаров в пути от клиента
const totalFromClientCount = stocksData.reduce((sum, item) =>
sum + item.stocks.reduce((stockSum, stock) => stockSum + stock.inWayFromClient, 0), 0
)
setTotalFromClient(totalFromClientCount)
const warehousesWithStock = new Set(
stocksData.flatMap(item => item.stocks.map(s => s.warehouseId))
)
@ -306,74 +396,88 @@ export function WBWarehouseDashboard() {
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
<div className="h-full w-full flex flex-col">
{/* Заголовок */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Склад Wildberries</h1>
{/* Результирующие вкладки */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 mb-4">
{/* Товаров */}
<div className="group relative overflow-hidden rounded-xl bg-gradient-to-br from-blue-500/20 to-blue-600/10 border border-blue-500/20 p-3 hover:border-blue-400/40 transition-all duration-300 hover:scale-105">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Package className="h-4 w-4 text-blue-400" />
<span className="text-blue-300 text-xs font-medium uppercase tracking-wide">Товаров</span>
</div>
<div className="text-xl font-bold text-white">
{loading ? <div className="h-5 w-8 bg-white/10 rounded animate-pulse" /> : totalProducts.toLocaleString()}
</div>
</div>
</div>
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/0 via-blue-500/5 to-blue-500/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div>
<div className="flex gap-3">
<Button
onClick={() => loadWarehouseData(true)}
disabled={refreshing}
className="glass-button text-white"
>
<RefreshCw className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
Обновить
</Button>
{/* Общий остаток */}
<div className="group relative overflow-hidden rounded-xl bg-gradient-to-br from-green-500/20 to-green-600/10 border border-green-500/20 p-3 hover:border-green-400/40 transition-all duration-300 hover:scale-105">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Warehouse className="h-4 w-4 text-green-400" />
<span className="text-green-300 text-xs font-medium uppercase tracking-wide">Остаток</span>
</div>
<div className="text-xl font-bold text-white">
{loading ? <div className="h-5 w-8 bg-white/10 rounded animate-pulse" /> : totalStocks.toLocaleString()}
</div>
</div>
</div>
<div className="absolute inset-0 bg-gradient-to-r from-green-500/0 via-green-500/5 to-green-500/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div>
</div>
{/* Статистика */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="glass-card border-white/10 p-4">
{/* К клиенту */}
<div className="group relative overflow-hidden rounded-xl bg-gradient-to-br from-orange-500/20 to-orange-600/10 border border-orange-500/20 p-3 hover:border-orange-400/40 transition-all duration-300 hover:scale-105">
<div className="flex items-center justify-between">
<div>
<p className="text-white/60 text-sm">Товаров</p>
<div className="text-2xl font-bold text-white">
{loading ? <Skeleton className="h-6 w-16 bg-white/10" /> : totalProducts.toLocaleString()}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<TrendingUp className="h-4 w-4 text-orange-400" />
<span className="text-orange-300 text-xs font-medium uppercase tracking-wide">К клиенту</span>
</div>
<div className="text-xl font-bold text-white">
{loading ? <div className="h-5 w-8 bg-white/10 rounded animate-pulse" /> : totalReserved.toLocaleString()}
</div>
</div>
<Package className="h-8 w-8 text-blue-400" />
</div>
</Card>
<div className="absolute inset-0 bg-gradient-to-r from-orange-500/0 via-orange-500/5 to-orange-500/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div>
<Card className="glass-card border-white/10 p-4">
{/* От клиента */}
<div className="group relative overflow-hidden rounded-xl bg-gradient-to-br from-red-500/20 to-red-600/10 border border-red-500/20 p-3 hover:border-red-400/40 transition-all duration-300 hover:scale-105">
<div className="flex items-center justify-between">
<div>
<p className="text-white/60 text-sm">Общий остаток</p>
<div className="text-2xl font-bold text-white">
{loading ? <Skeleton className="h-6 w-16 bg-white/10" /> : totalStocks.toLocaleString()}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<TrendingDown className="h-4 w-4 text-red-400" />
<span className="text-red-300 text-xs font-medium uppercase tracking-wide">От клиента</span>
</div>
<div className="text-xl font-bold text-white">
{loading ? <div className="h-5 w-8 bg-white/10 rounded animate-pulse" /> : totalFromClient.toLocaleString()}
</div>
</div>
<Warehouse className="h-8 w-8 text-green-400" />
</div>
</Card>
<div className="absolute inset-0 bg-gradient-to-r from-red-500/0 via-red-500/5 to-red-500/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div>
<Card className="glass-card border-white/10 p-4">
{/* Складов */}
<div className="group relative overflow-hidden rounded-xl bg-gradient-to-br from-purple-500/20 to-purple-600/10 border border-purple-500/20 p-3 hover:border-purple-400/40 transition-all duration-300 hover:scale-105">
<div className="flex items-center justify-between">
<div>
<p className="text-white/60 text-sm">В пути к клиенту</p>
<div className="text-2xl font-bold text-white">
{loading ? <Skeleton className="h-6 w-16 bg-white/10" /> : totalReserved.toLocaleString()}
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<MapPin className="h-4 w-4 text-purple-400" />
<span className="text-purple-300 text-xs font-medium uppercase tracking-wide">Складов</span>
</div>
<div className="text-xl font-bold text-white">
{loading ? <div className="h-5 w-8 bg-white/10 rounded animate-pulse" /> : activeWarehouses}
</div>
</div>
<TrendingUp className="h-8 w-8 text-orange-400" />
</div>
</Card>
<Card className="glass-card border-white/10 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-white/60 text-sm">Активных складов</p>
<div className="text-2xl font-bold text-white">
{loading ? <Skeleton className="h-6 w-16 bg-white/10" /> : activeWarehouses}
</div>
</div>
<MapPin className="h-8 w-8 text-purple-400" />
</div>
</Card>
<div className="absolute inset-0 bg-gradient-to-r from-purple-500/0 via-purple-500/5 to-purple-500/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div>
</div>
{/* Аналитика по складам WB */}
@ -404,45 +508,99 @@ export function WBWarehouseDashboard() {
)}
{/* Фильтры */}
<Card className="glass-card border-white/10 p-4 mb-6">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск по названию, артикулу или бренду..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="glass-input text-white placeholder:text-white/40 pl-10"
/>
</div>
</div>
<div className="w-full sm:w-64">
<select
value={selectedWarehouse}
onChange={(e) => setSelectedWarehouse(e.target.value)}
className="w-full h-10 px-3 rounded-lg bg-white/5 border border-white/10 text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
>
<option value="all">Все склады</option>
{warehouses.map(warehouse => (
<option key={warehouse.id} value={warehouse.id.toString()}>
{warehouse.name}
</option>
))}
</select>
<div className="flex flex-col sm:flex-row gap-3 mb-4">
<div className="flex-1 relative group">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40 group-focus-within:text-blue-400 transition-colors duration-200" />
<input
type="text"
placeholder="Поиск товаров..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full h-12 pl-12 pr-4 rounded-xl bg-white/5 border border-white/10 text-white placeholder:text-white/40 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/30 transition-all duration-200 hover:bg-white/10"
/>
</div>
<div className="relative group">
<Filter className="absolute left-4 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40 group-focus-within:text-purple-400 transition-colors duration-200" />
<select
value={selectedWarehouse}
onChange={(e) => setSelectedWarehouse(e.target.value)}
className="w-full sm:w-48 h-12 pl-12 pr-8 rounded-xl bg-white/5 border border-white/10 text-white text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/30 transition-all duration-200 hover:bg-white/10 appearance-none cursor-pointer"
>
<option value="all">Все склады</option>
{warehouses.map(warehouse => (
<option key={warehouse.id} value={warehouse.id.toString()} className="bg-gray-800">
{warehouse.name}
</option>
))}
</select>
<div className="absolute right-4 top-1/2 transform -translate-y-1/2 pointer-events-none">
<svg className="h-4 w-4 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</Card>
</div>
{/* Список товаров */}
<div className="flex-1 overflow-hidden">
{loading ? (
<div className="grid grid-cols-1 gap-4">
{[...Array(5)].map((_, i) => (
<Card key={i} className="glass-card border-white/10 p-4">
<Skeleton className="h-20 w-full bg-white/10" />
</Card>
))}
<div className="overflow-y-auto pr-2 max-h-full">
{/* Заголовки таблицы */}
<div className="sticky top-0 bg-black/50 backdrop-blur-sm border-b border-white/10 p-4 mb-2">
<div className="grid grid-cols-12 gap-4 text-xs font-medium text-white/60 uppercase tracking-wide">
<div className="col-span-3">Товар</div>
<div className="col-span-1 text-center">Остаток</div>
<div className="col-span-1 text-center">К клиенту</div>
<div className="col-span-1 text-center">От клиента</div>
<div className="col-span-1 text-center">Складов</div>
<div className="col-span-5">Характеристики</div>
</div>
</div>
{/* Skeleton строки */}
<div className="space-y-1">
{[...Array(8)].map((_, i) => (
<div key={i} className="grid grid-cols-12 gap-4 p-4 rounded-xl bg-white/5 border border-white/10">
<div className="col-span-3 flex items-center gap-3">
<div className="w-12 h-12 rounded-lg bg-white/10 animate-pulse" />
<div className="flex-1 space-y-2">
<div className="h-3 bg-white/10 rounded animate-pulse" style={{ width: '70%' }} />
<div className="h-2 bg-white/10 rounded animate-pulse" style={{ width: '50%' }} />
<div className="h-2 bg-white/10 rounded animate-pulse" style={{ width: '40%' }} />
</div>
</div>
<div className="col-span-1 flex items-center justify-center">
<div className="text-center space-y-1">
<div className="h-5 w-8 bg-white/10 rounded animate-pulse mx-auto" />
<div className="h-3 w-12 bg-white/10 rounded animate-pulse mx-auto" />
</div>
</div>
<div className="col-span-1 flex items-center justify-center">
<div className="text-center space-y-1">
<div className="h-5 w-6 bg-white/10 rounded animate-pulse mx-auto" />
<div className="h-3 w-8 bg-white/10 rounded animate-pulse mx-auto" />
</div>
</div>
<div className="col-span-1 flex items-center justify-center">
<div className="text-center space-y-1">
<div className="h-5 w-6 bg-white/10 rounded animate-pulse mx-auto" />
<div className="h-3 w-10 bg-white/10 rounded animate-pulse mx-auto" />
</div>
</div>
<div className="col-span-1 flex items-center justify-center">
<div className="text-center space-y-1">
<div className="h-5 w-4 bg-white/10 rounded animate-pulse mx-auto" />
<div className="h-3 w-12 bg-white/10 rounded animate-pulse mx-auto" />
</div>
</div>
<div className="col-span-5 space-y-1">
<div className="h-2 bg-white/10 rounded animate-pulse" style={{ width: '90%' }} />
<div className="h-2 bg-white/10 rounded animate-pulse" style={{ width: '75%' }} />
<div className="h-2 bg-white/10 rounded animate-pulse" style={{ width: '65%' }} />
</div>
</div>
))}
</div>
</div>
) : !hasWBApiKey ? (
<Card className="glass-card border-white/10 p-8 text-center">
@ -469,10 +627,25 @@ export function WBWarehouseDashboard() {
</p>
</Card>
) : (
<div className="space-y-4 overflow-y-auto pr-2 max-h-full">
{filteredStocks.map((item, index) => (
<StockCard key={`${item.nmId}-${index}`} item={item} />
))}
<div className="overflow-y-auto pr-2 max-h-full">
{/* Заголовки таблицы */}
<div className="sticky top-0 bg-black/50 backdrop-blur-sm border-b border-white/10 p-4 mb-2">
<div className="grid grid-cols-12 gap-4 text-xs font-medium text-white/60 uppercase tracking-wide">
<div className="col-span-3">Товар</div>
<div className="col-span-1 text-center">Остаток</div>
<div className="col-span-1 text-center">К клиенту</div>
<div className="col-span-1 text-center">От клиента</div>
<div className="col-span-1 text-center">Складов</div>
<div className="col-span-5">Характеристики</div>
</div>
</div>
{/* Таблица товаров */}
<div className="space-y-1">
{filteredStocks.map((item, index) => (
<StockTableRow key={`${item.nmId}-${index}`} item={item} />
))}
</div>
</div>
)}
</div>
@ -482,218 +655,381 @@ export function WBWarehouseDashboard() {
)
}
// Компонент карточки товара
function StockCard({ item }: { item: WBStock }) {
// Табличная строка товара
function StockTableRow({ item }: { item: WBStock }) {
// Получение изображений карточки через WildberriesService
const getCardImages = (item: WBStock): string[] => {
console.log(`WB Warehouse: Getting images for card ${item.nmId}`)
console.log(`WB Warehouse: Photos:`, item.photos)
console.log(`WB Warehouse: MediaFiles:`, item.mediaFiles)
// Если есть photos в формате WB API
if (item.photos && item.photos.length > 0) {
const urls = item.photos
.map(photo => photo.c246x328 || photo.c516x688 || photo.big)
.filter((url): url is string => Boolean(url))
console.log(`WB Warehouse: URLs from photos:`, urls)
return urls
}
// Проверяем mediaFiles (как в создании поставки)
if (item.mediaFiles && item.mediaFiles.length > 0) {
console.log(`WB Warehouse: URLs from mediaFiles:`, item.mediaFiles)
return item.mediaFiles
}
// Fallback - генерируем URL по стандартной схеме WB
const vol = Math.floor(item.nmId / 100000)
const part = Math.floor(item.nmId / 1000)
const fallbackUrl = `https://basket-${String(vol).padStart(2, '0')}.wbbasket.ru/vol${vol}/part${part}/${item.nmId}/images/c246x328/1.webp`
console.log(`WB Warehouse: Using fallback URL:`, fallbackUrl)
return [fallbackUrl]
}
const getStockStatus = (quantity: number) => {
if (quantity === 0) return { color: 'bg-red-500/20 text-red-400 border-red-500/30', label: 'Нет в наличии' }
if (quantity < 10) return { color: 'bg-orange-500/20 text-orange-400 border-orange-500/30', label: 'Мало' }
return { color: 'bg-green-500/20 text-green-400 border-green-500/30', label: 'В наличии' }
if (quantity === 0) return {
color: 'text-red-400',
bgColor: 'bg-red-500/10',
label: 'Нет в наличии'
}
if (quantity < 10) return {
color: 'text-orange-400',
bgColor: 'bg-orange-500/10',
label: 'Мало'
}
return {
color: 'text-green-400',
bgColor: 'bg-green-500/10',
label: 'В наличии'
}
}
const stockStatus = getStockStatus(item.totalQuantity)
// Получаем изображения из данных карточки WB
const images = getCardImages(item)
const mainImage = images[0] || null
// Отбираем ключевые характеристики для отображения в таблице
const keyCharacteristics = item.characteristics?.slice(0, 3) || []
return (
<Card className="glass-card border-white/10 overflow-hidden hover:border-white/20 transition-all duration-300">
<div className="p-4 space-y-4">
<div className="flex items-start gap-4">
{/* Изображение товара */}
<div className="w-20 h-20 rounded-lg overflow-hidden bg-white/5 flex-shrink-0 relative group">
<div className="group">
{/* Основная строка товара */}
<div
className="grid grid-cols-12 gap-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 transition-all duration-300"
>
{/* Товар (3 колонки) */}
<div className="col-span-3 flex items-center gap-3">
<div className="w-12 h-12 rounded-lg overflow-hidden bg-white/10 flex-shrink-0">
{mainImage ? (
<img
src={mainImage}
alt={item.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-8 w-8 text-white/40" />
<Package className="h-5 w-5 text-white/40" />
</div>
)}
{/* Индикатор WB */}
<div className="absolute top-1 right-1">
<Badge className="bg-blue-500/90 text-white border-0 text-xs px-1.5 py-0.5">
WB
</Badge>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs text-blue-300 bg-blue-500/20 px-2 py-0.5 rounded">
{item.brand || 'Без бренда'}
</span>
<span className="text-white/40 text-xs">#{item.nmId}</span>
</div>
<h3 className="text-white text-sm font-medium line-clamp-1 mb-1">
{item.title}
</h3>
<div className="text-white/60 text-xs">
{item.vendorCode}
</div>
</div>
</div>
{/* Информация о товаре */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
{/* Заголовок и бренд */}
<div className="flex items-center gap-2 mb-2">
<Badge className="bg-blue-500/20 text-blue-300 border-blue-500/30 text-xs font-medium">
{item.brand || 'Без бренда'}
</Badge>
<span className="text-white/40 text-xs">{item.nmId}</span>
</div>
<h3 className="text-white font-semibold text-sm mb-2 line-clamp-2 leading-tight">
{item.title}
</h3>
{/* Артикул */}
<div className="text-white/60 text-xs mb-2">
Артикул: <span className="text-white/80 font-mono">{item.vendorCode}</span>
</div>
</div>
<Badge className={`${stockStatus.color} border text-xs flex-shrink-0`}>
{stockStatus.label}
</Badge>
{/* Остаток */}
<div className="col-span-1 flex items-center justify-center">
<div className="text-center">
<div className={`text-lg font-bold ${stockStatus.color}`}>
{item.totalQuantity.toLocaleString()}
</div>
{/* Общая статистика */}
<div className="grid grid-cols-2 gap-3 p-3 bg-white/5 rounded-lg">
<div className="text-center">
<p className="text-white text-lg font-bold">{item.totalQuantity.toLocaleString()}</p>
<p className="text-white/60 text-xs">Доступно</p>
</div>
<div className="text-center">
<p className="text-purple-400 text-lg font-bold">{item.stocks.length}</p>
<p className="text-white/60 text-xs">Складов</p>
</div>
<div className={`text-xs px-2 py-0.5 rounded ${stockStatus.bgColor} ${stockStatus.color}`}>
{stockStatus.label}
</div>
</div>
</div>
{/* Статистика по движению товаров */}
{(item.stocks.some(s => s.inWayToClient > 0) || item.stocks.some(s => s.inWayFromClient > 0)) && (
<div className="grid grid-cols-2 gap-3 p-3 bg-blue-500/10 rounded-lg border border-blue-500/20">
<div className="text-center">
<p className="text-blue-400 text-lg font-bold">
{item.stocks.reduce((sum, s) => sum + s.inWayToClient, 0).toLocaleString()}
</p>
<p className="text-white/60 text-xs">К клиенту</p>
</div>
<div className="text-center">
<p className="text-orange-400 text-lg font-bold">
{item.stocks.reduce((sum, s) => sum + s.inWayFromClient, 0).toLocaleString()}
</p>
<p className="text-white/60 text-xs">От клиента</p>
</div>
{/* К клиенту */}
<div className="col-span-1 flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold text-orange-400">
{item.stocks.reduce((sum, s) => sum + s.inWayToClient, 0)}
</div>
<div className="text-xs text-white/60">в пути</div>
</div>
</div>
{/* От клиента */}
<div className="col-span-1 flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold text-red-400">
{item.stocks.reduce((sum, s) => sum + s.inWayFromClient, 0)}
</div>
<div className="text-xs text-white/60">возвраты</div>
</div>
</div>
{/* Складов */}
<div className="col-span-1 flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold text-purple-400">
{item.stocks.length}
</div>
<div className="text-xs text-white/60">активных</div>
</div>
</div>
{/* Характеристики (5 колонок) */}
<div className="col-span-5 flex items-center">
<div className="space-y-1 w-full">
{keyCharacteristics.map((char, index) => (
<div key={index} className="flex justify-between text-xs">
<span className="text-white/60 truncate w-1/2">{char.name}:</span>
<span className="text-white truncate w-1/2 text-right">
{Array.isArray(char.value) ? char.value.join(', ') : String(char.value)}
</span>
</div>
))}
{item.subjectName && (
<div className="flex justify-between text-xs">
<span className="text-white/60">Категория:</span>
<span className="text-blue-300 truncate text-right">{item.subjectName}</span>
</div>
)}
</div>
</div>
</div>
{/* Остатки по складам */}
<div>
<h4 className="text-white font-medium text-sm mb-3">Остатки по складам:</h4>
<div className="space-y-2">
{item.stocks.map((stock, stockIndex) => (
<div key={`${stock.warehouseId}-${stockIndex}`} className="flex items-center justify-between py-2 px-3 rounded-lg bg-white/5">
<div className="flex-1">
<p className="text-white text-sm font-medium">{stock.warehouseName}</p>
<p className="text-white/60 text-xs">ID: {stock.warehouseId}</p>
{/* Города в модулях */}
<div className="grid grid-cols-12 gap-4 p-3 bg-white/[0.02] border-l-2 border-blue-400/30">
<div className="col-span-12 flex flex-wrap gap-3">
{item.stocks.map((stock, stockIndex) => (
<div
key={`${stock.warehouseId}-${stockIndex}`}
className="bg-white/10 rounded-lg px-3 py-2 border border-white/20 hover:border-white/30 transition-colors"
>
{/* Название города */}
<div className="text-white text-sm font-medium mb-1">
{stock.warehouseName}
</div>
{/* Цифры */}
<div className="flex items-center gap-3 text-xs">
<div className="text-center">
<div className={`font-bold ${stock.quantity > 0 ? 'text-green-400' : 'text-white/30'}`}>
{stock.quantity}
</div>
<div className="text-white/50">остаток</div>
</div>
<div className="grid grid-cols-3 gap-3 text-sm">
{(stock.inWayToClient > 0 || stock.inWayFromClient > 0) && (
<>
<div className="w-px h-6 bg-white/20" />
{stock.inWayToClient > 0 && (
<div className="text-center">
<div className="font-bold text-orange-400">{stock.inWayToClient}</div>
<div className="text-white/50">к клиенту</div>
</div>
)}
{stock.inWayFromClient > 0 && (
<div className="text-center">
<div className="font-bold text-red-400">{stock.inWayFromClient}</div>
<div className="text-white/50">от клиента</div>
</div>
)}
</>
)}
</div>
</div>
))}
</div>
</div>
</div>
)
}
// Супер современная карточка товара (СТАРАЯ ВЕРСИЯ - НЕ ИСПОЛЬЗУЕТСЯ)
function StockCard({ item }: { item: WBStock }) {
// Получение изображений карточки через WildberriesService
const getCardImages = (item: WBStock): string[] => {
if (item.photos && item.photos.length > 0) {
const urls = item.photos
.map(photo => photo.c246x328 || photo.c516x688 || photo.big)
.filter((url): url is string => Boolean(url))
return urls
}
if (item.mediaFiles && item.mediaFiles.length > 0) {
return item.mediaFiles
}
const vol = Math.floor(item.nmId / 100000)
const part = Math.floor(item.nmId / 1000)
const fallbackUrl = `https://basket-${String(vol).padStart(2, '0')}.wbbasket.ru/vol${vol}/part${part}/${item.nmId}/images/c246x328/1.webp`
return [fallbackUrl]
}
const getStockStatus = (quantity: number) => {
if (quantity === 0) return {
color: 'from-red-500/20 to-red-600/5 border-red-500/30',
textColor: 'text-red-400',
label: 'Нет в наличии',
icon: '❌'
}
if (quantity < 10) return {
color: 'from-orange-500/20 to-orange-600/5 border-orange-500/30',
textColor: 'text-orange-400',
label: 'Мало',
icon: '⚠️'
}
return {
color: 'from-green-500/20 to-green-600/5 border-green-500/30',
textColor: 'text-green-400',
label: 'В наличии',
icon: '✅'
}
}
const stockStatus = getStockStatus(item.totalQuantity)
const images = getCardImages(item)
const mainImage = images[0] || null
return (
<div className="group relative overflow-hidden rounded-2xl bg-gradient-to-br from-white/5 to-white/[0.02] border border-white/10 hover:border-white/20 transition-all duration-500 hover:scale-[1.02] hover:shadow-2xl hover:shadow-blue-500/10">
{/* Градиентный фон при hover */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/0 via-purple-500/0 to-pink-500/0 group-hover:from-blue-500/5 group-hover:via-purple-500/5 group-hover:to-pink-500/5 transition-all duration-700" />
<div className="relative p-5">
{/* Хедер карточки */}
<div className="flex items-start gap-4 mb-4">
{/* Изображение товара */}
<div className="relative">
<div className="w-16 h-16 rounded-xl overflow-hidden bg-gradient-to-br from-white/10 to-white/5 flex-shrink-0 relative group/image">
{mainImage ? (
<img
src={mainImage}
alt={item.title}
className="w-full h-full object-cover group-hover/image:scale-110 transition-transform duration-500"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-6 w-6 text-white/40" />
</div>
)}
</div>
{/* WB Badge */}
<div className="absolute -top-1 -right-1">
<div className="bg-gradient-to-r from-blue-500 to-blue-600 text-white text-xs px-2 py-0.5 rounded-full font-bold shadow-lg">
WB
</div>
</div>
</div>
{/* Инфо товара */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs text-blue-300 font-medium bg-blue-500/20 px-2 py-0.5 rounded-md">
{item.brand || 'Без бренда'}
</span>
<span className="text-white/40 text-xs">#{item.nmId}</span>
</div>
<h3 className="text-white font-semibold text-sm leading-tight line-clamp-2">
{item.title}
</h3>
</div>
{/* Статус */}
<div className={`ml-2 px-2 py-1 rounded-lg bg-gradient-to-r ${stockStatus.color} border text-xs font-medium ${stockStatus.textColor} flex items-center gap-1`}>
<span>{stockStatus.icon}</span>
{stockStatus.label}
</div>
</div>
<div className="text-white/60 text-xs">
Артикул: <span className="text-white/80 font-mono">{item.vendorCode}</span>
</div>
</div>
</div>
{/* Компактная статистика */}
<div className="grid grid-cols-4 gap-2 mb-4">
<div className="text-center p-2 rounded-lg bg-gradient-to-br from-green-500/10 to-green-600/5 border border-green-500/20">
<div className="text-green-400 text-lg font-bold">{item.totalQuantity.toLocaleString()}</div>
<div className="text-green-300/60 text-xs">Остаток</div>
</div>
<div className="text-center p-2 rounded-lg bg-gradient-to-br from-purple-500/10 to-purple-600/5 border border-purple-500/20">
<div className="text-purple-400 text-lg font-bold">{item.stocks.length}</div>
<div className="text-purple-300/60 text-xs">Складов</div>
</div>
<div className="text-center p-2 rounded-lg bg-gradient-to-br from-orange-500/10 to-orange-600/5 border border-orange-500/20">
<div className="text-orange-400 text-lg font-bold">
{item.stocks.reduce((sum, s) => sum + s.inWayToClient, 0)}
</div>
<div className="text-orange-300/60 text-xs">К клиенту</div>
</div>
<div className="text-center p-2 rounded-lg bg-gradient-to-br from-red-500/10 to-red-600/5 border border-red-500/20">
<div className="text-red-400 text-lg font-bold">
{item.stocks.reduce((sum, s) => sum + s.inWayFromClient, 0)}
</div>
<div className="text-red-300/60 text-xs">От клиента</div>
</div>
</div>
{/* Склады - компактно */}
<div className="space-y-2">
<h4 className="text-white/80 font-medium text-xs uppercase tracking-wide flex items-center gap-2">
<Warehouse className="h-3 w-3" />
Склады
</h4>
<div className="space-y-1.5">
{item.stocks.slice(0, 3).map((stock, stockIndex) => (
<div key={`${stock.warehouseId}-${stockIndex}`} className="flex items-center justify-between p-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors duration-200">
<div className="flex-1">
<div className="text-white text-xs font-medium truncate">{stock.warehouseName}</div>
<div className="text-white/50 text-xs">ID: {stock.warehouseId}</div>
</div>
<div className="flex items-center gap-3 text-xs">
<div className="text-center">
<p className="text-green-400 font-bold text-lg">{stock.quantity}</p>
<p className="text-white/60 text-xs">Доступно</p>
<div className={`font-bold ${stock.quantity > 0 ? 'text-green-400' : 'text-white/30'}`}>
{stock.quantity}
</div>
</div>
<div className="text-center">
<p className={`font-bold text-lg ${stock.inWayToClient > 0 ? 'text-blue-400' : 'text-white/30'}`}>
<div className={`font-bold ${stock.inWayToClient > 0 ? 'text-orange-400' : 'text-white/30'}`}>
{stock.inWayToClient}
</p>
<p className="text-white/60 text-xs">К клиенту</p>
</div>
</div>
<div className="text-center">
<p className={`font-bold text-lg ${stock.inWayFromClient > 0 ? 'text-orange-400' : 'text-white/30'}`}>
<div className={`font-bold ${stock.inWayFromClient > 0 ? 'text-red-400' : 'text-white/30'}`}>
{stock.inWayFromClient}
</p>
<p className="text-white/60 text-xs">От клиента</p>
</div>
</div>
</div>
</div>
))}
{item.stocks.length > 3 && (
<div className="text-center text-white/40 text-xs py-1">
+{item.stocks.length - 3} ещё складов
</div>
)}
</div>
</div>
{/* Основная информация о товаре */}
{(item.subjectName || item.description) && (
<div>
<h4 className="text-white font-medium text-sm mb-3">Информация о товаре:</h4>
<div className="space-y-2 p-3 rounded-lg bg-white/5">
{item.subjectName && (
<div className="flex justify-between items-center">
<span className="text-white/60 text-sm">Категория:</span>
<span className="text-white text-sm font-medium">{item.subjectName}</span>
</div>
)}
{item.description && (
<div>
<span className="text-white/60 text-sm block mb-1">Описание:</span>
<p className="text-white text-sm leading-relaxed">{item.description}</p>
</div>
)}
</div>
</div>
)}
{/* Характеристики товара */}
{item.characteristics && item.characteristics.length > 0 && (
<div>
<h4 className="text-white font-medium text-sm mb-3">Характеристики:</h4>
<div className="space-y-1">
{item.characteristics.map((characteristic, charIndex) => (
<div key={`${characteristic.id}-${charIndex}`} className="flex justify-between items-start py-2 px-3 rounded-lg bg-white/5">
<span className="text-white/60 text-sm font-medium min-w-[100px]">
{characteristic.name}:
</span>
<div className="flex-1 text-right">
{Array.isArray(characteristic.value) ? (
characteristic.value.map((val, valIndex) => (
<span key={valIndex} className="text-white text-sm">
{val}
{valIndex < characteristic.value.length - 1 && ', '}
</span>
))
) : (
<span className="text-white text-sm">
{String(characteristic.value)}
</span>
)}
</div>
</div>
))}
{/* Категория */}
{item.subjectName && (
<div className="mt-3 pt-3 border-t border-white/10">
<div className="text-white/60 text-xs">
<span className="text-white/40">Категория:</span> {item.subjectName}
</div>
</div>
)}
</div>
</Card>
</div>
)
}

View File

@ -1168,7 +1168,7 @@ class WildberriesService {
dateFrom?: string
dateTo?: string
stockType?: '' | 'wb' | 'mp'
} = {}): Promise<WBStock[]> {
} = {}): Promise<StocksReportOfficesResponse> {
try {
console.log('WB Analytics API: Getting stocks report by offices...')
@ -1210,137 +1210,53 @@ class WildberriesService {
})
console.log('WB Analytics API: Response:', JSON.stringify(response, null, 2))
// Детальный анализ структуры ответа
console.log('\n=== ДЕТАЛЬНЫЙ АНАЛИЗ ОТВЕТА API ===')
if (response.data) {
console.log('✅ response.data существует')
if (response.data.regions) {
console.log('✅ response.data.regions существует, длина:', response.data.regions.length)
response.data.regions.forEach((region, regionIndex) => {
console.log(`\n📍 РЕГИОН ${regionIndex + 1}:`)
console.log(' - regionName:', region.regionName)
console.log(' - metrics:', region.metrics)
console.log(' - offices.length:', region.offices?.length || 0)
if (region.offices && region.offices.length > 0) {
region.offices.forEach((office, officeIndex) => {
console.log(`\n 🏢 СКЛАД ${officeIndex + 1}:`)
console.log(' - officeID:', office.officeID)
console.log(' - officeName:', office.officeName)
console.log(' - metrics:', office.metrics)
// Проверяем наличие метрик
if (office.metrics) {
console.log(' - stockCount:', office.metrics.stockCount || 0)
console.log(' - toClientCount:', office.metrics.toClientCount || 0)
console.log(' - fromClientCount:', office.metrics.fromClientCount || 0)
}
})
} else {
console.log(' ⚠️ Нет складов в этом регионе')
}
})
} else {
console.log('❌ response.data.regions отсутствует')
}
} else {
console.log('❌ response.data отсутствует')
}
console.log('=== КОНЕЦ АНАЛИЗА ===\n')
console.log('WB Analytics API: Processing response data...')
// Преобразуем данные Analytics API в формат WBStock
const stocks: WBStock[] = []
if (response.data?.regions) {
console.log(`WB Analytics API: Found ${response.data.regions.length} regions`)
// Получаем карточки товаров и остатки для сопоставления
console.log('WB Analytics API: Loading cards and current stocks for matching...')
const [cards, currentStocks] = await Promise.all([
WildberriesService.getAllCards(this.apiKey).catch(() => []),
this.getStocks().catch(() => [])
])
console.log(`WB Analytics API: Loaded ${cards.length} cards and ${currentStocks.length} stock records`)
const cardsMap = new Map(cards.map((card: WildberriesCard) => [card.nmID, card]))
// Создаем карту остатков по складам из текущих данных
const stocksByWarehouse = new Map<number, Record<string, unknown>[]>()
const typedCurrentStocks = currentStocks as Record<string, unknown>[]
typedCurrentStocks.forEach((stock: Record<string, unknown>) => {
const warehouseId = Number(stock.warehouseId || stock.warehouse) || 0
if (!stocksByWarehouse.has(warehouseId)) {
stocksByWarehouse.set(warehouseId, [])
}
stocksByWarehouse.get(warehouseId)!.push(stock)
})
response.data.regions.forEach(region => {
console.log(`WB Analytics API: Processing region "${region.regionName}" with ${region.offices.length} offices`)
region.offices.forEach(office => {
console.log(`WB Analytics API: Processing office "${office.officeName}" (ID: ${office.officeID})`)
console.log(`WB Analytics API: Office metrics:`, office.metrics)
// Получаем товары для этого склада WB
const warehouseStocks = stocksByWarehouse.get(office.officeID) || []
console.log(`WB Analytics API: Found ${warehouseStocks.length} stock records for warehouse ${office.officeID}`)
// Создаем записи для каждого товара на этом складе WB
// Если нет конкретных остатков, создаем на основе карточек товаров
if (warehouseStocks.length > 0) {
// Группируем по nmId
const stocksByNmId = new Map<number, Record<string, unknown>[]>()
warehouseStocks.forEach((stock: Record<string, unknown>) => {
const nmId = Number(stock.nmId) || 0
if (nmId > 0) {
if (!stocksByNmId.has(nmId)) {
stocksByNmId.set(nmId, [])
}
stocksByNmId.get(nmId)!.push(stock)
}
})
// Создаем записи для каждого товара
stocksByNmId.forEach((stockItems, nmId) => {
const firstStock = stockItems[0]
const card = cardsMap.get(nmId)
const stock: WBStock = {
nmId,
vendorCode: String(firstStock.vendorCode || firstStock.supplierArticle || ''),
title: String(firstStock.title || firstStock.subject || card?.title || `Товар ${nmId}`),
brand: String(firstStock.brand || card?.brand || ''),
price: Number(firstStock.price || firstStock.Price) || 0,
stocks: [{
warehouseId: office.officeID,
warehouseName: office.officeName,
quantity: Number(firstStock.quantity) || 0,
quantityFull: Number(firstStock.quantityFull) || 0,
inWayToClient: office.metrics.toClientCount, // Берем из Analytics API
inWayFromClient: office.metrics.fromClientCount // Берем из Analytics API
}],
totalQuantity: Number(firstStock.quantity) || 0,
totalReserved: office.metrics.toClientCount,
photos: Array.isArray(firstStock.photos) ? firstStock.photos : (card?.photos || []),
mediaFiles: Array.isArray(firstStock.mediaFiles) ? firstStock.mediaFiles : [],
characteristics: Array.isArray(firstStock.characteristics) ? firstStock.characteristics : (card?.characteristics || []),
subjectName: String(firstStock.subjectName || firstStock.subject || card?.subjectName || ''),
description: String(firstStock.description || card?.description || '')
}
stocks.push(stock)
})
} else {
console.log(`WB Analytics API: No stock records found for warehouse ${office.officeID}, creating entries for each product`)
// Создаем записи для каждого товара на этом складе WB
// Даже если нет точных остатков, показываем движение товаров
cardsMap.forEach((card, nmId) => {
const stock: WBStock = {
nmId,
vendorCode: String(card.vendorCode || ''),
title: String(card.title || `Товар ${nmId}`),
brand: String(card.brand || ''),
price: 0, // У карточки нет цены, используем 0
stocks: [{
warehouseId: office.officeID,
warehouseName: office.officeName,
quantity: office.metrics.stockCount, // Общее количество на складе
quantityFull: office.metrics.stockCount,
inWayToClient: office.metrics.toClientCount, // К клиенту
inWayFromClient: office.metrics.fromClientCount // От клиента
}],
totalQuantity: office.metrics.stockCount,
totalReserved: office.metrics.toClientCount,
photos: Array.isArray(card.photos) ? card.photos : [],
mediaFiles: Array.isArray(card.mediaFiles) ? card.mediaFiles : [],
characteristics: Array.isArray(card.characteristics) ? card.characteristics : [],
subjectName: String(card.subjectName || region.regionName),
description: String(card.description || `Регион: ${region.regionName}, Склад: ${office.officeName}`)
}
stocks.push(stock)
})
}
})
})
} else {
console.log('WB Analytics API: No regions data in response')
}
console.log(`WB Analytics API: Processed ${stocks.length} stock records`)
return stocks
console.log(`WB Analytics API: Returning raw response for processing in component`)
return response
} catch (error) {
console.error('WB Analytics API: Error getting stocks report:', error)
return []
return { data: { regions: [] } }
}
}