Обновлен компонент WBWarehouseDashboard: удалены неиспользуемые состояния и интерфейсы, добавлены вкладки для управления складами (фулфилмент, Wildberries, мой склад). Оптимизирована логика загрузки данных и отображения информации о товарах. Улучшен интерфейс с использованием новых компонентов для вкладок.
This commit is contained in:
292
src/components/wb-warehouse/fulfillment-warehouse-tab.tsx
Normal file
292
src/components/wb-warehouse/fulfillment-warehouse-tab.tsx
Normal file
@ -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<string>('all')
|
||||
|
||||
const [orders, setOrders] = useState<FulfillmentOrder[]>([
|
||||
{
|
||||
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 (
|
||||
<div className="h-full flex flex-col space-y-6">
|
||||
{/* Статистика */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Всего заказов</p>
|
||||
<p className="text-2xl font-bold text-white">{stats.totalOrders}</p>
|
||||
</div>
|
||||
<Package className="h-8 w-8 text-blue-400" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Ожидают обработки</p>
|
||||
<p className="text-2xl font-bold text-yellow-400">{stats.pendingOrders}</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-yellow-400" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">В обработке</p>
|
||||
<p className="text-2xl font-bold text-blue-400">{stats.processingOrders}</p>
|
||||
</div>
|
||||
<AlertCircle className="h-8 w-8 text-blue-400" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Отправлено</p>
|
||||
<p className="text-2xl font-bold text-green-400">{stats.shippedOrders}</p>
|
||||
</div>
|
||||
<Truck className="h-8 w-8 text-green-400" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Панель управления */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
<div className="flex gap-4 flex-1">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
|
||||
<Input
|
||||
placeholder="Поиск по номеру заказа или клиенту..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 bg-white/5 border-white/10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
className="px-3 py-2 bg-white/5 border border-white/10 rounded-md text-white text-sm"
|
||||
>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="pending">Ожидает</option>
|
||||
<option value="processing">Обрабатывается</option>
|
||||
<option value="shipped">Отправлен</option>
|
||||
<option value="delivered">Доставлен</option>
|
||||
<option value="cancelled">Отменён</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Новый заказ
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Список заказов */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{filteredOrders.length === 0 ? (
|
||||
<Card className="glass-card border-white/10 p-8 text-center">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
{searchTerm ? 'Заказы не найдены' : 'Нет заказов'}
|
||||
</h3>
|
||||
<p className="text-white/60 mb-4">
|
||||
{searchTerm ? 'Попробуйте изменить параметры поиска' : 'Здесь будут отображаться заказы фулфилмент'}
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="overflow-y-auto pr-2 max-h-full space-y-3">
|
||||
{filteredOrders.map((order) => (
|
||||
<Card key={order.id} className="glass-card border-white/10 p-4 hover:bg-white/5 transition-colors">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-white font-semibold">{order.orderId}</h3>
|
||||
<Badge className={getStatusColor(order.status)}>
|
||||
{getStatusText(order.status)}
|
||||
</Badge>
|
||||
<span className={`text-xs font-medium ${getPriorityColor(order.priority)}`}>
|
||||
{getPriorityText(order.priority)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white font-bold">{order.totalValue.toLocaleString()} ₽</p>
|
||||
<p className="text-white/60 text-xs">
|
||||
{new Date(order.createdAt).toLocaleDateString('ru-RU')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-white/60 text-xs mb-1">Клиент</p>
|
||||
<p className="text-white text-sm">{order.customerName}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-white/60 text-xs mb-1">Товары</p>
|
||||
<div className="space-y-1">
|
||||
{order.items.map((item, index) => (
|
||||
<p key={index} className="text-white text-xs">
|
||||
{item.name} × {item.quantity}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-white/60 text-xs mb-1">Адрес доставки</p>
|
||||
<p className="text-white text-xs">{order.shippingAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button size="sm" variant="outline" className="border-white/10 text-white/60 hover:bg-white/5">
|
||||
Подробнее
|
||||
</Button>
|
||||
{order.status === 'pending' && (
|
||||
<Button size="sm" className="bg-blue-600 hover:bg-blue-700">
|
||||
Начать обработку
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
210
src/components/wb-warehouse/my-warehouse-tab.tsx
Normal file
210
src/components/wb-warehouse/my-warehouse-tab.tsx
Normal file
@ -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<MyWarehouseItem[]>([
|
||||
{
|
||||
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 (
|
||||
<div className="h-full flex flex-col space-y-6">
|
||||
{/* Статистика */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Общее кол-во товаров</p>
|
||||
<p className="text-2xl font-bold text-white">{totalItems}</p>
|
||||
</div>
|
||||
<Package className="h-8 w-8 text-blue-400" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Общее количество</p>
|
||||
<p className="text-2xl font-bold text-white">{totalQuantity}</p>
|
||||
</div>
|
||||
<Warehouse className="h-8 w-8 text-green-400" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Общая стоимость</p>
|
||||
<p className="text-2xl font-bold text-white">{totalValue.toLocaleString()} ₽</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-purple-500 rounded-lg flex items-center justify-center text-white font-bold">₽</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="glass-card border-white/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/60 text-sm">Требует внимания</p>
|
||||
<p className="text-2xl font-bold text-yellow-400">{lowStockItems}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-yellow-500 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white text-lg">⚠</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Панель управления */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
|
||||
<Input
|
||||
placeholder="Поиск товаров..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 bg-white/5 border-white/10 text-white placeholder:text-white/40"
|
||||
/>
|
||||
</div>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Добавить товар
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Список товаров */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{filteredItems.length === 0 ? (
|
||||
<Card className="glass-card border-white/10 p-8 text-center">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
{searchTerm ? 'Товары не найдены' : 'Ваш склад пуст'}
|
||||
</h3>
|
||||
<p className="text-white/60 mb-4">
|
||||
{searchTerm ? 'Попробуйте изменить параметры поиска' : 'Добавьте первый товар на склад'}
|
||||
</p>
|
||||
{!searchTerm && (
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Добавить товар
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<div className="overflow-y-auto pr-2 max-h-full">
|
||||
{/* Заголовок таблицы */}
|
||||
<div className="grid grid-cols-7 gap-4 p-4 border-b border-white/10 text-white/60 text-sm font-medium">
|
||||
<div>SKU</div>
|
||||
<div>Название</div>
|
||||
<div>Категория</div>
|
||||
<div>Количество</div>
|
||||
<div>Цена</div>
|
||||
<div>Локация</div>
|
||||
<div>Статус</div>
|
||||
</div>
|
||||
|
||||
{/* Строки товаров */}
|
||||
<div className="space-y-1">
|
||||
{filteredItems.map((item) => (
|
||||
<Card key={item.id} className="glass-card border-white/10 p-4 hover:bg-white/5 transition-colors">
|
||||
<div className="grid grid-cols-7 gap-4 items-center">
|
||||
<div className="text-white font-mono text-sm">{item.sku}</div>
|
||||
<div className="text-white font-medium">{item.name}</div>
|
||||
<div className="text-white/60 text-sm">{item.category}</div>
|
||||
<div className="text-white font-bold">{item.quantity}</div>
|
||||
<div className="text-white">{item.price.toLocaleString()} ₽</div>
|
||||
<div className="text-white/60 text-sm font-mono">{item.location}</div>
|
||||
<div className={`text-sm font-medium ${getStatusColor(item.status)}`}>
|
||||
{getStatusText(item.status)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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<WBStock[]>([])
|
||||
const [warehouses, setWarehouses] = useState<WBWarehouse[]>([])
|
||||
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<any[]>([])
|
||||
|
||||
// Проверяем настройку 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<number, WBStock>()
|
||||
|
||||
// Создаем карту аналитических данных для быстрого поиска
|
||||
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<number, WBWarehouse>()
|
||||
|
||||
stocksData.forEach(stock => {
|
||||
stock.stocks.forEach(stockInfo => {
|
||||
if (!warehousesMap.has(stockInfo.warehouseId)) {
|
||||
warehousesMap.set(stockInfo.warehouseId, {
|
||||
id: stockInfo.warehouseId,
|
||||
name: stockInfo.warehouseName,
|
||||
cargoType: 1,
|
||||
deliveryType: 1
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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<string, string>
|
||||
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 (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
|
||||
<div className="h-full w-full flex flex-col">
|
||||
|
||||
{/* Результирующие вкладки */}
|
||||
<StatsCards
|
||||
totalProducts={totalProducts}
|
||||
totalStocks={totalStocks}
|
||||
totalReserved={totalReserved}
|
||||
totalFromClient={totalFromClient}
|
||||
activeWarehouses={activeWarehouses}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{/* Аналитика по складам WB */}
|
||||
{analyticsData.length > 0 && (
|
||||
<Card className="glass-card border-white/10 p-4 mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
|
||||
<TrendingUp className="h-5 w-5 mr-2 text-blue-400" />
|
||||
Движение товаров по складам WB
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{analyticsData.map((warehouse) => (
|
||||
<Card key={warehouse.warehouseId} className="bg-white/5 border-white/10 p-3">
|
||||
<div className="text-sm font-medium text-white mb-2">{warehouse.warehouseName}</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-white/60">К клиенту:</span>
|
||||
<span className="text-green-400 font-medium">{warehouse.toClient}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-white/60">От клиента:</span>
|
||||
<span className="text-orange-400 font-medium">{warehouse.fromClient}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Поиск */}
|
||||
<SearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
|
||||
{/* Список товаров */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="overflow-y-auto pr-2 max-h-full">
|
||||
<TableHeader />
|
||||
<LoadingSkeleton />
|
||||
</div>
|
||||
) : !hasWBApiKey ? (
|
||||
<Card className="glass-card border-white/10 p-8 text-center">
|
||||
<Package className="h-12 w-12 text-blue-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">Настройте API Wildberries</h3>
|
||||
<p className="text-white/60 mb-4">Для просмотра остатков добавьте API ключ Wildberries в настройках</p>
|
||||
<Button
|
||||
onClick={() => window.location.href = '/settings'}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
{/* Табы */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
|
||||
<TabsList className="grid grid-cols-3 w-full max-w-md mb-6 bg-white/5 border border-white/10">
|
||||
<TabsTrigger
|
||||
value="fulfillment"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white text-white/60"
|
||||
>
|
||||
Перейти в настройки
|
||||
</Button>
|
||||
</Card>
|
||||
) : filteredStocks.length === 0 ? (
|
||||
<Card className="glass-card border-white/10 p-8 text-center">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">Товары не найдены</h3>
|
||||
<p className="text-white/60">Попробуйте изменить параметры поиска</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="overflow-y-auto pr-2 max-h-full">
|
||||
<TableHeader />
|
||||
Склад фулфилмент
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="wildberries"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white text-white/60"
|
||||
>
|
||||
Склад Wildberries
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="my-warehouse"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white text-white/60"
|
||||
>
|
||||
Мой склад
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Таблица товаров */}
|
||||
<div className="space-y-1">
|
||||
{filteredStocks.map((item, index) => (
|
||||
<StockTableRow key={`${item.nmId}-${index}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<TabsContent value="fulfillment" className="h-full mt-0">
|
||||
<FulfillmentWarehouseTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="wildberries" className="h-full mt-0">
|
||||
<WildberriesWarehouseTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="my-warehouse" className="h-full mt-0">
|
||||
<MyWarehouseTab />
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
342
src/components/wb-warehouse/wildberries-warehouse-tab.tsx
Normal file
342
src/components/wb-warehouse/wildberries-warehouse-tab.tsx
Normal file
@ -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<WBStock[]>([])
|
||||
const [warehouses, setWarehouses] = useState<WBWarehouse[]>([])
|
||||
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<any[]>([])
|
||||
|
||||
// Проверяем настройку API ключа
|
||||
const hasWBApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive
|
||||
|
||||
// Комбинирование карточек с индивидуальными данными аналитики
|
||||
const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
|
||||
const stocksMap = new Map<number, WBStock>()
|
||||
|
||||
// Создаем карту аналитических данных для быстрого поиска
|
||||
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<number, WBWarehouse>()
|
||||
|
||||
stocksData.forEach(stock => {
|
||||
stock.stocks.forEach(stockInfo => {
|
||||
if (!warehousesMap.has(stockInfo.warehouseId)) {
|
||||
warehousesMap.set(stockInfo.warehouseId, {
|
||||
id: stockInfo.warehouseId,
|
||||
name: stockInfo.warehouseName,
|
||||
cargoType: 1,
|
||||
deliveryType: 1
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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<string, string>
|
||||
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 (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Статистика */}
|
||||
<StatsCards
|
||||
totalProducts={totalProducts}
|
||||
totalStocks={totalStocks}
|
||||
totalReserved={totalReserved}
|
||||
totalFromClient={totalFromClient}
|
||||
activeWarehouses={activeWarehouses}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{/* Аналитика по складам WB */}
|
||||
{analyticsData.length > 0 && (
|
||||
<Card className="glass-card border-white/10 p-4 mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
|
||||
<TrendingUp className="h-5 w-5 mr-2 text-blue-400" />
|
||||
Движение товаров по складам WB
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{analyticsData.map((warehouse) => (
|
||||
<Card key={warehouse.warehouseId} className="bg-white/5 border-white/10 p-3">
|
||||
<div className="text-sm font-medium text-white mb-2">{warehouse.warehouseName}</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-white/60">К клиенту:</span>
|
||||
<span className="text-green-400 font-medium">{warehouse.toClient}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-white/60">От клиента:</span>
|
||||
<span className="text-orange-400 font-medium">{warehouse.fromClient}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Поиск */}
|
||||
<SearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
|
||||
{/* Список товаров */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="overflow-y-auto pr-2 max-h-full">
|
||||
<TableHeader />
|
||||
<LoadingSkeleton />
|
||||
</div>
|
||||
) : !hasWBApiKey ? (
|
||||
<Card className="glass-card border-white/10 p-8 text-center">
|
||||
<Package className="h-12 w-12 text-blue-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">Настройте API Wildberries</h3>
|
||||
<p className="text-white/60 mb-4">Для просмотра остатков добавьте API ключ Wildberries в настройках</p>
|
||||
<Button
|
||||
onClick={() => window.location.href = '/settings'}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Перейти в настройки
|
||||
</Button>
|
||||
</Card>
|
||||
) : filteredStocks.length === 0 ? (
|
||||
<Card className="glass-card border-white/10 p-8 text-center">
|
||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">Товары не найдены</h3>
|
||||
<p className="text-white/60">Попробуйте изменить параметры поиска</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="overflow-y-auto pr-2 max-h-full">
|
||||
<TableHeader />
|
||||
|
||||
{/* Таблица товаров */}
|
||||
<div className="space-y-1">
|
||||
{filteredStocks.map((item, index) => (
|
||||
<StockTableRow key={`${item.nmId}-${index}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user