This commit is contained in:
Bivekich
2025-07-30 13:57:27 +03:00
parent d22eacb30c
commit 28312830a4
6 changed files with 343 additions and 13 deletions

View File

@ -110,6 +110,7 @@ model Organization {
supplySuppliers SupplySupplier[] @relation("SupplySuppliers") supplySuppliers SupplySupplier[] @relation("SupplySuppliers")
externalAds ExternalAd[] @relation("ExternalAds") externalAds ExternalAd[] @relation("ExternalAds")
wbWarehouseCaches WBWarehouseCache[] @relation("WBWarehouseCaches") wbWarehouseCaches WBWarehouseCache[] @relation("WBWarehouseCaches")
sellerStatsCaches SellerStatsCache[] @relation("SellerStatsCaches")
@@map("organizations") @@map("organizations")
} }
@ -546,3 +547,35 @@ model WBWarehouseCache {
@@index([organizationId, cacheDate]) @@index([organizationId, cacheDate])
@@map("wb_warehouse_caches") @@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")
}

View File

@ -95,6 +95,11 @@ interface CampaignStatsProps {
useCustomDates: boolean useCustomDates: boolean
startDate: string startDate: string
endDate: string endDate: string
// Новые пропсы для работы с кэшем
getCachedData?: () => any
setCachedData?: (data: any) => void
isLoadingData?: boolean
setIsLoadingData?: (loading: boolean) => void
} }
// Интерфейсы для API данных // Интерфейсы для 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() const { user } = useAuth()
// Состояния для раскрытия строк // Состояния для раскрытия строк
@ -481,6 +495,19 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
const [generatedLinksData, setGeneratedLinksData] = useState<Record<string, GeneratedLink[]>>({}) const [generatedLinksData, setGeneratedLinksData] = useState<Record<string, GeneratedLink[]>>({})
const prevCampaignStats = useRef<CampaignStats[]>([]) const prevCampaignStats = useRef<CampaignStats[]>([])
// Проверяем кэш при изменении периода
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 = () => { const getDateRange = () => {
if (useCustomDates && startDate && endDate) { if (useCustomDates && startDate && endDate) {
@ -949,9 +976,24 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
const newDailyData = convertCampaignDataToDailyData(campaignStats) const newDailyData = convertCampaignDataToDailyData(campaignStats)
setDailyData(newDailyData) setDailyData(newDailyData)
prevCampaignStats.current = campaignStats 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[]) => { const handleCampaignsSelected = (ids: number[]) => {
if (ids.length === 0) return if (ids.length === 0) return

View File

@ -58,6 +58,11 @@ interface SalesTabProps {
endDate?: string endDate?: string
onPeriodChange?: (period: string) => void onPeriodChange?: (period: string) => void
onUseCustomDatesChange?: (useCustom: boolean) => void onUseCustomDatesChange?: (useCustom: boolean) => void
// Новые пропсы для работы с кэшем
getCachedData?: () => any
setCachedData?: (data: any) => void
isLoadingData?: boolean
setIsLoadingData?: (loading: boolean) => void
} }
// Mock данные для графиков // 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({ const [visibleMetrics, setVisibleMetrics] = useState({
sales: true, sales: true,
@ -179,18 +195,55 @@ export function SalesTab({ selectedPeriod, useCustomDates, startDate, endDate, o
returns: true, returns: true,
}) })
// Получаем данные из WB API // Данные для графика и таблицы
const { data: wbData, loading, error } = useQuery(GET_WILDBERRIES_STATISTICS, { const [chartData, setChartData] = useState<typeof mockChartData>([])
const [tableData, setTableData] = useState<typeof mockTableData>([])
// Получаем данные из WB API только если нет в кэше
const { data: wbData, loading, error, refetch } = useQuery(GET_WILDBERRIES_STATISTICS, {
variables: useCustomDates variables: useCustomDates
? { startDate, endDate } ? { startDate, endDate }
: { period: selectedPeriod }, : { period: selectedPeriod },
errorPolicy: 'all', errorPolicy: 'all',
skip: useCustomDates && (!startDate || !endDate) // Не запрашиваем пока не выбраны обе даты skip: true, // Изначально пропускаем запрос, будем запускать вручную
}) })
// Данные для графика и таблицы // Эффект для проверки кэша и загрузки данных
const [chartData, setChartData] = useState<typeof mockChartData>([]) useEffect(() => {
const [tableData, setTableData] = useState<typeof mockTableData>([]) 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(() => { useEffect(() => {
if (wbData?.getWildberriesStatistics?.success && wbData.getWildberriesStatistics.data) { if (wbData?.getWildberriesStatistics?.success && wbData.getWildberriesStatistics.data) {
@ -311,8 +364,21 @@ export function SalesTab({ selectedPeriod, useCustomDates, startDate, endDate, o
setChartData(newChartData.reverse()) // Для графика - старые даты слева setChartData(newChartData.reverse()) // Для графика - старые даты слева
setTableData(newTableData) // Для таблицы - новые даты сверху 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) => { 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 const hasData = tableData.length > 0
// Состояние для сортировки // Состояние для сортировки

View File

@ -1,21 +1,127 @@
"use client" "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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { Sidebar } from '@/components/dashboard/sidebar' import { Sidebar } from '@/components/dashboard/sidebar'
import { useSidebar } from '@/hooks/useSidebar' import { useSidebar } from '@/hooks/useSidebar'
import { useAuth } from '@/hooks/useAuth'
import { SalesTab } from '@/components/seller-statistics/sales-tab' import { SalesTab } from '@/components/seller-statistics/sales-tab'
import { AdvertisingTab } from '@/components/seller-statistics/advertising-tab' import { AdvertisingTab } from '@/components/seller-statistics/advertising-tab'
import { DateRangePicker } from '@/components/ui/date-picker' 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' import { BarChart3, PieChart, TrendingUp, Calendar } from 'lucide-react'
export function SellerStatisticsDashboard() { export function SellerStatisticsDashboard() {
const { getSidebarMargin } = useSidebar() const { getSidebarMargin } = useSidebar()
const { user } = useAuth()
const [selectedPeriod, setSelectedPeriod] = useState('week') const [selectedPeriod, setSelectedPeriod] = useState('week')
const [useCustomDates, setUseCustomDates] = useState(false) const [useCustomDates, setUseCustomDates] = useState(false)
const [startDate, setStartDate] = useState('') const [startDate, setStartDate] = useState('')
const [endDate, setEndDate] = useState('') const [endDate, setEndDate] = useState('')
const [activeTab, setActiveTab] = useState('sales')
// Кэш для данных разных периодов и табов
const [salesCache, setSalesCache] = useState<Map<string, any>>(new Map())
const [advertisingCache, setAdvertisingCache] = useState<Map<string, any>>(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 ( return (
<div className="h-screen flex overflow-hidden"> <div className="h-screen flex overflow-hidden">
@ -25,7 +131,7 @@ export function SellerStatisticsDashboard() {
{/* Основной контент с табами */} {/* Основной контент с табами */}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<Tabs defaultValue="sales" className="h-full flex flex-col"> <Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-3 bg-white/5 backdrop-blur border border-white/10 rounded-xl flex-shrink-0 h-11"> <TabsList className="grid w-full grid-cols-3 bg-white/5 backdrop-blur border border-white/10 rounded-xl flex-shrink-0 h-11">
<TabsTrigger <TabsTrigger
value="sales" value="sales"
@ -60,6 +166,14 @@ export function SellerStatisticsDashboard() {
endDate={endDate} endDate={endDate}
onPeriodChange={setSelectedPeriod} onPeriodChange={setSelectedPeriod}
onUseCustomDatesChange={setUseCustomDates} onUseCustomDatesChange={setUseCustomDates}
// Передаём функции для работы с кэшем
getCachedData={() => getCachedData('sales')}
setCachedData={(data) => {
setCachedData('sales', data)
saveToCacheDB('sales', data)
}}
isLoadingData={isLoadingData}
setIsLoadingData={setIsLoadingData}
/> />
</TabsContent> </TabsContent>
@ -69,6 +183,14 @@ export function SellerStatisticsDashboard() {
useCustomDates={useCustomDates} useCustomDates={useCustomDates}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
// Передаём функции для работы с кэшем
getCachedData={() => getCachedData('advertising')}
setCachedData={(data) => {
setCachedData('advertising', data)
saveToCacheDB('advertising', data)
}}
isLoadingData={isLoadingData}
setIsLoadingData={setIsLoadingData}
/> />
</TabsContent> </TabsContent>

View File

@ -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
}
}
}
`;

View File

@ -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
}
}
}
`;