diff --git a/src/components/admin/ui-kit-section.tsx b/src/components/admin/ui-kit-section.tsx
index b28c666..6d3cea4 100644
--- a/src/components/admin/ui-kit-section.tsx
+++ b/src/components/admin/ui-kit-section.tsx
@@ -18,6 +18,7 @@ import { BusinessDemo } from "./ui-kit/business-demo";
import { TimesheetDemo } from "./ui-kit/timesheet-demo";
import { FulfillmentWarehouseDemo } from "./ui-kit/fulfillment-warehouse-demo";
import { SuppliesDemo } from "./ui-kit/supplies-demo";
+import { WBWarehouseDemo } from "./ui-kit/wb-warehouse-demo";
import { SuppliesNavigationDemo } from "./ui-kit/supplies-navigation-demo";
export function UIKitSection() {
@@ -134,6 +135,12 @@ export function UIKitSection() {
>
Поставки
+
+ Склад WB
+
+
+
+
+
diff --git a/src/components/admin/ui-kit/wb-warehouse-demo.tsx b/src/components/admin/ui-kit/wb-warehouse-demo.tsx
new file mode 100644
index 0000000..29148b8
--- /dev/null
+++ b/src/components/admin/ui-kit/wb-warehouse-demo.tsx
@@ -0,0 +1,315 @@
+"use client";
+
+import React from 'react';
+import { StatsCards } from '@/components/wb-warehouse/stats-cards';
+import { SearchBar } from '@/components/wb-warehouse/search-bar';
+import { TableHeader } from '@/components/wb-warehouse/table-header';
+import { LoadingSkeleton } from '@/components/wb-warehouse/loading-skeleton';
+import { StockTableRow } from '@/components/wb-warehouse/stock-table-row';
+
+export function WBWarehouseDemo() {
+ // Мок данные для демонстрации
+ const mockStatsData = {
+ totalProducts: 156,
+ totalStocks: 12847,
+ totalReserved: 342,
+ totalFromClient: 28,
+ activeWarehouses: 12,
+ loading: false
+ };
+
+ const mockStockItem = {
+ nmId: 444711032,
+ vendorCode: "V326",
+ title: "Электробритва для бороды с 3D головками триммер беспроводной",
+ brand: "ANNRennel",
+ price: 2990,
+ stocks: [
+ {
+ warehouseId: 120762,
+ warehouseName: "Электросталь",
+ quantity: 188,
+ quantityFull: 188,
+ inWayToClient: 2,
+ inWayFromClient: 1
+ },
+ {
+ warehouseId: 507,
+ warehouseName: "Коледино",
+ quantity: 0,
+ quantityFull: 0,
+ inWayToClient: 3,
+ inWayFromClient: 1
+ },
+ {
+ warehouseId: 208277,
+ warehouseName: "Невинномысск",
+ quantity: 56,
+ quantityFull: 56,
+ inWayToClient: 0,
+ inWayFromClient: 0
+ },
+ {
+ warehouseId: 130744,
+ warehouseName: "Краснодар",
+ quantity: 1,
+ quantityFull: 1,
+ inWayToClient: 0,
+ inWayFromClient: 0
+ }
+ ],
+ totalQuantity: 245,
+ totalReserved: 5,
+ photos: [
+ {
+ big: "https://basket-04.wbbasket.ru/vol444/part44471/444711032/images/big/1.webp",
+ c246x328: "https://basket-04.wbbasket.ru/vol444/part44471/444711032/images/c246x328/1.webp"
+ }
+ ],
+ mediaFiles: [],
+ characteristics: [
+ { name: "Способ бритья", value: "сухое" },
+ { name: "Модель", value: "V326" },
+ { name: "Время работы от аккумулятора (мин)", value: "60" },
+ { name: "Гарантийный срок", value: "1 год" },
+ { name: "Цвет", value: ["синий", "черный"] }
+ ],
+ subjectName: "Триммеры",
+ description: "Триммер для бороды - незаменимый помощник для каждого парня и мужчины."
+ };
+
+ const [searchTerm, setSearchTerm] = React.useState("");
+
+ return (
+
+ {/* Заголовок секции */}
+
+
WB Warehouse Components
+
+ Компоненты для страницы склада Wildberries
+
+
+
+ {/* Stats Cards */}
+
+
📊 StatsCards - Карточки статистики
+
+
+
+
+
📝 Код использования:
+
+{``}
+
+
+
+
+
+ {/* Search Bar */}
+
+
🔍 SearchBar - Поиск товаров
+
+
+
+
+
📝 Код использования:
+
+{``}
+
+
+
+
+
+ {/* Table Header */}
+
+
📋 TableHeader - Шапка таблицы
+
+
+
+
+
📝 Код использования:
+
+{``}
+
+
+
+
+
+ {/* Loading Skeleton */}
+
+
⏳ LoadingSkeleton - Скелетоны загрузки
+
+
+
+
+
+
+
📝 Код использования:
+
+{``}
+
+
+
+
+
+ {/* Stock Table Row */}
+
+
📦 StockTableRow - Строка товара
+
+
+
+
+
📝 Код использования:
+
+{`
+
+// где stockItem содержит:
+// - nmId, vendorCode, title, brand
+// - stocks[] - данные по складам
+// - totalQuantity, totalReserved
+// - photos[], characteristics[]
+// - subjectName, description`}
+
+
+
+
+
+ {/* States Demo */}
+
+
🎭 States - Состояния компонентов
+
+
+ {/* Loading State */}
+
+
⏳ Loading State
+
+
+
+ {/* Empty State */}
+
+
🔍 Search State
+ {}}
+ />
+
+
+ {/* Multiple Stock Rows */}
+
+
📋 Table with Multiple Items
+
+
+
+
+
+
+
+
+
+ {/* Color Variants */}
+
+
🎨 Color Variants - Цветовые варианты
+
+
+ {/* Товары - синий */}
+
+
+ {/* Остаток - зеленый */}
+
+
+ {/* К клиенту - оранжевый */}
+
+
+ {/* От клиента - красный */}
+
+
+ {/* Складов - фиолетовый */}
+
+
+
+
+ {/* Usage Guidelines */}
+
+
📚 Usage Guidelines - Рекомендации
+
+
+
+
+
✅ Правильно
+
+ - • Используй StatsCards для отображения ключевых метрик
+ - • SearchBar всегда размещай перед таблицей
+ - • TableHeader обязателен для понимания структуры
+ - • LoadingSkeleton соответствует структуре данных
+ - • StockTableRow содержит основную и дополнительную информацию
+
+
+
+
+
❌ Неправильно
+
+ - • Не используй StatsCards без данных
+ - • Не размещай SearchBar после таблицы
+ - • Не показывай данные без TableHeader
+ - • Не используй LoadingSkeleton с готовыми данными
+ - • Не модифицируй внутреннюю структуру StockTableRow
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/wb-warehouse/loading-skeleton.tsx b/src/components/wb-warehouse/loading-skeleton.tsx
new file mode 100644
index 0000000..b64e994
--- /dev/null
+++ b/src/components/wb-warehouse/loading-skeleton.tsx
@@ -0,0 +1,51 @@
+"use client"
+
+import React from 'react'
+
+export function LoadingSkeleton() {
+ return (
+
+ {[...Array(8)].map((_, i) => (
+
+ ))}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/wb-warehouse/search-bar.tsx b/src/components/wb-warehouse/search-bar.tsx
new file mode 100644
index 0000000..583bd71
--- /dev/null
+++ b/src/components/wb-warehouse/search-bar.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import React from 'react'
+import { Search } from 'lucide-react'
+
+interface SearchBarProps {
+ searchTerm: string
+ onSearchChange: (value: string) => void
+}
+
+export function SearchBar({ searchTerm, onSearchChange }: SearchBarProps) {
+ return (
+
+
+
+ onSearchChange(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"
+ />
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/wb-warehouse/stats-cards.tsx b/src/components/wb-warehouse/stats-cards.tsx
new file mode 100644
index 0000000..de528bc
--- /dev/null
+++ b/src/components/wb-warehouse/stats-cards.tsx
@@ -0,0 +1,106 @@
+"use client"
+
+import React from 'react'
+import { Package, Warehouse, TrendingUp, TrendingDown, MapPin } from 'lucide-react'
+
+interface StatsCardsProps {
+ totalProducts: number
+ totalStocks: number
+ totalReserved: number
+ totalFromClient: number
+ activeWarehouses: number
+ loading: boolean
+}
+
+export function StatsCards({
+ totalProducts,
+ totalStocks,
+ totalReserved,
+ totalFromClient,
+ activeWarehouses,
+ loading
+}: StatsCardsProps) {
+ return (
+
+ {/* Товаров */}
+
+
+
+
+
+ {loading ?
: totalProducts.toLocaleString()}
+
+
+
+
+
+
+ {/* Общий остаток */}
+
+
+
+
+
+ Остаток
+
+
+ {loading ?
: totalStocks.toLocaleString()}
+
+
+
+
+
+
+ {/* К клиенту */}
+
+
+
+
+
+ К клиенту
+
+
+ {loading ?
: totalReserved.toLocaleString()}
+
+
+
+
+
+
+ {/* От клиента */}
+
+
+
+
+
+ От клиента
+
+
+ {loading ?
: totalFromClient.toLocaleString()}
+
+
+
+
+
+
+ {/* Складов */}
+
+
+
+
+
+ Складов
+
+
+ {loading ?
: activeWarehouses}
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/wb-warehouse/stock-table-row.tsx b/src/components/wb-warehouse/stock-table-row.tsx
new file mode 100644
index 0000000..81492f7
--- /dev/null
+++ b/src/components/wb-warehouse/stock-table-row.tsx
@@ -0,0 +1,231 @@
+"use client"
+
+import React from 'react'
+import { Package } from 'lucide-react'
+
+// Интерфейсы (можно будет вынести в отдельный файл types.ts)
+interface WBStockInfo {
+ warehouseId: number
+ warehouseName: string
+ quantity: number
+ quantityFull: number
+ inWayToClient: number
+ inWayFromClient: number
+}
+
+interface WBStock {
+ nmId: number
+ vendorCode: string
+ title: string
+ brand: string
+ price: number
+ stocks: WBStockInfo[]
+ totalQuantity: number
+ totalReserved: number
+ photos: any[]
+ mediaFiles: any[]
+ characteristics: any[]
+ subjectName: string
+ description: string
+}
+
+interface StockTableRowProps {
+ item: WBStock
+}
+
+export function StockTableRow({ item }: StockTableRowProps) {
+ // Функция для получения изображений карточки
+ const getCardImages = (item: WBStock) => {
+ const fallbackUrl = `https://basket-${String(item.nmId).slice(0, 2)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(item.nmId).slice(0, -3)}/${item.nmId}/images/big/1.webp`
+
+ // Проверяем photos
+ if (item.photos && item.photos.length > 0) {
+ return item.photos.map(photo => photo.big || photo.c516x688 || photo.c246x328 || photo.square || photo.tm || fallbackUrl)
+ }
+
+ // Проверяем mediaFiles
+ if (item.mediaFiles && item.mediaFiles.length > 0) {
+ return item.mediaFiles.map(media => media.big || media.c516x688 || media.c246x328 || media.square || media.tm || fallbackUrl)
+ }
+
+ return [fallbackUrl]
+ }
+
+ const getStockStatus = (quantity: number) => {
+ 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)
+ const images = getCardImages(item)
+ const mainImage = images[0] || null
+
+ // Отбираем ключевые характеристики для отображения в таблице
+ const keyCharacteristics = item.characteristics?.slice(0, 3) || []
+
+ return (
+
+ {/* Основная строка товара */}
+
+ {/* Товар (3 колонки) */}
+
+
+ {mainImage ? (
+

{
+ const target = e.target as HTMLImageElement
+ target.src = `https://basket-${String(item.nmId).slice(0, 2)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(item.nmId).slice(0, -3)}/${item.nmId}/images/big/1.webp`
+ }}
+ />
+ ) : (
+
+ )}
+
+
+
+
+ {item.brand || 'Без бренда'}
+
+ #{item.nmId}
+
+
+ {item.title}
+
+
+ {item.vendorCode}
+
+
+
+
+ {/* Остаток */}
+
+
+
+ {item.totalQuantity.toLocaleString()}
+
+
+ {stockStatus.label}
+
+
+
+
+ {/* К клиенту */}
+
+
+
+ {item.stocks.reduce((sum, s) => sum + s.inWayToClient, 0)}
+
+
в пути
+
+
+
+ {/* От клиента */}
+
+
+
+ {item.stocks.reduce((sum, s) => sum + s.inWayFromClient, 0)}
+
+
возвраты
+
+
+
+ {/* Складов */}
+
+
+
+ {item.stocks.length}
+
+
активных
+
+
+
+ {/* Характеристики (5 колонок) */}
+
+
+ {keyCharacteristics.map((char, index) => (
+
+ {char.name}:
+
+ {Array.isArray(char.value) ? char.value.join(', ') : String(char.value)}
+
+
+ ))}
+ {item.subjectName && (
+
+ Категория:
+ {item.subjectName}
+
+ )}
+
+
+
+
+ {/* Города в модулях */}
+
+
+ {item.stocks.map((stock, stockIndex) => (
+
+ {/* Название города */}
+
+ {stock.warehouseName}
+
+
+ {/* Цифры */}
+
+
+
0 ? 'text-green-400' : 'text-white/30'}`}>
+ {stock.quantity}
+
+
остаток
+
+
+ {(stock.inWayToClient > 0 || stock.inWayFromClient > 0) && (
+ <>
+
+
+ {stock.inWayToClient > 0 && (
+
+
{stock.inWayToClient}
+
к клиенту
+
+ )}
+
+ {stock.inWayFromClient > 0 && (
+
+
{stock.inWayFromClient}
+
от клиента
+
+ )}
+ >
+ )}
+
+
+ ))}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/wb-warehouse/table-header.tsx b/src/components/wb-warehouse/table-header.tsx
new file mode 100644
index 0000000..697abd7
--- /dev/null
+++ b/src/components/wb-warehouse/table-header.tsx
@@ -0,0 +1,37 @@
+"use client"
+
+import React from 'react'
+import { Package, Warehouse, TrendingUp, TrendingDown, MapPin, Info } from 'lucide-react'
+
+export function TableHeader() {
+ return (
+
+
+
+
+
+ Остаток
+
+
+
+ К клиенту
+
+
+
+ От клиента
+
+
+
+ Складов
+
+
+
+ Характеристики
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/wb-warehouse/wb-warehouse-dashboard-old.tsx b/src/components/wb-warehouse/wb-warehouse-dashboard-old.tsx
new file mode 100644
index 0000000..1565319
--- /dev/null
+++ b/src/components/wb-warehouse/wb-warehouse-dashboard-old.tsx
@@ -0,0 +1,894 @@
+"use client"
+
+import React, { useState, useEffect } from 'react'
+import { useAuth } from '@/hooks/useAuth'
+import { Sidebar } from '@/components/dashboard/sidebar'
+import { useSidebar } from '@/hooks/useSidebar'
+import { Card } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Badge } from '@/components/ui/badge'
+import { Skeleton } from '@/components/ui/skeleton'
+import { WildberriesService } from '@/services/wildberries-service'
+import { toast } from 'sonner'
+import { StatsCards } from './stats-cards'
+import { SearchBar } from './search-bar'
+import { TableHeader } from './table-header'
+import { LoadingSkeleton } from './loading-skeleton'
+import { StockTableRow } from './stock-table-row'
+import { TrendingUp, Search, Package, Warehouse, TrendingDown, MapPin, Info } from 'lucide-react'
+
+interface WBStock {
+ nmId: number
+ vendorCode: string
+ title: string
+ brand: string
+ price: number
+ stocks: Array<{
+ warehouseId: number
+ warehouseName: string
+ quantity: number
+ quantityFull: number
+ inWayToClient: number
+ inWayFromClient: number
+ }>
+ totalQuantity: number
+ totalReserved: number
+ photos?: Array<{
+ big?: string
+ c246x328?: string
+ c516x688?: string
+ square?: string
+ tm?: string
+ }>
+ mediaFiles?: string[]
+ characteristics?: Array<{
+ id: number
+ name: string
+ value: string[] | string
+ }>
+ subjectName?: string
+ description?: string
+}
+
+interface WBWarehouse {
+ id: number
+ name: string
+ cargoType: number
+ deliveryType: number
+}
+
+export function WBWarehouseDashboard() {
+ const { user } = useAuth()
+ const { getSidebarMargin } = useSidebar()
+
+ const [stocks, setStocks] = useState([])
+ const [warehouses, setWarehouses] = useState([])
+ const [analyticsData, setAnalyticsData] = useState>([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+ const [searchTerm, setSearchTerm] = useState('')
+ const [selectedWarehouse, setSelectedWarehouse] = useState('all')
+
+ // Статистика
+ 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)
+
+ // Загрузка данных
+ const loadWarehouseData = async (showToast = false) => {
+ const isInitialLoad = loading
+ if (!isInitialLoad) setRefreshing(true)
+
+ try {
+ const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
+
+ if (!wbApiKey?.isActive) {
+ toast.error('API ключ Wildberries не настроен')
+ return
+ }
+
+ const validationData = wbApiKey.validationData as Record
+ const apiToken = validationData?.token ||
+ validationData?.apiKey ||
+ validationData?.key ||
+ (wbApiKey as { apiKey?: string }).apiKey
+
+ if (!apiToken) {
+ toast.error('Токен API не найден')
+ return
+ }
+
+ const wbService = new WildberriesService(apiToken)
+
+ console.log('WB Warehouse: Starting data load...')
+
+ // Сначала получаем карточки товаров - это основа для всего
+ 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:', cards.length)
+ console.log('WB Warehouse: Card IDs for analytics:', nmIds)
+
+ if (cards.length === 0) {
+ console.log('WB Warehouse: No cards found, cannot proceed with analytics')
+ setStocks([])
+ setWarehouses([])
+ return
+ }
+
+ // Получаем данные по складам для каждого товара отдельно
+ console.log('WB Warehouse: Getting stocks analytics for each card separately...')
+ const analyticsResults = []
+
+ 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
+ const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults)
+ console.log('WB Warehouse: Combined stocks:', combinedStocks.length)
+
+ // Отключаем общую аналитику - будем показывать детализацию по товарам в карточках
+ setAnalyticsData([])
+
+ // Используем объединенные данные
+ setStocks(combinedStocks)
+
+ // Извлекаем информацию о складах из данных Analytics API
+ const warehousesFromAnalytics = extractWarehousesFromStocks(combinedStocks)
+ setWarehouses(warehousesFromAnalytics)
+
+ // Обновляем статистику
+ updateStatistics(combinedStocks, warehousesFromAnalytics)
+
+ if (showToast) {
+ toast.success('Данные обновлены')
+ }
+
+ } catch (error) {
+ console.error('Ошибка загрузки данных склада:', error)
+ toast.error('Ошибка загрузки данных склада')
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+
+
+ // Объединение карточек товаров с индивидуальными данными Analytics API
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
+ const stocksMap = new Map()
+
+ 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()
+
+ 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()
+
+ 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 // По умолчанию
+ })
+ }
+ })
+ })
+
+ return Array.from(warehousesMap.values())
+ }
+
+ // Обновление статистики
+ const updateStatistics = (stocksData: WBStock[], warehousesData: WBWarehouse[]) => {
+ setTotalProducts(stocksData.length)
+ 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))
+ )
+ setActiveWarehouses(warehousesWithStock.size)
+ }
+
+
+
+ // Фильтрация товаров (показываем все товары, включая с нулевыми остатками)
+ const filteredStocks = stocks.filter(item => {
+ const matchesSearch = searchTerm === '' ||
+ item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ item.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ item.brand.toLowerCase().includes(searchTerm.toLowerCase())
+
+ const matchesWarehouse = selectedWarehouse === 'all' ||
+ item.stocks.some(s => s.warehouseId.toString() === selectedWarehouse)
+
+ return matchesSearch && matchesWarehouse
+ })
+
+ useEffect(() => {
+ if (user?.organization?.type === 'SELLER') {
+ loadWarehouseData()
+ }
+ }, [user])
+
+ // Проверяем настройку API ключа
+ const hasWBApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive
+
+ return (
+
+
+
+
+
+ {/* Результирующие вкладки */}
+
+
+ {/* Аналитика по складам WB */}
+ {analyticsData.length > 0 && (
+
+
+
+ Движение товаров по складам WB
+
+
+ {analyticsData.map((warehouse) => (
+
+ {warehouse.warehouseName}
+
+
+ К клиенту:
+ {warehouse.toClient}
+
+
+ От клиента:
+ {warehouse.fromClient}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Поиск */}
+
+
+ {/* Список товаров */}
+
+ {loading ? (
+
+ ) : !hasWBApiKey ? (
+
+
+ Настройте API ключ Wildberries
+
+ Для просмотра остатков товаров на складах WB необходимо добавить API ключ
+
+
+
+ ) : filteredStocks.length === 0 ? (
+
+
+
+ {searchTerm || selectedWarehouse !== 'all'
+ ? 'Товары не найдены по заданным фильтрам'
+ : 'Нет карточек товаров в WB'
+ }
+
+
+ ) : (
+
+ {/* Красивая шапка таблицы */}
+
+
+
+
+
+ Остаток
+
+
+
+ К клиенту
+
+
+
+ От клиента
+
+
+
+ Складов
+
+
+
+ Характеристики
+
+
+
+
+ {/* Таблица товаров */}
+
+ {filteredStocks.map((item, index) => (
+
+ ))}
+
+
+ )}
+
+
+
+
+ )
+}
+
+// Табличная строка товара
+function StockTableRow({ 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: '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)
+ const images = getCardImages(item)
+ const mainImage = images[0] || null
+
+ // Отбираем ключевые характеристики для отображения в таблице
+ const keyCharacteristics = item.characteristics?.slice(0, 3) || []
+
+ return (
+
+ {/* Основная строка товара */}
+
+ {/* Товар (3 колонки) */}
+
+
+ {mainImage ? (
+

+ ) : (
+
+ )}
+
+
+
+
+ {item.brand || 'Без бренда'}
+
+ #{item.nmId}
+
+
+ {item.title}
+
+
+ {item.vendorCode}
+
+
+
+
+ {/* Остаток */}
+
+
+
+ {item.totalQuantity.toLocaleString()}
+
+
+ {stockStatus.label}
+
+
+
+
+ {/* К клиенту */}
+
+
+
+ {item.stocks.reduce((sum, s) => sum + s.inWayToClient, 0)}
+
+
в пути
+
+
+
+ {/* От клиента */}
+
+
+
+ {item.stocks.reduce((sum, s) => sum + s.inWayFromClient, 0)}
+
+
возвраты
+
+
+
+ {/* Складов */}
+
+
+
+ {item.stocks.length}
+
+
активных
+
+
+
+ {/* Характеристики (5 колонок) */}
+
+
+ {keyCharacteristics.map((char, index) => (
+
+ {char.name}:
+
+ {Array.isArray(char.value) ? char.value.join(', ') : String(char.value)}
+
+
+ ))}
+ {item.subjectName && (
+
+ Категория:
+ {item.subjectName}
+
+ )}
+
+
+
+
+ {/* Города в модулях */}
+
+
+ {item.stocks.map((stock, stockIndex) => (
+
+ {/* Название города */}
+
+ {stock.warehouseName}
+
+
+ {/* Цифры */}
+
+
+
0 ? 'text-green-400' : 'text-white/30'}`}>
+ {stock.quantity}
+
+
остаток
+
+
+ {(stock.inWayToClient > 0 || stock.inWayFromClient > 0) && (
+ <>
+
+
+ {stock.inWayToClient > 0 && (
+
+
{stock.inWayToClient}
+
к клиенту
+
+ )}
+
+ {stock.inWayFromClient > 0 && (
+
+
{stock.inWayFromClient}
+
от клиента
+
+ )}
+ >
+ )}
+
+
+ ))}
+
+
+
+ )
+}
+
+// Супер современная карточка товара (СТАРАЯ ВЕРСИЯ - НЕ ИСПОЛЬЗУЕТСЯ)
+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 (
+
+ {/* Градиентный фон при hover */}
+
+
+
+ {/* Хедер карточки */}
+
+ {/* Изображение товара */}
+
+
+ {mainImage ? (
+

+ ) : (
+
+ )}
+
+ {/* WB Badge */}
+
+
+
+ {/* Инфо товара */}
+
+
+
+
+
+ {item.brand || 'Без бренда'}
+
+ #{item.nmId}
+
+
+ {item.title}
+
+
+
+ {/* Статус */}
+
+ {stockStatus.icon}
+ {stockStatus.label}
+
+
+
+
+ Артикул: {item.vendorCode}
+
+
+
+
+ {/* Компактная статистика */}
+
+
+
{item.totalQuantity.toLocaleString()}
+
Остаток
+
+
+
{item.stocks.length}
+
Складов
+
+
+
+ {item.stocks.reduce((sum, s) => sum + s.inWayToClient, 0)}
+
+
К клиенту
+
+
+
+ {item.stocks.reduce((sum, s) => sum + s.inWayFromClient, 0)}
+
+
От клиента
+
+
+
+ {/* Склады - компактно */}
+
+
+
+ Склады
+
+
+ {item.stocks.slice(0, 3).map((stock, stockIndex) => (
+
+
+
{stock.warehouseName}
+
ID: {stock.warehouseId}
+
+
+
+
0 ? 'text-green-400' : 'text-white/30'}`}>
+ {stock.quantity}
+
+
+
+
0 ? 'text-orange-400' : 'text-white/30'}`}>
+ {stock.inWayToClient}
+
+
+
+
0 ? 'text-red-400' : 'text-white/30'}`}>
+ {stock.inWayFromClient}
+
+
+
+
+ ))}
+ {item.stocks.length > 3 && (
+
+ +{item.stocks.length - 3} ещё складов
+
+ )}
+
+
+
+ {/* Категория */}
+ {item.subjectName && (
+
+
+ Категория: {item.subjectName}
+
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx b/src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx
new file mode 100644
index 0000000..3dc13c7
--- /dev/null
+++ b/src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx
@@ -0,0 +1,344 @@
+"use client"
+
+import React, { useState, useEffect } from 'react'
+import { useAuth } from '@/hooks/useAuth'
+import { Sidebar } from '@/components/dashboard/sidebar'
+import { useSidebar } from '@/hooks/useSidebar'
+import { Card } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { WildberriesService } from '@/services/wildberries-service'
+import { toast } from 'sonner'
+import { StatsCards } from './stats-cards'
+import { SearchBar } from './search-bar'
+import { TableHeader } from './table-header'
+import { LoadingSkeleton } from './loading-skeleton'
+import { StockTableRow } from './stock-table-row'
+import { TrendingUp, Package } from 'lucide-react'
+
+interface WBStock {
+ nmId: number
+ vendorCode: string
+ title: string
+ brand: string
+ price: number
+ stocks: Array<{
+ warehouseId: number
+ warehouseName: string
+ quantity: number
+ quantityFull: number
+ inWayToClient: number
+ inWayFromClient: number
+ }>
+ totalQuantity: number
+ totalReserved: number
+ photos?: Array<{
+ big?: string
+ c246x328?: string
+ c516x688?: string
+ square?: string
+ tm?: string
+ }>
+ mediaFiles?: string[]
+ characteristics?: Array<{
+ name: string
+ value: string | string[]
+ }>
+ subjectName: string
+ description: string
+}
+
+interface WBWarehouse {
+ id: number
+ name: string
+ cargoType: number
+ deliveryType: number
+}
+
+export function WBWarehouseDashboard() {
+ const { user } = useAuth()
+ const { isCollapsed, getSidebarMargin } = useSidebar()
+
+ const [stocks, setStocks] = useState([])
+ const [warehouses, setWarehouses] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [searchTerm, setSearchTerm] = useState('')
+
+ // Статистика
+ 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)
+
+ // Analytics data
+ const [analyticsData, setAnalyticsData] = useState([])
+
+ const hasWBApiKey = user?.wildberriesApiKey
+
+ // Комбинирование карточек с индивидуальными данными аналитики
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
+ const stocksMap = new Map()
+
+ // Создаем карту аналитических данных для быстрого поиска
+ const analyticsMap = new Map() // Map nmId to its analytics data
+ 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: [],
+ 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
+ }
+
+ const analyticsData = analyticsMap.get(card.nmID)
+ 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) => {
+ 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
+ })
+ }
+ })
+ }
+
+ stocksMap.set(card.nmID, stock)
+ })
+
+ return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity)
+ }
+
+ // Извлечение информации о складах из данных
+ const extractWarehousesFromStocks = (stocksData: WBStock[]): WBWarehouse[] => {
+ const warehousesMap = new Map()
+
+ 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
+ })
+ }
+ })
+ })
+
+ return Array.from(warehousesMap.values())
+ }
+
+ // Обновление статистики
+ const updateStatistics = (stocksData: WBStock[], warehousesData: WBWarehouse[]) => {
+ setTotalProducts(stocksData.length)
+ 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))
+ )
+ setActiveWarehouses(warehousesWithStock.size)
+ }
+
+ // Загрузка данных склада
+ const loadWarehouseData = async () => {
+ if (!user?.wildberriesApiKey) return
+
+ setLoading(true)
+ try {
+ const apiToken = user.wildberriesApiKey
+ const wbService = new WildberriesService(apiToken)
+
+ // 1. Получаем карточки товаров
+ const cards = await WildberriesService.getAllCards(apiToken).catch(() => [])
+ console.log('WB Warehouse: Loaded cards:', cards.length)
+
+ if (cards.length === 0) {
+ toast.error('Нет карточек товаров в WB')
+ return
+ }
+
+ const nmIds = cards.map(card => card.nmID).filter(id => id > 0)
+ console.log('WB Warehouse: NM IDs to process:', nmIds.length)
+
+ // 2. Получаем аналитику для каждого товара индивидуально
+ const analyticsResults = []
+ for (const nmId of nmIds) {
+ try {
+ console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`)
+ const result = await wbService.getStocksReportByOffices({
+ nmIDs: [nmId],
+ stockType: ''
+ })
+ analyticsResults.push({ nmId, data: result })
+ await new Promise(resolve => setTimeout(resolve, 1000))
+ } catch (error) {
+ console.error(`WB Warehouse: Error fetching analytics for nmId ${nmId}:`, error)
+ }
+ }
+
+ console.log('WB Warehouse: Analytics results:', analyticsResults.length)
+
+ // 3. Комбинируем данные
+ const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults)
+ console.log('WB Warehouse: Combined stocks:', combinedStocks.length)
+
+ // 4. Извлекаем склады и обновляем статистику
+ const extractedWarehouses = extractWarehousesFromStocks(combinedStocks)
+
+ setStocks(combinedStocks)
+ setWarehouses(extractedWarehouses)
+ updateStatistics(combinedStocks, extractedWarehouses)
+
+ toast.success(`Загружено товаров: ${combinedStocks.length}`)
+ } catch (error: any) {
+ console.error('WB Warehouse: Error loading data:', error)
+ toast.error('Ошибка загрузки данных: ' + (error.message || 'Неизвестная ошибка'))
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ if (hasWBApiKey) {
+ loadWarehouseData()
+ }
+ }, [hasWBApiKey])
+
+ // Фильтрация товаров
+ const filteredStocks = stocks.filter(item => {
+ if (!searchTerm) return true
+ const search = searchTerm.toLowerCase()
+ return (
+ item.title.toLowerCase().includes(search) ||
+ String(item.nmId).includes(search) ||
+ item.brand.toLowerCase().includes(search) ||
+ item.vendorCode.toLowerCase().includes(search)
+ )
+ })
+
+ return (
+
+
+
+
+
+ {/* Результирующие вкладки */}
+
+
+ {/* Аналитика по складам WB */}
+ {analyticsData.length > 0 && (
+
+
+
+ Движение товаров по складам WB
+
+
+ {analyticsData.map((warehouse) => (
+
+ {warehouse.warehouseName}
+
+
+ К клиенту:
+ {warehouse.toClient}
+
+
+ От клиента:
+ {warehouse.fromClient}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Поиск */}
+
+
+ {/* Список товаров */}
+
+ {loading ? (
+
+ ) : !hasWBApiKey ? (
+
+
+ Настройте API Wildberries
+ Для просмотра остатков добавьте API ключ Wildberries в настройках
+
+
+ ) : filteredStocks.length === 0 ? (
+
+
+ Товары не найдены
+ Попробуйте изменить параметры поиска
+
+ ) : (
+
+
+
+ {/* Таблица товаров */}
+
+ {filteredStocks.map((item, index) => (
+
+ ))}
+
+
+ )}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/wb-warehouse/wb-warehouse-dashboard.tsx b/src/components/wb-warehouse/wb-warehouse-dashboard.tsx
index ca1fe39..1c3e0c1 100644
--- a/src/components/wb-warehouse/wb-warehouse-dashboard.tsx
+++ b/src/components/wb-warehouse/wb-warehouse-dashboard.tsx
@@ -1,4 +1,5 @@
"use client"
+/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useEffect } from 'react'
import { useAuth } from '@/hooks/useAuth'
@@ -6,21 +7,14 @@ import { Sidebar } from '@/components/dashboard/sidebar'
import { useSidebar } from '@/hooks/useSidebar'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { Badge } from '@/components/ui/badge'
-import { Skeleton } from '@/components/ui/skeleton'
import { WildberriesService } from '@/services/wildberries-service'
import { toast } from 'sonner'
-import {
- Search,
- Package,
- Warehouse,
- TrendingUp,
- TrendingDown,
- RefreshCw,
- Filter,
- MapPin
-} from 'lucide-react'
+import { StatsCards } from './stats-cards'
+import { SearchBar } from './search-bar'
+import { TableHeader } from './table-header'
+import { LoadingSkeleton } from './loading-skeleton'
+import { StockTableRow } from './stock-table-row'
+import { TrendingUp, Package } from 'lucide-react'
interface WBStock {
nmId: number
@@ -38,21 +32,11 @@ interface WBStock {
}>
totalQuantity: number
totalReserved: number
- photos?: Array<{
- big?: string
- c246x328?: string
- c516x688?: string
- square?: string
- tm?: string
- }>
- mediaFiles?: string[]
- characteristics?: Array<{
- id: number
- name: string
- value: string[] | string
- }>
- subjectName?: string
- description?: string
+ photos: any[]
+ mediaFiles: any[]
+ characteristics: any[]
+ subjectName: string
+ description: string
}
interface WBWarehouse {
@@ -64,15 +48,12 @@ interface WBWarehouse {
export function WBWarehouseDashboard() {
const { user } = useAuth()
- const { getSidebarMargin } = useSidebar()
+ const { isCollapsed, getSidebarMargin } = useSidebar()
const [stocks, setStocks] = useState([])
const [warehouses, setWarehouses] = useState([])
- const [analyticsData, setAnalyticsData] = useState>([])
- const [loading, setLoading] = useState(true)
- const [refreshing, setRefreshing] = useState(false)
+ const [loading, setLoading] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
- const [selectedWarehouse, setSelectedWarehouse] = useState('all')
// Статистика
const [totalProducts, setTotalProducts] = useState(0)
@@ -80,12 +61,116 @@ export function WBWarehouseDashboard() {
const [totalReserved, setTotalReserved] = useState(0)
const [totalFromClient, setTotalFromClient] = useState(0)
const [activeWarehouses, setActiveWarehouses] = useState(0)
+
+ // Analytics data
+ const [analyticsData, setAnalyticsData] = useState([])
- // Загрузка данных
- const loadWarehouseData = async (showToast = false) => {
- const isInitialLoad = loading
- if (!isInitialLoad) setRefreshing(true)
+ // Проверяем настройку API ключа
+ const hasWBApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive
+
+ // Комбинирование карточек с индивидуальными данными аналитики
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
+ const stocksMap = new Map()
+ // Создаем карту аналитических данных для быстрого поиска
+ const analyticsMap = new Map() // Map nmId to its analytics data
+ 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: [],
+ 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
+ }
+
+ const analyticsData = analyticsMap.get(card.nmID)
+ 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) => {
+ 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
+ })
+ }
+ })
+ }
+
+ stocksMap.set(card.nmID, stock)
+ })
+
+ return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity)
+ }
+
+ // Извлечение информации о складах из данных
+ const extractWarehousesFromStocks = (stocksData: WBStock[]): WBWarehouse[] => {
+ const warehousesMap = new Map()
+
+ 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
+ })
+ }
+ })
+ })
+
+ return Array.from(warehousesMap.values())
+ }
+
+ // Обновление статистики
+ const updateStatistics = (stocksData: WBStock[], warehousesData: WBWarehouse[]) => {
+ setTotalProducts(stocksData.length)
+ 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))
+ )
+ setActiveWarehouses(warehousesWithStock.size)
+ }
+
+ // Загрузка данных склада
+ const loadWarehouseData = async () => {
+ if (!hasWBApiKey) return
+
+ setLoading(true)
try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
@@ -106,290 +191,74 @@ export function WBWarehouseDashboard() {
}
const wbService = new WildberriesService(apiToken)
-
- console.log('WB Warehouse: Starting data load...')
-
- // Сначала получаем карточки товаров - это основа для всего
- console.log('WB Warehouse: Getting cards...')
+
+ // 1. Получаем карточки товаров
const cards = await WildberriesService.getAllCards(apiToken).catch(() => [])
- const nmIds = cards.map(card => card.nmID).filter(id => id > 0)
- console.log('WB Warehouse: Found cards:', cards.length)
- console.log('WB Warehouse: Card IDs for analytics:', nmIds)
+ console.log('WB Warehouse: Loaded cards:', cards.length)
if (cards.length === 0) {
- console.log('WB Warehouse: No cards found, cannot proceed with analytics')
- setStocks([])
- setWarehouses([])
+ toast.error('Нет карточек товаров в WB')
return
}
- // Получаем данные по складам для каждого товара отдельно
- console.log('WB Warehouse: Getting stocks analytics for each card separately...')
+ const nmIds = cards.map(card => card.nmID).filter(id => id > 0)
+ console.log('WB Warehouse: NM IDs to process:', nmIds.length)
+
+ // 2. Получаем аналитику для каждого товара индивидуально
const analyticsResults = []
-
for (const nmId of nmIds) {
- console.log(`WB Warehouse: Getting analytics for nmId: ${nmId}`)
try {
+ console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`)
const result = await wbService.getStocksReportByOffices({
- nmIds: [nmId], // Один товар за раз
- stockType: '' // все склады
+ 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.error(`WB Warehouse: Error fetching analytics for nmId ${nmId}:`, error)
}
}
- console.log('WB Warehouse: Cards loaded:', cards.length)
- console.log('WB Warehouse: Analytics data received')
-
- // Объединяем карточки товаров с данными Analytics API
+ console.log('WB Warehouse: Analytics results:', analyticsResults.length)
+
+ // 3. Комбинируем данные
const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults)
console.log('WB Warehouse: Combined stocks:', combinedStocks.length)
+
+ // 4. Извлекаем склады и обновляем статистику
+ const extractedWarehouses = extractWarehousesFromStocks(combinedStocks)
- // Отключаем общую аналитику - будем показывать детализацию по товарам в карточках
- setAnalyticsData([])
-
- // Используем объединенные данные
setStocks(combinedStocks)
-
- // Извлекаем информацию о складах из данных Analytics API
- const warehousesFromAnalytics = extractWarehousesFromStocks(combinedStocks)
- setWarehouses(warehousesFromAnalytics)
-
- // Обновляем статистику
- updateStatistics(combinedStocks, warehousesFromAnalytics)
-
- if (showToast) {
- toast.success('Данные обновлены')
- }
-
- } catch (error) {
- console.error('Ошибка загрузки данных склада:', error)
- toast.error('Ошибка загрузки данных склада')
+ setWarehouses(extractedWarehouses)
+ updateStatistics(combinedStocks, extractedWarehouses)
+
+ toast.success(`Загружено товаров: ${combinedStocks.length}`)
+ } catch (error: any) {
+ console.error('WB Warehouse: Error loading data:', error)
+ toast.error('Ошибка загрузки данных: ' + (error.message || 'Неизвестная ошибка'))
} finally {
setLoading(false)
- setRefreshing(false)
}
}
-
-
- // Объединение карточек товаров с индивидуальными данными Analytics API
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
- const stocksMap = new Map()
-
- 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()
-
- 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()
-
- 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 // По умолчанию
- })
- }
- })
- })
-
- return Array.from(warehousesMap.values())
- }
-
- // Обновление статистики
- const updateStatistics = (stocksData: WBStock[], warehousesData: WBWarehouse[]) => {
- setTotalProducts(stocksData.length)
- 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))
- )
- setActiveWarehouses(warehousesWithStock.size)
- }
-
-
-
- // Фильтрация товаров (показываем все товары, включая с нулевыми остатками)
- const filteredStocks = stocks.filter(item => {
- const matchesSearch = searchTerm === '' ||
- item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
- item.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
- item.brand.toLowerCase().includes(searchTerm.toLowerCase())
-
- const matchesWarehouse = selectedWarehouse === 'all' ||
- item.stocks.some(s => s.warehouseId.toString() === selectedWarehouse)
-
- return matchesSearch && matchesWarehouse
- })
-
useEffect(() => {
- if (user?.organization?.type === 'SELLER') {
+ if (hasWBApiKey) {
loadWarehouseData()
}
- }, [user])
+ }, [hasWBApiKey])
- // Проверяем настройку API ключа
- const hasWBApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive
+ // Фильтрация товаров
+ const filteredStocks = stocks.filter(item => {
+ if (!searchTerm) return true
+ const search = searchTerm.toLowerCase()
+ return (
+ item.title.toLowerCase().includes(search) ||
+ String(item.nmId).includes(search) ||
+ item.brand.toLowerCase().includes(search) ||
+ item.vendorCode.toLowerCase().includes(search)
+ )
+ })
return (
@@ -398,87 +267,14 @@ export function WBWarehouseDashboard() {
{/* Результирующие вкладки */}
-
- {/* Товаров */}
-
-
-
-
-
- {loading ?
: totalProducts.toLocaleString()}
-
-
-
-
-
-
- {/* Общий остаток */}
-
-
-
-
-
- Остаток
-
-
- {loading ?
: totalStocks.toLocaleString()}
-
-
-
-
-
-
- {/* К клиенту */}
-
-
-
-
-
- К клиенту
-
-
- {loading ?
: totalReserved.toLocaleString()}
-
-
-
-
-
-
- {/* От клиента */}
-
-
-
-
-
- От клиента
-
-
- {loading ?
: totalFromClient.toLocaleString()}
-
-
-
-
-
-
- {/* Складов */}
-
-
-
-
-
- Складов
-
-
- {loading ?
: activeWarehouses}
-
-
-
-
-
-
+
{/* Аналитика по складам WB */}
{analyticsData.length > 0 && (
@@ -507,111 +303,27 @@ export function WBWarehouseDashboard() {
)}
- {/* Фильтры */}
-
-
-
- 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"
- />
-
-
-
-
-
-
-
+ {/* Поиск */}
+
{/* Список товаров */}
{loading ? (
- {/* Заголовки таблицы */}
-
-
-
Товар
-
Остаток
-
К клиенту
-
От клиента
-
Складов
-
Характеристики
-
-
-
- {/* Skeleton строки */}
-
- {[...Array(8)].map((_, i) => (
-
- ))}
-
+
+
) : !hasWBApiKey ? (
- Настройте API ключ Wildberries
-
- Для просмотра остатков товаров на складах WB необходимо добавить API ключ
-
-