Files
sfera-new/src/components/wb-warehouse/wb-warehouse-dashboard.tsx
Bivekich 547e6e7d95 Обновление компонентов интерфейса и оптимизация логики
- Добавлен компонент AppShell в RootLayout для улучшения структуры
- Обновлен компонент Sidebar для предотвращения дублирования при рендеринге
- Оптимизированы импорты в компонентах AdvertisingTab и SalesTab
- Реализована логика кэширования статистики селлера в GraphQL резолверах
2025-08-08 09:24:15 +03:00

425 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMutation, useQuery } from '@apollo/client'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Sidebar } from '@/components/dashboard/sidebar'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { SAVE_WB_WAREHOUSE_CACHE } from '@/graphql/mutations'
import { GET_WB_WAREHOUSE_DATA } from '@/graphql/queries'
import { useAuth } from '@/hooks/useAuth'
import { useSidebar } from '@/hooks/useSidebar'
import { WildberriesService } from '@/services/wildberries-service'
import { FulfillmentWarehouseTab } from './fulfillment-warehouse-tab'
import { MyWarehouseTab } from './my-warehouse-tab'
import { WildberriesWarehouseTab } from './wildberries-warehouse-tab'
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: _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 && analytics.data && analytics.data.regions && Array.isArray(analytics.data.regions)) {
analytics.data.regions.forEach((region: any) => {
if (region.offices && Array.isArray(region.offices)) {
region.offices.forEach((office: any) => {
stock.stocks.push({
warehouseId: office.officeID || 0,
warehouseName: String(office.officeName || 'Неизвестный склад'),
quantity: Number(office.metrics?.stockCount) || 0,
quantityFull: Number(office.metrics?.stockCount) || 0,
inWayToClient: Number(office.metrics?.toClientCount) || 0,
inWayFromClient: Number(office.metrics?.fromClientCount) || 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.warn('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.warn('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.warn('WB Warehouse: NM IDs to process:', nmIds.length)
// 2. Получаем аналитику для каждого товара индивидуально
const analyticsResults = []
for (const nmId of nmIds) {
try {
console.warn(`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.warn('WB Warehouse: Analytics results:', analyticsResults.length)
// 3. Комбинируем данные
const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults)
console.warn('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.warn('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.warn('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 min-h-0">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300 min-h-0 flex flex-col`}>
<div className="h-full w-full flex flex-col min-h-0">
{/* Табы */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0">
<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"
>
Склад фулфилмент
</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="flex-1 overflow-hidden min-h-0">
<TabsContent value="fulfillment" className="h-full mt-0 min-h-0">
<FulfillmentWarehouseTab />
</TabsContent>
<TabsContent value="wildberries" className="h-full mt-0 min-h-0">
<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 min-h-0">
<MyWarehouseTab />
</TabsContent>
</div>
</Tabs>
</div>
</main>
</div>
)
}