Добавлены модели для внешней рекламы и кэша склада WB в схему Prisma. Обновлены компоненты AdvertisingTab и WBWarehouseDashboard для работы с новыми данными. Реализованы GraphQL запросы и мутации для управления внешней рекламой и кэшем склада. Оптимизирована логика отображения статистики и добавлены новые функции для работы с рекламой.

This commit is contained in:
Bivekich
2025-07-29 17:44:40 +03:00
parent 8b0d3cde00
commit c174a9f83c
13 changed files with 4576 additions and 780 deletions

View File

@ -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">