diff --git a/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx
index 81d0f13..0798b03 100644
--- a/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx
+++ b/src/components/fulfillment-supplies/create-fulfillment-consumables-supply-page.tsx
@@ -244,8 +244,8 @@ export function CreateFulfillmentConsumablesSupplyPage() {
setProductSearchQuery("");
setSearchQuery("");
- // Перенаправляем на страницу поставок фулфилмента
- router.push("/fulfillment-supplies");
+ // Перенаправляем на страницу поставок фулфилмента с активной вкладкой "Наши расходники"
+ router.push("/fulfillment-supplies?tab=detailed-supplies");
} else {
toast.error(
result.data?.createSupplyOrder?.message ||
diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx
index 38aefd5..7faba5a 100644
--- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx
+++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx
@@ -1,6 +1,7 @@
"use client";
-import { useState } from "react";
+import { useState, useEffect } from "react";
+import { useSearchParams, useRouter } from "next/navigation";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Package, Wrench, RotateCcw, Building2 } from "lucide-react";
@@ -13,13 +14,31 @@ import { FulfillmentConsumablesOrdersTab } from "./fulfillment-consumables-order
import { FulfillmentDetailedSuppliesTab } from "./fulfillment-detailed-supplies-tab";
export function FulfillmentSuppliesTab() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
const [activeTab, setActiveTab] = useState("goods");
+ // Проверяем URL параметр при загрузке
+ useEffect(() => {
+ const tabParam = searchParams.get("tab");
+ if (tabParam && ["goods", "detailed-supplies", "consumables", "returns"].includes(tabParam)) {
+ setActiveTab(tabParam);
+ }
+ }, [searchParams]);
+
+ // Обновляем URL при смене вкладки
+ const handleTabChange = (newTab: string) => {
+ setActiveTab(newTab);
+ const currentPath = window.location.pathname;
+ const newUrl = `${currentPath}?tab=${newTab}`;
+ router.replace(newUrl);
+ };
+
return (
diff --git a/src/components/supplies/direct-supply-creation.tsx b/src/components/supplies/direct-supply-creation.tsx
index ea20e33..93963b0 100644
--- a/src/components/supplies/direct-supply-creation.tsx
+++ b/src/components/supplies/direct-supply-creation.tsx
@@ -672,7 +672,7 @@ export function DirectSupplyCreation({
{/* НОВЫЙ БЛОК СОЗДАНИЯ ПОСТАВКИ */}
{/* Первая строка */}
-
+
{/* 1. Модуль выбора даты */}
@@ -700,7 +700,7 @@ export function DirectSupplyCreation({
value={selectedFulfillment}
onValueChange={setSelectedFulfillment}
>
-
+
@@ -745,7 +745,7 @@ export function DirectSupplyCreation({
{/* Вторая строка */}
-
+
{/* 5. Цена товаров */}
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"
+ />
+
+
+
setSelectedStatus(e.target.value)}
+ className="px-3 py-2 bg-white/5 border border-white/10 rounded-md text-white text-sm"
+ >
+ Все статусы
+ Ожидает
+ Обрабатывается
+ Отправлен
+ Доставлен
+ Отменён
+
+
+
+
+
+ Новый заказ
+
+
+
+ {/* Список заказов */}
+
+ {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 в настройках
- window.location.href = '/settings'}
- className="bg-blue-600 hover:bg-blue-700"
- >
- Перейти в настройки
-
-
- ) : 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 в настройках
+ window.location.href = '/settings'}
+ className="bg-blue-600 hover:bg-blue-700"
+ >
+ Перейти в настройки
+
+
+ ) : filteredStocks.length === 0 ? (
+
+
+ Товары не найдены
+ Попробуйте изменить параметры поиска
+
+ ) : (
+
+
+
+ {/* Таблица товаров */}
+
+ {filteredStocks.map((item, index) => (
+
+ ))}
+
+
+ )}
+
+
+ )
+}
\ No newline at end of file