Merge branch 'main' of https://gittea.biveki.ru/Sfera/sfera
This commit is contained in:
@ -106,6 +106,10 @@ export function Sidebar() {
|
||||
router.push("/warehouse");
|
||||
};
|
||||
|
||||
const handleWBWarehouseClick = () => {
|
||||
router.push("/wb-warehouse");
|
||||
};
|
||||
|
||||
const handleEmployeesClick = () => {
|
||||
router.push("/employees");
|
||||
};
|
||||
@ -151,6 +155,7 @@ export function Sidebar() {
|
||||
const isMessengerActive = pathname.startsWith("/messenger");
|
||||
const isServicesActive = pathname.startsWith("/services");
|
||||
const isWarehouseActive = pathname.startsWith("/warehouse");
|
||||
const isWBWarehouseActive = pathname.startsWith("/wb-warehouse");
|
||||
const isFulfillmentWarehouseActive = pathname.startsWith(
|
||||
"/fulfillment-warehouse"
|
||||
);
|
||||
@ -419,6 +424,25 @@ export function Sidebar() {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Склад ВБ - для селлеров */}
|
||||
{user?.organization?.type === "SELLER" && (
|
||||
<Button
|
||||
variant={isWBWarehouseActive ? "secondary" : "ghost"}
|
||||
className={`w-full ${
|
||||
isCollapsed ? "justify-center px-2 h-9" : "justify-start h-10"
|
||||
} text-left transition-all duration-200 text-xs ${
|
||||
isWBWarehouseActive
|
||||
? "bg-white/20 text-white hover:bg-white/30"
|
||||
: "text-white/80 hover:bg-white/10 hover:text-white"
|
||||
} cursor-pointer`}
|
||||
onClick={handleWBWarehouseClick}
|
||||
title={isCollapsed ? "Склад ВБ" : ""}
|
||||
>
|
||||
<Warehouse className="h-4 w-4 flex-shrink-0" />
|
||||
{!isCollapsed && <span className="ml-3">Склад ВБ</span>}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Статистика - для селлеров */}
|
||||
{user?.organization?.type === "SELLER" && (
|
||||
<Button
|
||||
|
699
src/components/wb-warehouse/wb-warehouse-dashboard.tsx
Normal file
699
src/components/wb-warehouse/wb-warehouse-dashboard.tsx
Normal file
@ -0,0 +1,699 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { WildberriesService } from '@/services/wildberries-service'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Search,
|
||||
Package,
|
||||
Warehouse,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
RefreshCw,
|
||||
Filter,
|
||||
MapPin
|
||||
} from 'lucide-react'
|
||||
|
||||
interface WBStock {
|
||||
nmId: number
|
||||
vendorCode: string
|
||||
title: string
|
||||
brand: string
|
||||
price: number
|
||||
stocks: Array<{
|
||||
warehouseId: number
|
||||
warehouseName: string
|
||||
quantity: number
|
||||
quantityFull: number
|
||||
inWayToClient: number
|
||||
inWayFromClient: number
|
||||
}>
|
||||
totalQuantity: number
|
||||
totalReserved: number
|
||||
photos?: Array<{
|
||||
big?: string
|
||||
c246x328?: string
|
||||
c516x688?: string
|
||||
square?: string
|
||||
tm?: string
|
||||
}>
|
||||
mediaFiles?: string[]
|
||||
characteristics?: Array<{
|
||||
id: number
|
||||
name: string
|
||||
value: string[] | string
|
||||
}>
|
||||
subjectName?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface WBWarehouse {
|
||||
id: number
|
||||
name: string
|
||||
cargoType: number
|
||||
deliveryType: number
|
||||
}
|
||||
|
||||
export function WBWarehouseDashboard() {
|
||||
const { user } = useAuth()
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
|
||||
const [stocks, setStocks] = useState<WBStock[]>([])
|
||||
const [warehouses, setWarehouses] = useState<WBWarehouse[]>([])
|
||||
const [analyticsData, setAnalyticsData] = useState<Array<{warehouseId: number; warehouseName: string; toClient: number; fromClient: number}>>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedWarehouse, setSelectedWarehouse] = useState<string>('all')
|
||||
|
||||
// Статистика
|
||||
const [totalProducts, setTotalProducts] = useState(0)
|
||||
const [totalStocks, setTotalStocks] = useState(0)
|
||||
const [totalReserved, setTotalReserved] = useState(0)
|
||||
const [activeWarehouses, setActiveWarehouses] = useState(0)
|
||||
|
||||
// Загрузка данных
|
||||
const loadWarehouseData = async (showToast = false) => {
|
||||
const isInitialLoad = loading
|
||||
if (!isInitialLoad) setRefreshing(true)
|
||||
|
||||
try {
|
||||
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
|
||||
|
||||
if (!wbApiKey?.isActive) {
|
||||
toast.error('API ключ Wildberries не настроен')
|
||||
return
|
||||
}
|
||||
|
||||
const validationData = wbApiKey.validationData as Record<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)
|
||||
|
||||
console.log('WB Warehouse: Starting data load with Analytics API...')
|
||||
|
||||
// Сначала получаем карточки товаров для передачи в Analytics API
|
||||
console.log('WB Warehouse: Getting cards for Analytics API...')
|
||||
const cards = await WildberriesService.getAllCards(apiToken).catch(() => [])
|
||||
const nmIds = cards.map(card => card.nmID).filter(id => id > 0)
|
||||
console.log('WB Warehouse: Found cards for Analytics API:', nmIds)
|
||||
|
||||
// Загружаем склады, основные данные и Analytics API для движения товаров
|
||||
const [warehousesData, stocksData, rawAnalyticsData] = await Promise.all([
|
||||
wbService.getWarehouses().catch((error) => {
|
||||
console.error('WB Warehouse: Error loading warehouses:', error)
|
||||
return []
|
||||
}),
|
||||
wbService.getStocks().catch((error) => {
|
||||
console.error('WB Warehouse: Error loading stocks:', error)
|
||||
return []
|
||||
}),
|
||||
wbService.getStocksReportByOffices({
|
||||
nmIds: nmIds.length > 0 ? nmIds : undefined, // Передаем ID твоих товаров
|
||||
stockType: '' // все склады - покажем все данные
|
||||
}).catch((error) => {
|
||||
console.error('WB Warehouse: Error loading analytics data:', error)
|
||||
return []
|
||||
})
|
||||
])
|
||||
|
||||
console.log('WB Warehouse: Warehouses loaded:', warehousesData.length)
|
||||
console.log('WB Warehouse: Basic stocks loaded:', stocksData.length)
|
||||
console.log('WB Warehouse: Analytics data loaded:', rawAnalyticsData.length)
|
||||
|
||||
setWarehouses(warehousesData)
|
||||
|
||||
// Analytics API создает записи с другой структурой - изучаем что пришло
|
||||
console.log('WB Warehouse: Raw analytics data structure:', rawAnalyticsData)
|
||||
console.log('WB Warehouse: Sample analytics item:', rawAnalyticsData[0])
|
||||
|
||||
// Отключаем общую аналитику - будем показывать детализацию по товарам в карточках
|
||||
setAnalyticsData([])
|
||||
|
||||
// Объединяем основные данные со склады и данные Analytics API по складам WB
|
||||
const combinedStocks = [...stocksData, ...rawAnalyticsData]
|
||||
const processedStocks = processStocksData(combinedStocks, warehousesData, rawAnalyticsData)
|
||||
setStocks(processedStocks)
|
||||
|
||||
// Обновляем статистику
|
||||
updateStatistics(processedStocks, warehousesData)
|
||||
|
||||
if (showToast) {
|
||||
toast.success('Данные обновлены')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных склада:', error)
|
||||
toast.error('Ошибка загрузки данных склада')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка данных остатков с дополнением данными из Analytics API
|
||||
const processStocksData = (stocksData: unknown[], warehousesData: WBWarehouse[], analyticsData: WBStock[] = []): WBStock[] => {
|
||||
const stocksMap = new Map<number, WBStock>()
|
||||
|
||||
// Создаем карту данных Analytics API по складам для быстрого поиска
|
||||
const analyticsMap = new Map<number, { toClientCount: number, fromClientCount: number }>()
|
||||
analyticsData.forEach(item => {
|
||||
item.stocks.forEach(stock => {
|
||||
analyticsMap.set(stock.warehouseId, {
|
||||
toClientCount: stock.inWayToClient,
|
||||
fromClientCount: stock.inWayFromClient
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
console.log('WB Warehouse: Analytics map created with', analyticsMap.size, 'warehouse entries')
|
||||
|
||||
stocksData.forEach((stockItem: unknown) => {
|
||||
const stock = stockItem as Record<string, unknown>
|
||||
const nmId = Number(stock.nmId) || 0
|
||||
|
||||
if (!stocksMap.has(nmId)) {
|
||||
console.log(`WB Warehouse: Processing stock for nmId ${nmId}`)
|
||||
console.log(`WB Warehouse: Stock item:`, stock)
|
||||
|
||||
stocksMap.set(nmId, {
|
||||
nmId,
|
||||
vendorCode: String(stock.vendorCode || stock.supplierArticle || ''),
|
||||
title: String(stock.title || stock.subject || `Товар ${nmId}`),
|
||||
brand: String(stock.brand || ''),
|
||||
price: Number(stock.price || stock.Price) || 0,
|
||||
stocks: [],
|
||||
totalQuantity: 0,
|
||||
totalReserved: 0,
|
||||
photos: Array.isArray(stock.photos) ? stock.photos as Array<{big?: string; c246x328?: string; c516x688?: string; square?: string; tm?: string}> : [],
|
||||
mediaFiles: Array.isArray(stock.mediaFiles) ? stock.mediaFiles as string[] : [],
|
||||
characteristics: Array.isArray(stock.characteristics) ? stock.characteristics as Array<{id: number; name: string; value: string[] | string}> : [],
|
||||
subjectName: String(stock.subjectName || stock.subject || ''),
|
||||
description: String(stock.description || '')
|
||||
})
|
||||
}
|
||||
|
||||
const item = stocksMap.get(nmId)!
|
||||
|
||||
// Для Analytics API данных берем warehouseId из первого stock в массиве stocks
|
||||
let warehouseId = Number(stock.warehouseId || stock.warehouse) || 0
|
||||
let warehouseName = String(stock.warehouseName || '')
|
||||
|
||||
// Если это данные Analytics API (есть массив stocks)
|
||||
if (Array.isArray(stock.stocks) && stock.stocks.length > 0) {
|
||||
const firstStock = stock.stocks[0]
|
||||
warehouseId = Number(firstStock.warehouseId) || 0
|
||||
warehouseName = String(firstStock.warehouseName || `Склад ${warehouseId}`)
|
||||
console.log(`WB Warehouse: Analytics stock - warehouseId: ${warehouseId}, name: ${warehouseName}`)
|
||||
} else {
|
||||
// Обычные данные
|
||||
warehouseName = warehouseName || warehousesData.find(w => w.id === warehouseId)?.name || `Склад ${warehouseId}`
|
||||
}
|
||||
|
||||
let quantity = Number(stock.quantity) || 0
|
||||
let quantityFull = Number(stock.quantityFull) || 0
|
||||
let inWayToClient = 0
|
||||
let inWayFromClient = 0
|
||||
|
||||
// Если это данные Analytics API
|
||||
if (Array.isArray(stock.stocks) && stock.stocks.length > 0) {
|
||||
const firstStock = stock.stocks[0]
|
||||
quantity = Number(firstStock.quantity) || 0
|
||||
quantityFull = Number(firstStock.quantityFull) || 0
|
||||
inWayToClient = Number(firstStock.inWayToClient) || 0
|
||||
inWayFromClient = Number(firstStock.inWayFromClient) || 0
|
||||
} else {
|
||||
// Обычные данные - используем Analytics API если доступны
|
||||
const analyticsInfo = analyticsMap.get(warehouseId)
|
||||
inWayToClient = analyticsInfo?.toClientCount ?? (Number(stock.inWayToClient) || 0)
|
||||
inWayFromClient = analyticsInfo?.fromClientCount ?? (Number(stock.inWayFromClient) || 0)
|
||||
}
|
||||
|
||||
const hasAnalytics = Array.isArray(stock.stocks) && stock.stocks.length > 0
|
||||
console.log(`WB Warehouse: Warehouse ${warehouseId} - Analytics: ${hasAnalytics ? 'YES' : 'NO'}, toClient: ${inWayToClient}, fromClient: ${inWayFromClient}`)
|
||||
|
||||
const warehouseStock = {
|
||||
warehouseId,
|
||||
warehouseName,
|
||||
quantity,
|
||||
quantityFull,
|
||||
inWayToClient,
|
||||
inWayFromClient
|
||||
}
|
||||
|
||||
item.stocks.push(warehouseStock)
|
||||
item.totalQuantity += quantity
|
||||
item.totalReserved += inWayToClient
|
||||
})
|
||||
|
||||
return Array.from(stocksMap.values()).sort((a, b) => b.totalQuantity - a.totalQuantity)
|
||||
}
|
||||
|
||||
// Обновление статистики
|
||||
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 warehousesWithStock = new Set(
|
||||
stocksData.flatMap(item => item.stocks.map(s => s.warehouseId))
|
||||
)
|
||||
setActiveWarehouses(warehousesWithStock.size)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Фильтрация товаров (показываем все товары, включая с нулевыми остатками)
|
||||
const filteredStocks = stocks.filter(item => {
|
||||
const matchesSearch = searchTerm === '' ||
|
||||
item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.brand.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
||||
const matchesWarehouse = selectedWarehouse === 'all' ||
|
||||
item.stocks.some(s => s.warehouseId.toString() === selectedWarehouse)
|
||||
|
||||
return matchesSearch && matchesWarehouse
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.organization?.type === 'SELLER') {
|
||||
loadWarehouseData()
|
||||
}
|
||||
}, [user])
|
||||
|
||||
// Проверяем настройку API ключа
|
||||
const hasWBApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* Заголовок */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Склад Wildberries</h1>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={() => loadWarehouseData(true)}
|
||||
disabled={refreshing}
|
||||
className="glass-button text-white"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
Обновить
|
||||
</Button>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Статистика */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<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>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{loading ? <Skeleton className="h-6 w-16 bg-white/10" /> : totalProducts.toLocaleString()}
|
||||
</div>
|
||||
</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>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{loading ? <Skeleton className="h-6 w-16 bg-white/10" /> : totalStocks.toLocaleString()}
|
||||
</div>
|
||||
</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>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{loading ? <Skeleton className="h-6 w-16 bg-white/10" /> : totalReserved.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<TrendingUp className="h-8 w-8 text-orange-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>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{loading ? <Skeleton className="h-6 w-16 bg-white/10" /> : activeWarehouses}
|
||||
</div>
|
||||
</div>
|
||||
<MapPin className="h-8 w-8 text-purple-400" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Аналитика по складам 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>
|
||||
)}
|
||||
|
||||
{/* Фильтры */}
|
||||
<Card className="glass-card border-white/10 p-4 mb-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<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="glass-input text-white placeholder:text-white/40 pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full sm:w-64">
|
||||
<select
|
||||
value={selectedWarehouse}
|
||||
onChange={(e) => setSelectedWarehouse(e.target.value)}
|
||||
className="w-full h-10 px-3 rounded-lg bg-white/5 border border-white/10 text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
>
|
||||
<option value="all">Все склады</option>
|
||||
{warehouses.map(warehouse => (
|
||||
<option key={warehouse.id} value={warehouse.id.toString()}>
|
||||
{warehouse.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Список товаров */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Card key={i} className="glass-card border-white/10 p-4">
|
||||
<Skeleton className="h-20 w-full bg-white/10" />
|
||||
</Card>
|
||||
))}
|
||||
</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-xl font-semibold text-white mb-2">Настройте API ключ Wildberries</h3>
|
||||
<p className="text-white/60 mb-4">
|
||||
Для просмотра остатков товаров на складах WB необходимо добавить API ключ
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => window.location.href = '/settings'}
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white"
|
||||
>
|
||||
Перейти в настройки
|
||||
</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" />
|
||||
<p className="text-white/60">
|
||||
{searchTerm || selectedWarehouse !== 'all'
|
||||
? 'Товары не найдены по заданным фильтрам'
|
||||
: 'Нет карточек товаров в WB'
|
||||
}
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4 overflow-y-auto pr-2 max-h-full">
|
||||
{filteredStocks.map((item, index) => (
|
||||
<StockCard key={`${item.nmId}-${index}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент карточки товара
|
||||
function StockCard({ item }: { item: WBStock }) {
|
||||
// Получение изображений карточки через WildberriesService
|
||||
const getCardImages = (item: WBStock): string[] => {
|
||||
console.log(`WB Warehouse: Getting images for card ${item.nmId}`)
|
||||
console.log(`WB Warehouse: Photos:`, item.photos)
|
||||
console.log(`WB Warehouse: MediaFiles:`, item.mediaFiles)
|
||||
|
||||
// Если есть photos в формате WB API
|
||||
if (item.photos && item.photos.length > 0) {
|
||||
const urls = item.photos
|
||||
.map(photo => photo.c246x328 || photo.c516x688 || photo.big)
|
||||
.filter((url): url is string => Boolean(url))
|
||||
console.log(`WB Warehouse: URLs from photos:`, urls)
|
||||
return urls
|
||||
}
|
||||
|
||||
// Проверяем mediaFiles (как в создании поставки)
|
||||
if (item.mediaFiles && item.mediaFiles.length > 0) {
|
||||
console.log(`WB Warehouse: URLs from mediaFiles:`, item.mediaFiles)
|
||||
return item.mediaFiles
|
||||
}
|
||||
|
||||
// Fallback - генерируем URL по стандартной схеме WB
|
||||
const vol = Math.floor(item.nmId / 100000)
|
||||
const part = Math.floor(item.nmId / 1000)
|
||||
const fallbackUrl = `https://basket-${String(vol).padStart(2, '0')}.wbbasket.ru/vol${vol}/part${part}/${item.nmId}/images/c246x328/1.webp`
|
||||
console.log(`WB Warehouse: Using fallback URL:`, fallbackUrl)
|
||||
return [fallbackUrl]
|
||||
}
|
||||
|
||||
const getStockStatus = (quantity: number) => {
|
||||
if (quantity === 0) return { color: 'bg-red-500/20 text-red-400 border-red-500/30', label: 'Нет в наличии' }
|
||||
if (quantity < 10) return { color: 'bg-orange-500/20 text-orange-400 border-orange-500/30', label: 'Мало' }
|
||||
return { color: 'bg-green-500/20 text-green-400 border-green-500/30', label: 'В наличии' }
|
||||
}
|
||||
|
||||
const stockStatus = getStockStatus(item.totalQuantity)
|
||||
// Получаем изображения из данных карточки WB
|
||||
const images = getCardImages(item)
|
||||
const mainImage = images[0] || null
|
||||
|
||||
return (
|
||||
<Card className="glass-card border-white/10 overflow-hidden hover:border-white/20 transition-all duration-300">
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Изображение товара */}
|
||||
<div className="w-20 h-20 rounded-lg overflow-hidden bg-white/5 flex-shrink-0 relative group">
|
||||
{mainImage ? (
|
||||
<img
|
||||
src={mainImage}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Package className="h-8 w-8 text-white/40" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Индикатор WB */}
|
||||
<div className="absolute top-1 right-1">
|
||||
<Badge className="bg-blue-500/90 text-white border-0 text-xs px-1.5 py-0.5">
|
||||
WB
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Информация о товаре */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Заголовок и бренд */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border-blue-500/30 text-xs font-medium">
|
||||
{item.brand || 'Без бренда'}
|
||||
</Badge>
|
||||
<span className="text-white/40 text-xs">№{item.nmId}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-white font-semibold text-sm mb-2 line-clamp-2 leading-tight">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
{/* Артикул */}
|
||||
<div className="text-white/60 text-xs mb-2">
|
||||
Артикул: <span className="text-white/80 font-mono">{item.vendorCode}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge className={`${stockStatus.color} border text-xs flex-shrink-0`}>
|
||||
{stockStatus.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Общая статистика */}
|
||||
<div className="grid grid-cols-2 gap-3 p-3 bg-white/5 rounded-lg">
|
||||
<div className="text-center">
|
||||
<p className="text-white text-lg font-bold">{item.totalQuantity.toLocaleString()}</p>
|
||||
<p className="text-white/60 text-xs">Доступно</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-purple-400 text-lg font-bold">{item.stocks.length}</p>
|
||||
<p className="text-white/60 text-xs">Складов</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Статистика по движению товаров */}
|
||||
{(item.stocks.some(s => s.inWayToClient > 0) || item.stocks.some(s => s.inWayFromClient > 0)) && (
|
||||
<div className="grid grid-cols-2 gap-3 p-3 bg-blue-500/10 rounded-lg border border-blue-500/20">
|
||||
<div className="text-center">
|
||||
<p className="text-blue-400 text-lg font-bold">
|
||||
{item.stocks.reduce((sum, s) => sum + s.inWayToClient, 0).toLocaleString()}
|
||||
</p>
|
||||
<p className="text-white/60 text-xs">К клиенту</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-orange-400 text-lg font-bold">
|
||||
{item.stocks.reduce((sum, s) => sum + s.inWayFromClient, 0).toLocaleString()}
|
||||
</p>
|
||||
<p className="text-white/60 text-xs">От клиента</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Остатки по складам */}
|
||||
<div>
|
||||
<h4 className="text-white font-medium text-sm mb-3">Остатки по складам:</h4>
|
||||
<div className="space-y-2">
|
||||
{item.stocks.map((stock, stockIndex) => (
|
||||
<div key={`${stock.warehouseId}-${stockIndex}`} className="flex items-center justify-between py-2 px-3 rounded-lg bg-white/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-white text-sm font-medium">{stock.warehouseName}</p>
|
||||
<p className="text-white/60 text-xs">ID: {stock.warehouseId}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
||||
<div className="text-center">
|
||||
<p className="text-green-400 font-bold text-lg">{stock.quantity}</p>
|
||||
<p className="text-white/60 text-xs">Доступно</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className={`font-bold text-lg ${stock.inWayToClient > 0 ? 'text-blue-400' : 'text-white/30'}`}>
|
||||
{stock.inWayToClient}
|
||||
</p>
|
||||
<p className="text-white/60 text-xs">К клиенту</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className={`font-bold text-lg ${stock.inWayFromClient > 0 ? 'text-orange-400' : 'text-white/30'}`}>
|
||||
{stock.inWayFromClient}
|
||||
</p>
|
||||
<p className="text-white/60 text-xs">От клиента</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Основная информация о товаре */}
|
||||
{(item.subjectName || item.description) && (
|
||||
<div>
|
||||
<h4 className="text-white font-medium text-sm mb-3">Информация о товаре:</h4>
|
||||
<div className="space-y-2 p-3 rounded-lg bg-white/5">
|
||||
{item.subjectName && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-white/60 text-sm">Категория:</span>
|
||||
<span className="text-white text-sm font-medium">{item.subjectName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.description && (
|
||||
<div>
|
||||
<span className="text-white/60 text-sm block mb-1">Описание:</span>
|
||||
<p className="text-white text-sm leading-relaxed">{item.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Характеристики товара */}
|
||||
{item.characteristics && item.characteristics.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-white font-medium text-sm mb-3">Характеристики:</h4>
|
||||
<div className="space-y-1">
|
||||
{item.characteristics.map((characteristic, charIndex) => (
|
||||
<div key={`${characteristic.id}-${charIndex}`} className="flex justify-between items-start py-2 px-3 rounded-lg bg-white/5">
|
||||
<span className="text-white/60 text-sm font-medium min-w-[100px]">
|
||||
{characteristic.name}:
|
||||
</span>
|
||||
<div className="flex-1 text-right">
|
||||
{Array.isArray(characteristic.value) ? (
|
||||
characteristic.value.map((val, valIndex) => (
|
||||
<span key={valIndex} className="text-white text-sm">
|
||||
{val}
|
||||
{valIndex < characteristic.value.length - 1 && ', '}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-white text-sm">
|
||||
{String(characteristic.value)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user