Добавлены модели для внешней рекламы и кэша склада WB в схему Prisma. Обновлены компоненты AdvertisingTab и WBWarehouseDashboard для работы с новыми данными. Реализованы GraphQL запросы и мутации для управления внешней рекламой и кэшем склада. Оптимизирована логика отображения статистики и добавлены новые функции для работы с рекламой.
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
@ -10,12 +10,351 @@ 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'
|
||||
import { WildberriesService } from '@/services/wildberries-service'
|
||||
import { toast } from 'sonner'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { GET_WB_WAREHOUSE_DATA } from '@/graphql/queries'
|
||||
import { SAVE_WB_WAREHOUSE_CACHE } from '@/graphql/mutations'
|
||||
|
||||
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 WBWarehouseDashboard() {
|
||||
const { user } = useAuth()
|
||||
const { isCollapsed, getSidebarMargin } = useSidebar()
|
||||
const [activeTab, setActiveTab] = useState('fulfillment')
|
||||
|
||||
// Состояние данных WB Warehouse
|
||||
const [stocks, setStocks] = useState<WBStock[]>([])
|
||||
const [warehouses, setWarehouses] = useState<WBWarehouse[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
|
||||
// Статистика
|
||||
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
|
||||
|
||||
// GraphQL хуки для работы с кешем
|
||||
const { data: cacheData, loading: cacheLoading, refetch: refetchCache } = useQuery(GET_WB_WAREHOUSE_DATA, {
|
||||
skip: !hasWBApiKey,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
})
|
||||
|
||||
const [saveCache] = useMutation(SAVE_WB_WAREHOUSE_CACHE)
|
||||
|
||||
// Комбинирование карточек с индивидуальными данными аналитики
|
||||
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 || ''),
|
||||
description: String(card.description || ''),
|
||||
}
|
||||
|
||||
// Получаем аналитические данные для данного nmId
|
||||
const analytics = analyticsMap.get(card.nmID)
|
||||
if (analytics && Array.isArray(analytics)) {
|
||||
analytics.forEach((item: any) => {
|
||||
if (item.stocks && Array.isArray(item.stocks)) {
|
||||
item.stocks.forEach((stockItem: any) => {
|
||||
stock.stocks.push({
|
||||
warehouseId: stockItem.warehouseId || 0,
|
||||
warehouseName: String(stockItem.warehouseName || 'Неизвестный склад'),
|
||||
quantity: Number(stockItem.quantity) || 0,
|
||||
quantityFull: Number(stockItem.quantityFull) || 0,
|
||||
inWayToClient: Number(stockItem.inWayToClient) || 0,
|
||||
inWayFromClient: Number(stockItem.inWayFromClient) || 0,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Подсчитываем общие показатели
|
||||
stock.totalQuantity = stock.stocks.reduce((sum, s) => sum + s.quantity, 0)
|
||||
stock.totalReserved = stock.stocks.reduce((sum, s) => sum + s.inWayToClient, 0)
|
||||
|
||||
stocksMap.set(card.nmID, stock)
|
||||
})
|
||||
|
||||
return Array.from(stocksMap.values())
|
||||
}
|
||||
|
||||
// Извлечение складов из данных о товарах
|
||||
const extractWarehousesFromStocks = (stocksData: WBStock[]): WBWarehouse[] => {
|
||||
const warehousesMap = new Map<number, WBWarehouse>()
|
||||
|
||||
stocksData.forEach(item => {
|
||||
item.stocks.forEach(stock => {
|
||||
if (!warehousesMap.has(stock.warehouseId)) {
|
||||
warehousesMap.set(stock.warehouseId, {
|
||||
id: stock.warehouseId,
|
||||
name: stock.warehouseName,
|
||||
cargoType: 0,
|
||||
deliveryType: 0
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(warehousesMap.values())
|
||||
}
|
||||
|
||||
// Обновление статистики
|
||||
const updateStatistics = (stocksData: WBStock[], warehousesData: WBWarehouse[]) => {
|
||||
setTotalProducts(stocksData.length)
|
||||
|
||||
const totalStocksCount = stocksData.reduce((sum, item) => sum + item.totalQuantity, 0)
|
||||
setTotalStocks(totalStocksCount)
|
||||
|
||||
const totalReservedCount = stocksData.reduce((sum, item) => sum + item.totalReserved, 0)
|
||||
setTotalReserved(totalReservedCount)
|
||||
|
||||
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 loadWarehouseDataFromCache = (cacheData: any) => {
|
||||
try {
|
||||
const parsedData = typeof cacheData.data === 'string' ? JSON.parse(cacheData.data) : cacheData.data
|
||||
|
||||
const cachedStocks = parsedData.stocks || []
|
||||
const cachedWarehouses = parsedData.warehouses || []
|
||||
const cachedAnalytics = parsedData.analyticsData || []
|
||||
|
||||
setStocks(cachedStocks)
|
||||
setWarehouses(cachedWarehouses)
|
||||
setAnalyticsData(cachedAnalytics)
|
||||
|
||||
// Обновляем статистику из кеша
|
||||
setTotalProducts(cacheData.totalProducts)
|
||||
setTotalStocks(cacheData.totalStocks)
|
||||
setTotalReserved(cacheData.totalReserved)
|
||||
|
||||
const totalFromClientCount = (cachedStocks || []).reduce((sum: number, item: WBStock) =>
|
||||
sum + item.stocks.reduce((stockSum, stock) => stockSum + stock.inWayFromClient, 0), 0
|
||||
)
|
||||
setTotalFromClient(totalFromClientCount)
|
||||
|
||||
const warehousesWithStock = new Set(
|
||||
(cachedStocks || []).flatMap((item: WBStock) => item.stocks.map(s => s.warehouseId))
|
||||
)
|
||||
setActiveWarehouses(warehousesWithStock.size)
|
||||
|
||||
console.log('WB Warehouse: Data loaded from cache:', cachedStocks?.length || 0, 'items')
|
||||
toast.success(`Загружено из кеша: ${cachedStocks?.length || 0} товаров`)
|
||||
} catch (error) {
|
||||
console.error('WB Warehouse: Error parsing cache data:', error)
|
||||
toast.error('Ошибка загрузки данных из кеша')
|
||||
// Если кеш поврежден, загружаем из API
|
||||
loadWarehouseDataFromAPI()
|
||||
} finally {
|
||||
setInitialized(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка данных из API и сохранение в кеш
|
||||
const loadWarehouseDataFromAPI = 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)
|
||||
|
||||
// 5. Подготавливаем статистику
|
||||
const stats = {
|
||||
totalProducts: combinedStocks.length,
|
||||
totalStocks: combinedStocks.reduce((sum, item) => sum + item.totalQuantity, 0),
|
||||
totalReserved: combinedStocks.reduce((sum, item) => sum + item.totalReserved, 0),
|
||||
}
|
||||
|
||||
// 6. Сохраняем в кеш
|
||||
try {
|
||||
await saveCache({
|
||||
variables: {
|
||||
input: {
|
||||
data: JSON.stringify({
|
||||
stocks: combinedStocks,
|
||||
warehouses: extractedWarehouses,
|
||||
analyticsData: analyticsData,
|
||||
}),
|
||||
totalProducts: stats.totalProducts,
|
||||
totalStocks: stats.totalStocks,
|
||||
totalReserved: stats.totalReserved,
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log('WB Warehouse: Data saved to cache')
|
||||
} catch (cacheError) {
|
||||
console.error('WB Warehouse: Error saving to cache:', cacheError)
|
||||
}
|
||||
|
||||
// 7. Обновляем состояние
|
||||
setStocks(combinedStocks)
|
||||
setWarehouses(extractedWarehouses)
|
||||
updateStatistics(combinedStocks, extractedWarehouses)
|
||||
|
||||
toast.success(`Загружено товаров: ${combinedStocks.length}`)
|
||||
} catch (error) {
|
||||
console.error('WB Warehouse: Error loading data from API:', error)
|
||||
toast.error('Ошибка при загрузке данных из API')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setInitialized(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Основная функция загрузки данных
|
||||
const loadWarehouseData = async () => {
|
||||
if (!hasWBApiKey) {
|
||||
setInitialized(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Сначала пытаемся получить данные из кеша
|
||||
try {
|
||||
const result = await refetchCache()
|
||||
const cacheResponse = result.data?.getWBWarehouseData
|
||||
|
||||
if (cacheResponse?.success && cacheResponse?.fromCache && cacheResponse?.cache) {
|
||||
// Данные найдены в кеше
|
||||
loadWarehouseDataFromCache(cacheResponse.cache)
|
||||
} else {
|
||||
// Кеша нет или он устарел, загружаем из API
|
||||
console.log('WB Warehouse: No cache found, loading from API')
|
||||
await loadWarehouseDataFromAPI()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WB Warehouse: Error checking cache:', error)
|
||||
// При ошибке кеша загружаем из API
|
||||
await loadWarehouseDataFromAPI()
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем данные только один раз при инициализации
|
||||
useEffect(() => {
|
||||
if (!cacheLoading && user?.organization && !initialized) {
|
||||
loadWarehouseData()
|
||||
}
|
||||
}, [cacheLoading, user?.organization, initialized])
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden">
|
||||
<Sidebar />
|
||||
@ -50,7 +389,20 @@ export function WBWarehouseDashboard() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="wildberries" className="h-full mt-0">
|
||||
<WildberriesWarehouseTab />
|
||||
<WildberriesWarehouseTab
|
||||
stocks={stocks}
|
||||
warehouses={warehouses}
|
||||
loading={loading}
|
||||
initialized={initialized}
|
||||
cacheLoading={cacheLoading}
|
||||
totalProducts={totalProducts}
|
||||
totalStocks={totalStocks}
|
||||
totalReserved={totalReserved}
|
||||
totalFromClient={totalFromClient}
|
||||
activeWarehouses={activeWarehouses}
|
||||
analyticsData={analyticsData}
|
||||
onRefresh={loadWarehouseDataFromAPI}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="my-warehouse" className="h-full mt-0">
|
||||
|
@ -1,11 +1,9 @@
|
||||
"use client"
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import React, { useState } from 'react'
|
||||
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'
|
||||
@ -44,203 +42,36 @@ interface WBWarehouse {
|
||||
deliveryType: number
|
||||
}
|
||||
|
||||
export function WildberriesWarehouseTab() {
|
||||
const { user } = useAuth()
|
||||
|
||||
const [stocks, setStocks] = useState<WBStock[]>([])
|
||||
const [warehouses, setWarehouses] = useState<WBWarehouse[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
interface WildberriesWarehouseTabProps {
|
||||
stocks: WBStock[]
|
||||
warehouses: WBWarehouse[]
|
||||
loading: boolean
|
||||
initialized: boolean
|
||||
cacheLoading: boolean
|
||||
totalProducts: number
|
||||
totalStocks: number
|
||||
totalReserved: number
|
||||
totalFromClient: number
|
||||
activeWarehouses: number
|
||||
analyticsData: any[]
|
||||
onRefresh: () => Promise<void>
|
||||
}
|
||||
|
||||
export function WildberriesWarehouseTab({
|
||||
stocks,
|
||||
warehouses,
|
||||
loading,
|
||||
initialized,
|
||||
cacheLoading,
|
||||
totalProducts,
|
||||
totalStocks,
|
||||
totalReserved,
|
||||
totalFromClient,
|
||||
activeWarehouses,
|
||||
analyticsData,
|
||||
onRefresh
|
||||
}: WildberriesWarehouseTabProps) {
|
||||
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 => {
|
||||
@ -254,6 +85,15 @@ export function WildberriesWarehouseTab() {
|
||||
)
|
||||
})
|
||||
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
await onRefresh()
|
||||
} catch (error) {
|
||||
console.error('Error refreshing data:', error)
|
||||
toast.error('Ошибка при обновлении данных')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Статистика */}
|
||||
@ -263,80 +103,103 @@ export function WildberriesWarehouseTab() {
|
||||
totalReserved={totalReserved}
|
||||
totalFromClient={totalFromClient}
|
||||
activeWarehouses={activeWarehouses}
|
||||
loading={loading}
|
||||
loading={!initialized || loading || cacheLoading}
|
||||
/>
|
||||
|
||||
{/* Аналитика по складам WB */}
|
||||
{analyticsData.length > 0 && (
|
||||
{initialized && 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
|
||||
Аналитика по складам 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 className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{analyticsData.slice(0, 6).map((item, index) => (
|
||||
<div key={index} className="bg-white/5 rounded-lg p-3">
|
||||
<div className="text-sm text-white/60">Склад {index + 1}</div>
|
||||
<div className="text-lg font-medium text-white">
|
||||
{JSON.stringify(item).length > 50
|
||||
? `${JSON.stringify(item).substring(0, 50)}...`
|
||||
: JSON.stringify(item)
|
||||
}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</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} />
|
||||
))}
|
||||
{/* Основной контент */}
|
||||
<Card className="glass-card border-white/10 flex-1 flex flex-col overflow-hidden">
|
||||
<div className="p-6 border-b border-white/10 flex-shrink-0">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||
<Package className="h-6 w-6 mr-2 text-blue-400" />
|
||||
Склад Wildberries
|
||||
</h2>
|
||||
<p className="text-white/60 text-sm mt-1">
|
||||
Управление товарами на складах Wildberries
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{loading ? 'Обновление...' : 'Обновить данные'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Поиск */}
|
||||
<div className="mt-4">
|
||||
<SearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Контент с таблицей */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{!initialized || loading || cacheLoading ? (
|
||||
<div className="p-6">
|
||||
<LoadingSkeleton />
|
||||
</div>
|
||||
) : filteredStocks.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<Package className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">
|
||||
{searchTerm ? 'Товары не найдены' : 'Нет данных о товарах'}
|
||||
</p>
|
||||
{!searchTerm && (
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
className="mt-4 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
Загрузить данные
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="p-6 space-y-3">
|
||||
{/* Заголовок таблицы */}
|
||||
<TableHeader />
|
||||
|
||||
{/* Строки товаров */}
|
||||
{filteredStocks.map((item) => (
|
||||
<StockTableRow key={item.nmId} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user