From f65e9c42f693f5c6ec1d15114a530054a458e6fb Mon Sep 17 00:00:00 2001 From: Bivekich Date: Thu, 24 Jul 2025 12:11:52 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20WBWarehouseDemo=20=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D1=83=D1=8E=D1=89=D0=B0=D1=8F=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=20UIKitSection.=20=D0=9E=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=20WBWarehouseDashboard:=20=D1=83=D0=BB?= =?UTF-8?q?=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D1=82=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2.=20?= =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=20=D1=81=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B8=20=D0=BE=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8.=20=D0=9E=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=B8=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D1=80=D0=B0=20=D0=BA=D0=BE=D0=B4=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=B2=D1=8B=D1=88=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=83=D0=B4=D0=BE=D0=B1=D1=81=D1=82=D0=B2=D0=B0=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/ui-kit-section.tsx | 11 + .../admin/ui-kit/wb-warehouse-demo.tsx | 315 +++++ .../wb-warehouse/loading-skeleton.tsx | 51 + src/components/wb-warehouse/search-bar.tsx | 26 + src/components/wb-warehouse/stats-cards.tsx | 106 ++ .../wb-warehouse/stock-table-row.tsx | 231 ++++ src/components/wb-warehouse/table-header.tsx | 37 + .../wb-warehouse-dashboard-old.tsx | 894 ++++++++++++++ .../wb-warehouse-dashboard-refactored.tsx | 344 ++++++ .../wb-warehouse/wb-warehouse-dashboard.tsx | 1047 +++-------------- 10 files changed, 2198 insertions(+), 864 deletions(-) create mode 100644 src/components/admin/ui-kit/wb-warehouse-demo.tsx create mode 100644 src/components/wb-warehouse/loading-skeleton.tsx create mode 100644 src/components/wb-warehouse/search-bar.tsx create mode 100644 src/components/wb-warehouse/stats-cards.tsx create mode 100644 src/components/wb-warehouse/stock-table-row.tsx create mode 100644 src/components/wb-warehouse/table-header.tsx create mode 100644 src/components/wb-warehouse/wb-warehouse-dashboard-old.tsx create mode 100644 src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx diff --git a/src/components/admin/ui-kit-section.tsx b/src/components/admin/ui-kit-section.tsx index 35e3c54..ebb9128 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"; export function UIKitSection() { return ( @@ -133,6 +134,12 @@ export function UIKitSection() { > Поставки + + Склад WB + @@ -202,6 +209,10 @@ export function UIKitSection() { + + + + ); 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 - Цветовые варианты

+
+ + {/* Товары - синий */} +
+
156
+
Товаров
+
+ + {/* Остаток - зеленый */} +
+
12,847
+
Остаток
+
+ + {/* К клиенту - оранжевый */} +
+
342
+
К клиенту
+
+ + {/* От клиента - красный */} +
+
28
+
От клиента
+
+ + {/* Складов - фиолетовый */} +
+
12
+
Складов
+
+
+
+ + {/* 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 ? ( + {item.title} { + 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.title} + ) : ( +
+ +
+ )} +
+
+
+ + {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 ? ( + {item.title} + ) : ( +
+ +
+ )} +
+ {/* WB Badge */} +
+
+ WB +
+
+
+ + {/* Инфо товара */} +
+
+
+
+ + {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 ключ -

- @@ -619,27 +331,13 @@ export function WBWarehouseDashboard() { ) : filteredStocks.length === 0 ? ( -

- {searchTerm || selectedWarehouse !== 'all' - ? 'Товары не найдены по заданным фильтрам' - : 'Нет карточек товаров в WB' - } -

+

Товары не найдены

+

Попробуйте изменить параметры поиска

) : (
- {/* Заголовки таблицы */} -
-
-
Товар
-
Остаток
-
К клиенту
-
От клиента
-
Складов
-
Характеристики
-
-
- + + {/* Таблица товаров */}
{filteredStocks.map((item, index) => ( @@ -653,383 +351,4 @@ export function WBWarehouseDashboard() {
) -} - -// Табличная строка товара -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.title} - ) : ( -
- -
- )} -
-
-
- - {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 ? ( - {item.title} - ) : ( -
- -
- )} -
- {/* WB Badge */} -
-
- WB -
-
-
- - {/* Инфо товара */} -
-
-
-
- - {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