From 6f8ec078d1ebffe504c4dc2f91e1de28eba47f96 Mon Sep 17 00:00:00 2001 From: Bivekich Date: Thu, 24 Jul 2025 19:05:15 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20WBWarehouseDashboard:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B5=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D1=83=D0=B5=D0=BC=D1=8B=D0=B5=20=D1=81=D0=BE=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=8F=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=B8=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D1=84=D0=B5=D0=B9=D1=81=D1=8B,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BC=D0=B8=20(=D1=84=D1=83=D0=BB=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D0=BC=D0=B5=D0=BD=D1=82,=20Wildberries,=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=81=D0=BA=D0=BB=D0=B0=D0=B4).=20=D0=9E=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=B8=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=BD=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BE=20=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=80=D0=B0=D1=85.=20=D0=A3=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=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=B2=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=BE=D0=BA.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fulfillment-warehouse-tab.tsx | 292 ++++++++++++++ .../wb-warehouse/my-warehouse-tab.tsx | 210 ++++++++++ .../wb-warehouse/wb-warehouse-dashboard.tsx | 373 ++---------------- .../wildberries-warehouse-tab.tsx | 342 ++++++++++++++++ 4 files changed, 886 insertions(+), 331 deletions(-) create mode 100644 src/components/wb-warehouse/fulfillment-warehouse-tab.tsx create mode 100644 src/components/wb-warehouse/my-warehouse-tab.tsx create mode 100644 src/components/wb-warehouse/wildberries-warehouse-tab.tsx diff --git a/src/components/wb-warehouse/fulfillment-warehouse-tab.tsx b/src/components/wb-warehouse/fulfillment-warehouse-tab.tsx new file mode 100644 index 0000000..5cb3ec3 --- /dev/null +++ b/src/components/wb-warehouse/fulfillment-warehouse-tab.tsx @@ -0,0 +1,292 @@ +"use client" + +import React, { useState } from 'react' +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 { Truck, Package, Clock, AlertCircle, Search, Plus } from 'lucide-react' + +interface FulfillmentOrder { + id: string + orderId: string + customerName: string + items: Array<{ + name: string + quantity: number + sku: string + }> + status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' + priority: 'low' | 'medium' | 'high' + createdAt: string + shippingAddress: string + totalValue: number +} + +interface FulfillmentStats { + totalOrders: number + pendingOrders: number + processingOrders: number + shippedOrders: number + averageProcessingTime: number +} + +export function FulfillmentWarehouseTab() { + const [searchTerm, setSearchTerm] = useState('') + const [selectedStatus, setSelectedStatus] = useState('all') + + const [orders, setOrders] = useState([ + { + id: '1', + orderId: 'FL-2024-001', + customerName: 'Иван Петров', + items: [ + { name: 'Товар A', quantity: 2, sku: 'SKU-001' }, + { name: 'Товар B', quantity: 1, sku: 'SKU-002' } + ], + status: 'pending', + priority: 'high', + createdAt: '2024-01-15T10:30:00', + shippingAddress: 'Москва, ул. Ленина, 10', + totalValue: 3500 + }, + { + id: '2', + orderId: 'FL-2024-002', + customerName: 'Анна Сидорова', + items: [ + { name: 'Товар C', quantity: 1, sku: 'SKU-003' } + ], + status: 'processing', + priority: 'medium', + createdAt: '2024-01-14T15:20:00', + shippingAddress: 'СПб, пр. Невский, 25', + totalValue: 1200 + }, + { + id: '3', + orderId: 'FL-2024-003', + customerName: 'Олег Козлов', + items: [ + { name: 'Товар D', quantity: 3, sku: 'SKU-004' }, + { name: 'Товар E', quantity: 2, sku: 'SKU-005' } + ], + status: 'shipped', + priority: 'low', + createdAt: '2024-01-13T09:15:00', + shippingAddress: 'Екатеринбург, ул. Мира, 45', + totalValue: 5600 + } + ]) + + const stats: FulfillmentStats = { + totalOrders: orders.length, + pendingOrders: orders.filter(o => o.status === 'pending').length, + processingOrders: orders.filter(o => o.status === 'processing').length, + shippedOrders: orders.filter(o => o.status === 'shipped').length, + averageProcessingTime: 2.5 + } + + const filteredOrders = orders.filter(order => { + const matchesSearch = !searchTerm || + order.orderId.toLowerCase().includes(searchTerm.toLowerCase()) || + order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) + + const matchesStatus = selectedStatus === 'all' || order.status === selectedStatus + + return matchesSearch && matchesStatus + }) + + const getStatusColor = (status: string) => { + switch (status) { + case 'pending': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30' + case 'processing': return 'bg-blue-500/20 text-blue-400 border-blue-500/30' + case 'shipped': return 'bg-green-500/20 text-green-400 border-green-500/30' + case 'delivered': return 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' + case 'cancelled': return 'bg-red-500/20 text-red-400 border-red-500/30' + default: return 'bg-gray-500/20 text-gray-400 border-gray-500/30' + } + } + + const getStatusText = (status: string) => { + switch (status) { + case 'pending': return 'Ожидает' + case 'processing': return 'Обрабатывается' + case 'shipped': return 'Отправлен' + case 'delivered': return 'Доставлен' + case 'cancelled': return 'Отменён' + default: return status + } + } + + const getPriorityColor = (priority: string) => { + switch (priority) { + case 'high': return 'text-red-400' + case 'medium': return 'text-yellow-400' + case 'low': return 'text-green-400' + default: return 'text-white/60' + } + } + + const getPriorityText = (priority: string) => { + switch (priority) { + case 'high': return 'Высокий' + case 'medium': return 'Средний' + case 'low': return 'Низкий' + default: return priority + } + } + + return ( +
+ {/* Статистика */} +
+ +
+
+

Всего заказов

+

{stats.totalOrders}

+
+ +
+
+ + +
+
+

Ожидают обработки

+

{stats.pendingOrders}

+
+ +
+
+ + +
+
+

В обработке

+

{stats.processingOrders}

+
+ +
+
+ + +
+
+

Отправлено

+

{stats.shippedOrders}

+
+ +
+
+
+ + {/* Панель управления */} +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-10 bg-white/5 border-white/10 text-white placeholder:text-white/40" + /> +
+ + +
+ + +
+ + {/* Список заказов */} +
+ {filteredOrders.length === 0 ? ( + + +

+ {searchTerm ? 'Заказы не найдены' : 'Нет заказов'} +

+

+ {searchTerm ? 'Попробуйте изменить параметры поиска' : 'Здесь будут отображаться заказы фулфилмент'} +

+
+ ) : ( +
+ {filteredOrders.map((order) => ( + +
+
+

{order.orderId}

+ + {getStatusText(order.status)} + + + {getPriorityText(order.priority)} + +
+
+

{order.totalValue.toLocaleString()} ₽

+

+ {new Date(order.createdAt).toLocaleDateString('ru-RU')} +

+
+
+ +
+
+

Клиент

+

{order.customerName}

+
+ +
+

Товары

+
+ {order.items.map((item, index) => ( +

+ {item.name} × {item.quantity} +

+ ))} +
+
+ +
+

Адрес доставки

+

{order.shippingAddress}

+
+
+ +
+ + {order.status === 'pending' && ( + + )} +
+
+ ))} +
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/src/components/wb-warehouse/my-warehouse-tab.tsx b/src/components/wb-warehouse/my-warehouse-tab.tsx new file mode 100644 index 0000000..f2b3910 --- /dev/null +++ b/src/components/wb-warehouse/my-warehouse-tab.tsx @@ -0,0 +1,210 @@ +"use client" + +import React, { useState } from 'react' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Package, Plus, Search, Warehouse } from 'lucide-react' + +interface MyWarehouseItem { + id: string + sku: string + name: string + category: string + quantity: number + price: number + location: string + status: 'in_stock' | 'low_stock' | 'out_of_stock' + lastUpdated: string +} + +export function MyWarehouseTab() { + const [searchTerm, setSearchTerm] = useState('') + const [items, setItems] = useState([ + { + id: '1', + sku: 'SKU-001', + name: 'Товар 1', + category: 'Электроника', + quantity: 25, + price: 1500, + location: 'A-01-15', + status: 'in_stock', + lastUpdated: '2024-01-15' + }, + { + id: '2', + sku: 'SKU-002', + name: 'Товар 2', + category: 'Одежда', + quantity: 5, + price: 800, + location: 'B-02-08', + status: 'low_stock', + lastUpdated: '2024-01-14' + }, + { + id: '3', + sku: 'SKU-003', + name: 'Товар 3', + category: 'Дом и сад', + quantity: 0, + price: 650, + location: 'C-01-22', + status: 'out_of_stock', + lastUpdated: '2024-01-13' + } + ]) + + const filteredItems = items.filter(item => { + if (!searchTerm) return true + const search = searchTerm.toLowerCase() + return ( + item.name.toLowerCase().includes(search) || + item.sku.toLowerCase().includes(search) || + item.category.toLowerCase().includes(search) + ) + }) + + const getStatusColor = (status: string) => { + switch (status) { + case 'in_stock': return 'text-green-400' + case 'low_stock': return 'text-yellow-400' + case 'out_of_stock': return 'text-red-400' + default: return 'text-white/60' + } + } + + const getStatusText = (status: string) => { + switch (status) { + case 'in_stock': return 'В наличии' + case 'low_stock': return 'Мало' + case 'out_of_stock': return 'Нет в наличии' + default: return 'Неизвестно' + } + } + + const totalItems = items.length + const totalQuantity = items.reduce((sum, item) => sum + item.quantity, 0) + const totalValue = items.reduce((sum, item) => sum + (item.quantity * item.price), 0) + const lowStockItems = items.filter(item => item.status === 'low_stock' || item.status === 'out_of_stock').length + + return ( +
+ {/* Статистика */} +
+ +
+
+

Общее кол-во товаров

+

{totalItems}

+
+ +
+
+ + +
+
+

Общее количество

+

{totalQuantity}

+
+ +
+
+ + +
+
+

Общая стоимость

+

{totalValue.toLocaleString()} ₽

+
+
+
+
+ + +
+
+

Требует внимания

+

{lowStockItems}

+
+
+ +
+
+
+
+ + {/* Панель управления */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10 bg-white/5 border-white/10 text-white placeholder:text-white/40" + /> +
+ +
+ + {/* Список товаров */} +
+ {filteredItems.length === 0 ? ( + + +

+ {searchTerm ? 'Товары не найдены' : 'Ваш склад пуст'} +

+

+ {searchTerm ? 'Попробуйте изменить параметры поиска' : 'Добавьте первый товар на склад'} +

+ {!searchTerm && ( + + )} +
+ ) : ( +
+ {/* Заголовок таблицы */} +
+
SKU
+
Название
+
Категория
+
Количество
+
Цена
+
Локация
+
Статус
+
+ + {/* Строки товаров */} +
+ {filteredItems.map((item) => ( + +
+
{item.sku}
+
{item.name}
+
{item.category}
+
{item.quantity}
+
{item.price.toLocaleString()} ₽
+
{item.location}
+
+ {getStatusText(item.status)} +
+
+
+ ))} +
+
+ )} +
+
+ ) +} \ 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 1c3e0c1..95d1193 100644 --- a/src/components/wb-warehouse/wb-warehouse-dashboard.tsx +++ b/src/components/wb-warehouse/wb-warehouse-dashboard.tsx @@ -1,352 +1,63 @@ "use client" /* eslint-disable @typescript-eslint/no-explicit-any */ -import React, { useState, useEffect } from 'react' +import React, { useState } 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: any[] - mediaFiles: any[] - characteristics: any[] - subjectName: string - description: string -} - -interface WBWarehouse { - id: number - name: string - cargoType: number - deliveryType: number -} +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { WildberriesWarehouseTab } from './wildberries-warehouse-tab' +import { MyWarehouseTab } from './my-warehouse-tab' +import { FulfillmentWarehouseTab } from './fulfillment-warehouse-tab' 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([]) - - // Проверяем настройку 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') - - 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) - - // 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) - ) - }) + const [activeTab, setActiveTab] = useState('fulfillment') return (
+ {/* Табы */} + + + + Склад фулфилмент + + + Склад Wildberries + + + Мой склад + + - {/* Результирующие вкладки */} - - - {/* Аналитика по складам 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) => ( - - ))} -
-
- )} -
+
+ + + + + + + + + + + +
+
diff --git a/src/components/wb-warehouse/wildberries-warehouse-tab.tsx b/src/components/wb-warehouse/wildberries-warehouse-tab.tsx new file mode 100644 index 0000000..1e80f36 --- /dev/null +++ b/src/components/wb-warehouse/wildberries-warehouse-tab.tsx @@ -0,0 +1,342 @@ +"use client" +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import React, { useState, useEffect } from 'react' +import { useAuth } from '@/hooks/useAuth' +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: any[] + mediaFiles: any[] + characteristics: any[] + subjectName: string + description: string +} + +interface WBWarehouse { + id: number + name: string + cargoType: number + deliveryType: number +} + +export function WildberriesWarehouseTab() { + const { user } = useAuth() + + 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([]) + + // Проверяем настройку API ключа + const hasWBApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive + + // Комбинирование карточек с индивидуальными данными аналитики + 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) { + analyticsData.data.regions.forEach((region: any) => { + if (region.offices && region.offices.length > 0) { + 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') + + 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) + + // 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