From 28312830a497aae62049dd65ad3c2d028af5a47a Mon Sep 17 00:00:00 2001 From: Bivekich Date: Wed, 30 Jul 2025 13:57:27 +0300 Subject: [PATCH] fixing --- prisma/schema.prisma | 33 +++++ .../seller-statistics/advertising-tab.tsx | 46 ++++++- .../seller-statistics/sales-tab.tsx | 84 ++++++++++-- .../seller-statistics-dashboard.tsx | 126 +++++++++++++++++- src/graphql/mutations.ts | 29 ++++ src/graphql/queries.ts | 38 ++++++ 6 files changed, 343 insertions(+), 13 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 490b555..66769cc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -110,6 +110,7 @@ model Organization { supplySuppliers SupplySupplier[] @relation("SupplySuppliers") externalAds ExternalAd[] @relation("ExternalAds") wbWarehouseCaches WBWarehouseCache[] @relation("WBWarehouseCaches") + sellerStatsCaches SellerStatsCache[] @relation("SellerStatsCaches") @@map("organizations") } @@ -546,3 +547,35 @@ model WBWarehouseCache { @@index([organizationId, cacheDate]) @@map("wb_warehouse_caches") } + +model SellerStatsCache { + id String @id @default(cuid()) + organizationId String // ID организации + cacheDate DateTime // Дата кеширования (только дата, без времени) + period String // Период статистики (week, month, quarter, custom) + dateFrom DateTime? // Дата начала периода (для custom) + dateTo DateTime? // Дата окончания периода (для custom) + + // Данные товаров + productsData Json? // Кешированные данные товаров + productsTotalSales Decimal? @db.Decimal(15, 2) // Общая сумма продаж товаров + productsTotalOrders Int? // Общее количество заказов товаров + productsCount Int? // Количество товаров + + // Данные рекламы + advertisingData Json? // Кешированные данные рекламы + advertisingTotalCost Decimal? @db.Decimal(15, 2) // Общие расходы на рекламу + advertisingTotalViews Int? // Общие показы рекламы + advertisingTotalClicks Int? // Общие клики рекламы + + // Метаданные + expiresAt DateTime // Время истечения кеша (24 часа) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organization Organization @relation("SellerStatsCaches", fields: [organizationId], references: [id], onDelete: Cascade) + + @@unique([organizationId, cacheDate, period, dateFrom, dateTo]) + @@index([organizationId, cacheDate]) + @@index([expiresAt]) + @@map("seller_stats_caches") +} diff --git a/src/components/seller-statistics/advertising-tab.tsx b/src/components/seller-statistics/advertising-tab.tsx index 1b68555..2a1a3d6 100644 --- a/src/components/seller-statistics/advertising-tab.tsx +++ b/src/components/seller-statistics/advertising-tab.tsx @@ -95,6 +95,11 @@ interface CampaignStatsProps { useCustomDates: boolean startDate: string endDate: string + // Новые пропсы для работы с кэшем + getCachedData?: () => any + setCachedData?: (data: any) => void + isLoadingData?: boolean + setIsLoadingData?: (loading: boolean) => void } // Интерфейсы для API данных @@ -455,7 +460,16 @@ const CompactCampaignSelector = ({ ) } -export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endDate }: CampaignStatsProps) { +export function AdvertisingTab({ + selectedPeriod, + useCustomDates, + startDate, + endDate, + getCachedData, + setCachedData, + isLoadingData, + setIsLoadingData +}: CampaignStatsProps) { const { user } = useAuth() // Состояния для раскрытия строк @@ -481,6 +495,19 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD const [generatedLinksData, setGeneratedLinksData] = useState>({}) const prevCampaignStats = useRef([]) + // Проверяем кэш при изменении периода + useEffect(() => { + if (getCachedData) { + const cachedData = getCachedData() + if (cachedData) { + setDailyData(cachedData.dailyData || []) + setCampaignStats(cachedData.campaignStats || []) + console.log('Advertising: Using cached data') + return + } + } + }, [selectedPeriod, useCustomDates, startDate, endDate, getCachedData]) + // Вычисляем диапазон дат для запроса внешней рекламы const getDateRange = () => { if (useCustomDates && startDate && endDate) { @@ -949,9 +976,24 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD const newDailyData = convertCampaignDataToDailyData(campaignStats) setDailyData(newDailyData) prevCampaignStats.current = campaignStats + + // Сохраняем данные в кэш + if (setCachedData) { + const cacheData = { + dailyData: newDailyData, + campaignStats: campaignStats, + totalCost: newDailyData.reduce((sum, day) => sum + day.totalSum, 0), + totalViews: newDailyData.reduce((sum, day) => + sum + day.products.reduce((daySum, product) => daySum + product.totalViews, 0), 0), + totalClicks: newDailyData.reduce((sum, day) => + sum + day.products.reduce((daySum, product) => daySum + product.totalClicks, 0), 0), + } + setCachedData(cacheData) + console.log('Advertising: Data cached successfully') + } } } - }, [campaignStats, externalAdsData]) // Добавляем externalAdsData в зависимости + }, [campaignStats, externalAdsData, setCachedData]) // Добавляем externalAdsData и setCachedData в зависимости const handleCampaignsSelected = (ids: number[]) => { if (ids.length === 0) return diff --git a/src/components/seller-statistics/sales-tab.tsx b/src/components/seller-statistics/sales-tab.tsx index c8d6310..0b429a7 100644 --- a/src/components/seller-statistics/sales-tab.tsx +++ b/src/components/seller-statistics/sales-tab.tsx @@ -58,6 +58,11 @@ interface SalesTabProps { endDate?: string onPeriodChange?: (period: string) => void onUseCustomDatesChange?: (useCustom: boolean) => void + // Новые пропсы для работы с кэшем + getCachedData?: () => any + setCachedData?: (data: any) => void + isLoadingData?: boolean + setIsLoadingData?: (loading: boolean) => void } // Mock данные для графиков @@ -169,7 +174,18 @@ const mockTableData = [ }, ] -export function SalesTab({ selectedPeriod, useCustomDates, startDate, endDate, onPeriodChange, onUseCustomDatesChange }: SalesTabProps) { +export function SalesTab({ + selectedPeriod, + useCustomDates, + startDate, + endDate, + onPeriodChange, + onUseCustomDatesChange, + getCachedData, + setCachedData, + isLoadingData, + setIsLoadingData +}: SalesTabProps) { // Состояния для чекбоксов фильтрации const [visibleMetrics, setVisibleMetrics] = useState({ sales: true, @@ -179,18 +195,55 @@ export function SalesTab({ selectedPeriod, useCustomDates, startDate, endDate, o returns: true, }) - // Получаем данные из WB API - const { data: wbData, loading, error } = useQuery(GET_WILDBERRIES_STATISTICS, { + // Данные для графика и таблицы + const [chartData, setChartData] = useState([]) + const [tableData, setTableData] = useState([]) + + // Получаем данные из WB API только если нет в кэше + const { data: wbData, loading, error, refetch } = useQuery(GET_WILDBERRIES_STATISTICS, { variables: useCustomDates ? { startDate, endDate } : { period: selectedPeriod }, errorPolicy: 'all', - skip: useCustomDates && (!startDate || !endDate) // Не запрашиваем пока не выбраны обе даты + skip: true, // Изначально пропускаем запрос, будем запускать вручную }) - // Данные для графика и таблицы - const [chartData, setChartData] = useState([]) - const [tableData, setTableData] = useState([]) + // Эффект для проверки кэша и загрузки данных + useEffect(() => { + const loadData = async () => { + // Сначала проверяем локальный кэш + if (getCachedData) { + const cachedData = getCachedData() + if (cachedData) { + setChartData(cachedData.chartData || mockChartData) + setTableData(cachedData.tableData || mockTableData) + console.log('Sales: Using cached data') + return + } + } + + // Если нет кэша, запрашиваем данные + if (setIsLoadingData) setIsLoadingData(true) + + try { + const result = await refetch() + if (result.data?.getWildberriesStatistics?.success) { + console.log('Sales: Loading fresh data from API') + // Обрабатываем данные в существующем useEffect + } + } catch (error) { + console.error('Sales: Error loading data:', error) + } finally { + if (setIsLoadingData) setIsLoadingData(false) + } + } + + // Загружаем данные только если не пропускаем загрузку + const shouldSkip = useCustomDates && (!startDate || !endDate) + if (!shouldSkip) { + loadData() + } + }, [selectedPeriod, useCustomDates, startDate, endDate, getCachedData, refetch, setIsLoadingData]) useEffect(() => { if (wbData?.getWildberriesStatistics?.success && wbData.getWildberriesStatistics.data) { @@ -311,8 +364,21 @@ export function SalesTab({ selectedPeriod, useCustomDates, startDate, endDate, o setChartData(newChartData.reverse()) // Для графика - старые даты слева setTableData(newTableData) // Для таблицы - новые даты сверху + + // Сохраняем данные в кэш + if (setCachedData) { + const cacheData = { + chartData: newChartData, + tableData: newTableData, + totalSales: newTableData.reduce((sum, item) => sum + item.sales, 0), + totalOrders: newTableData.reduce((sum, item) => sum + item.orders, 0), + productsCount: newTableData.length, + } + setCachedData(cacheData) + console.log('Sales: Data cached successfully') + } } - }, [wbData]) + }, [wbData, setCachedData]) // Функция для переключения видимости метрики const toggleMetric = (metric: keyof typeof visibleMetrics) => { @@ -323,7 +389,7 @@ export function SalesTab({ selectedPeriod, useCustomDates, startDate, endDate, o } // Проверяем состояние загрузки и данных - const isLoading = loading || (useCustomDates && (!startDate || !endDate)) + const isLoading = (isLoadingData !== undefined ? isLoadingData : loading) || (useCustomDates && (!startDate || !endDate)) const hasData = tableData.length > 0 // Состояние для сортировки diff --git a/src/components/seller-statistics/seller-statistics-dashboard.tsx b/src/components/seller-statistics/seller-statistics-dashboard.tsx index df832ce..f2dc261 100644 --- a/src/components/seller-statistics/seller-statistics-dashboard.tsx +++ b/src/components/seller-statistics/seller-statistics-dashboard.tsx @@ -1,21 +1,127 @@ "use client" -import { useState } from 'react' +import { useState, useEffect, useRef } from 'react' +import { useQuery, useMutation } from '@apollo/client' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Card } from '@/components/ui/card' import { Sidebar } from '@/components/dashboard/sidebar' import { useSidebar } from '@/hooks/useSidebar' +import { useAuth } from '@/hooks/useAuth' import { SalesTab } from '@/components/seller-statistics/sales-tab' import { AdvertisingTab } from '@/components/seller-statistics/advertising-tab' import { DateRangePicker } from '@/components/ui/date-picker' +import { GET_SELLER_STATS_CACHE } from '@/graphql/queries' +import { SAVE_SELLER_STATS_CACHE } from '@/graphql/mutations' import { BarChart3, PieChart, TrendingUp, Calendar } from 'lucide-react' export function SellerStatisticsDashboard() { const { getSidebarMargin } = useSidebar() + const { user } = useAuth() const [selectedPeriod, setSelectedPeriod] = useState('week') const [useCustomDates, setUseCustomDates] = useState(false) const [startDate, setStartDate] = useState('') const [endDate, setEndDate] = useState('') + const [activeTab, setActiveTab] = useState('sales') + + // Кэш для данных разных периодов и табов + const [salesCache, setSalesCache] = useState>(new Map()) + const [advertisingCache, setAdvertisingCache] = useState>(new Map()) + const [isLoadingData, setIsLoadingData] = useState(false) + + // Мутация для сохранения кэша + const [saveCache] = useMutation(SAVE_SELLER_STATS_CACHE) + + // Создаём ключ для кэша на основе периода и дат + const getCacheKey = () => { + if (useCustomDates && startDate && endDate) { + return `custom_${startDate}_${endDate}` + } + return selectedPeriod + } + + // Проверяем есть ли данные в локальном кэше + const getCachedData = (type: 'sales' | 'advertising') => { + const cache = type === 'sales' ? salesCache : advertisingCache + const cacheKey = getCacheKey() + return cache.get(cacheKey) + } + + // Сохраняем данные в локальный кэш + const setCachedData = (type: 'sales' | 'advertising', data: any) => { + const cacheKey = getCacheKey() + if (type === 'sales') { + setSalesCache(new Map(salesCache.set(cacheKey, data))) + } else { + setAdvertisingCache(new Map(advertisingCache.set(cacheKey, data))) + } + } + + // Запрос кэша из БД + const { data: cacheData, refetch: refetchCache } = useQuery(GET_SELLER_STATS_CACHE, { + variables: { + period: useCustomDates ? 'custom' : selectedPeriod, + dateFrom: useCustomDates ? startDate : undefined, + dateTo: useCustomDates ? endDate : undefined, + }, + skip: !user?.organization, + fetchPolicy: 'cache-first', + errorPolicy: 'ignore', + }) + + // Загружаем данные из кэша БД при изменении периода + useEffect(() => { + if (cacheData?.getSellerStatsCache?.success && cacheData.getSellerStatsCache.cache) { + const cache = cacheData.getSellerStatsCache.cache + const cacheKey = getCacheKey() + + // Проверяем не истёк ли кэш (24 часа) + const expiresAt = new Date(cache.expiresAt) + const now = new Date() + + if (expiresAt > now) { + // Кэш актуален, загружаем данные + if (cache.productsData) { + setSalesCache(new Map(salesCache.set(cacheKey, JSON.parse(cache.productsData)))) + } + if (cache.advertisingData) { + setAdvertisingCache(new Map(advertisingCache.set(cacheKey, JSON.parse(cache.advertisingData)))) + } + } + } + }, [cacheData, selectedPeriod, useCustomDates, startDate, endDate]) + + // Сохраняем данные в БД кэш + const saveToCacheDB = async (type: 'sales' | 'advertising', data: any) => { + try { + const cacheKey = getCacheKey() + const expiresAt = new Date() + expiresAt.setHours(expiresAt.getHours() + 24) // 24 часа + + const input: any = { + period: useCustomDates ? 'custom' : selectedPeriod, + dateFrom: useCustomDates ? startDate : null, + dateTo: useCustomDates ? endDate : null, + expiresAt: expiresAt.toISOString(), + } + + if (type === 'sales') { + input.productsData = JSON.stringify(data) + input.productsTotalSales = data.totalSales || 0 + input.productsTotalOrders = data.totalOrders || 0 + input.productsCount = data.productsCount || 0 + } else { + input.advertisingData = JSON.stringify(data) + input.advertisingTotalCost = data.totalCost || 0 + input.advertisingTotalViews = data.totalViews || 0 + input.advertisingTotalClicks = data.totalClicks || 0 + } + + await saveCache({ variables: { input } }) + console.log(`Cached ${type} data saved to DB for period ${cacheKey}`) + } catch (error) { + console.error(`Error saving ${type} cache:`, error) + } + } return (
@@ -25,7 +131,7 @@ export function SellerStatisticsDashboard() { {/* Основной контент с табами */}
- + getCachedData('sales')} + setCachedData={(data) => { + setCachedData('sales', data) + saveToCacheDB('sales', data) + }} + isLoadingData={isLoadingData} + setIsLoadingData={setIsLoadingData} /> @@ -69,6 +183,14 @@ export function SellerStatisticsDashboard() { useCustomDates={useCustomDates} startDate={startDate} endDate={endDate} + // Передаём функции для работы с кэшем + getCachedData={() => getCachedData('advertising')} + setCachedData={(data) => { + setCachedData('advertising', data) + saveToCacheDB('advertising', data) + }} + isLoadingData={isLoadingData} + setIsLoadingData={setIsLoadingData} /> diff --git a/src/graphql/mutations.ts b/src/graphql/mutations.ts index 7bbefd0..8ca9fc0 100644 --- a/src/graphql/mutations.ts +++ b/src/graphql/mutations.ts @@ -1334,3 +1334,32 @@ export const SAVE_WB_WAREHOUSE_CACHE = gql` } } `; + +// Мутации для кеша статистики продаж +export const SAVE_SELLER_STATS_CACHE = gql` + mutation SaveSellerStatsCache($input: SellerStatsCacheInput!) { + saveSellerStatsCache(input: $input) { + success + message + cache { + id + organizationId + cacheDate + period + dateFrom + dateTo + productsData + productsTotalSales + productsTotalOrders + productsCount + advertisingData + advertisingTotalCost + advertisingTotalViews + advertisingTotalClicks + expiresAt + createdAt + updatedAt + } + } + } +`; diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 9ccd61d..e7f7f82 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -977,3 +977,41 @@ export const GET_WB_WAREHOUSE_DATA = gql` } } `; + +// Запросы для кеша статистики продаж +export const GET_SELLER_STATS_CACHE = gql` + query GetSellerStatsCache( + $period: String! + $dateFrom: String + $dateTo: String + ) { + getSellerStatsCache( + period: $period + dateFrom: $dateFrom + dateTo: $dateTo + ) { + success + message + fromCache + cache { + id + organizationId + cacheDate + period + dateFrom + dateTo + productsData + productsTotalSales + productsTotalOrders + productsCount + advertisingData + advertisingTotalCost + advertisingTotalViews + advertisingTotalClicks + expiresAt + createdAt + updatedAt + } + } + } +`;