
- Добавлен компонент AppShell в RootLayout для улучшения структуры - Обновлен компонент Sidebar для предотвращения дублирования при рендеринге - Оптимизированы импорты в компонентах AdvertisingTab и SalesTab - Реализована логика кэширования статистики селлера в GraphQL резолверах
425 lines
16 KiB
TypeScript
425 lines
16 KiB
TypeScript
'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>
|
||
)
|
||
}
|