@ -8,8 +8,32 @@ import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert , AlertDescription } from '@/components/ui/alert'
import { GET_WILDBERRIES_CAMPAIGN_STATS } from '@/graphql/queries '
import { TrendingUp , Search , Calendar , Eye , MousePointer , ShoppingCart , DollarSign , Percent , AlertCircle } from 'lucide-react '
import { Checkbox } from '@/components/ui/checkbox '
import { GET_WILDBERRIES_CAMPAIGN_STATS , GET_WILDBERRIES_CAMPAIGNS_LIST } from '@/graphql/queries '
import {
TrendingUp ,
Search ,
Calendar ,
Eye ,
MousePointer ,
ShoppingCart ,
DollarSign ,
Percent ,
AlertCircle ,
ChevronDown ,
ChevronRight ,
Monitor ,
Smartphone ,
Globe ,
Package ,
Target ,
BarChart3 ,
Maximize2 ,
Minimize2 ,
Settings ,
Filter ,
ArrowUpDown
} from 'lucide-react'
import {
ChartContainer ,
ChartTooltip ,
@ -18,13 +42,63 @@ import {
ChartLegendContent ,
type ChartConfig
} from '@/components/ui/chart'
import { LineChart , Line , XAxis , YAxis , CartesianGrid , ResponsiveContainer } from 'recharts'
import { LineChart , Line , XAxis , YAxis , CartesianGrid } from 'recharts'
interface CampaignStatsProps {
selectedPeriod : string
useCustomDates : boolean
startDate : string
endDate : string
// Интерфейсы для типизации данных API
interface CampaignProduct {
views : number
clicks : number
ctr : number
cpc : number
sum : number
atbs : number
orders : number
cr : number
shks : number
sum_price : number
name : string
nmId : number
}
interface CampaignApp {
views : number
clicks : number
ctr : number
cpc : number
sum : number
atbs : number
orders : number
cr : number
shks : number
sum_price : number
appType : number
nm : CampaignProduct [ ]
}
interface CampaignDay {
date : string
views : number
clicks : number
ctr : number
cpc : number
sum : number
atbs : number
orders : number
cr : number
shks : number
sum_price : number
apps : CampaignApp [ ]
}
interface BoosterStat {
date : string
nm : number
avg_position : number
}
interface CampaignInterval {
begin : string
end : string
}
interface CampaignStats {
@ -39,38 +113,556 @@ interface CampaignStats {
cr : number
shks : number
sum_price : number
dates : string [ ]
days : Arr ay< {
date : string
views : number
clicks : number
ctr : number
cpc : number
sum : number
atbs : number
orders : number
cr : number
shks : number
sum_price : number
} >
boosterStats : Array < {
dat e : string
views : number
clicks : number
ctr : number
cpc : number
sum : number
atbs : number
orders : number
cr : number
shks : number
sum_price : number
} >
interval? : CampaignInterval
days : CampaignD ay[ ]
boosterStats : BoosterStat [ ]
}
interface CampaignStatsProps {
selectedPeriod : string
useCustomDates : boolean
startDate : string
endDate : string
}
// Интерфейсы для списка кампаний
interface CampaignListItem {
advertId : number
changeTim e : string
}
interface CampaignGroup {
type : number
status : number
count : number
advert_list : CampaignListItem [ ]
}
interface CampaignsListData {
adverts : CampaignGroup [ ]
all : number
}
// Компонент компактного селектора кампаний
const CompactCampaignSelector = ( {
onCampaignsSelected ,
selectedCampaigns ,
loading : statsLoading
} : {
onCampaignsSelected : ( ids : number [ ] ) = > void ,
selectedCampaigns : number [ ] ,
loading : boolean
} ) = > {
const [ isExpanded , setIsExpanded ] = useState ( false )
const [ showManualInput , setShowManualInput ] = useState ( false )
const [ manualIds , setManualIds ] = useState ( '' )
const [ selectedIds , setSelectedIds ] = useState < Set < number > > ( new Set ( selectedCampaigns ) )
const [ filterType , setFilterType ] = useState < number | 'all' > ( 'all' )
const [ filterStatus , setFilterStatus ] = useState < number | 'all' > ( 'all' )
const { data : campaignsData , loading , error } = useQuery ( GET_WILDBERRIES_CAMPAIGNS_LIST , {
errorPolicy : 'all'
} )
const campaigns = campaignsData ? . getWildberriesCampaignsList ? . data ? . adverts || [ ]
// Функции для получения названий типов и статусов
const getCampaignTypeName = ( type : number ) = > {
const types : Record < number , string > = {
4 : 'Авто' ,
5 : 'Фразы' ,
6 : 'Предмет' ,
7 : 'Бренд' ,
8 : 'Медиа' ,
9 : 'Карусель'
}
return types [ type ] || ` Тип ${ type } `
}
const getCampaignStatusName = ( status : number ) = > {
const statuses : Record < number , string > = {
7 : 'Завершена' ,
8 : 'Отклонена' ,
9 : 'Активна' ,
11 : 'Н а паузе'
}
return statuses [ status ] || ` Статус ${ status } `
}
const getStatusColor = ( status : number ) = > {
const colors : Record < number , string > = {
9 : 'text-green-400' ,
11 : 'text-yellow-400' ,
7 : 'text-gray-400' ,
8 : 'text-red-400'
}
return colors [ status ] || 'text-white'
}
// Фильтрация кампаний
const filteredCampaigns = campaigns . filter ( ( group : CampaignGroup ) = >
( filterType === 'all' || group . type === filterType ) &&
( filterStatus === 'all' || group . status === filterStatus )
)
const handleCampaignToggle = ( campaignId : number ) = > {
const newSelected = new Set ( selectedIds )
if ( newSelected . has ( campaignId ) ) {
newSelected . delete ( campaignId )
} else {
newSelected . add ( campaignId )
}
setSelectedIds ( newSelected )
}
const handleSelectAll = ( group : CampaignGroup ) = > {
const newSelected = new Set ( selectedIds )
const groupIds = group . advert_list . map ( ( item : CampaignListItem ) = > item . advertId )
const allSelected = groupIds . every ( ( id : number ) = > newSelected . has ( id ) )
if ( allSelected ) {
groupIds . forEach ( id = > newSelected . delete ( id ) )
} else {
groupIds . forEach ( id = > newSelected . add ( id ) )
}
setSelectedIds ( newSelected )
}
const handleApplySelection = ( ) = > {
if ( showManualInput && manualIds . trim ( ) ) {
const ids = manualIds . split ( ',' ) . map ( id = > parseInt ( id . trim ( ) ) ) . filter ( id = > ! isNaN ( id ) )
onCampaignsSelected ( ids )
} else {
onCampaignsSelected ( Array . from ( selectedIds ) )
}
}
const uniqueTypes = [ . . . new Set ( campaigns . map ( ( group : CampaignGroup ) = > group . type ) ) ] as number [ ]
const uniqueStatuses = [ . . . new Set ( campaigns . map ( ( group : CampaignGroup ) = > group . status ) ) ] as number [ ]
return (
< Card className = "glass-card p-3" >
< div className = "space-y-3" >
{ /* Компактный заголовок */ }
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-2" >
< Button
variant = "ghost"
size = "sm"
onClick = { ( ) = > setIsExpanded ( ! isExpanded ) }
className = "h-7 px-2 text-white hover:bg-white/10"
>
{ isExpanded ? < Minimize2 className = "h-3 w-3" / > : < Maximize2 className = "h-3 w-3" / > }
< span className = "ml-1 text-sm" > К а м п а н и и < / span >
< / Button >
< Badge variant = "outline" className = "border-white/20 text-white text-xs" >
{ selectedIds . size }
< / Badge >
< / div >
< div className = "flex items-center gap-1" >
< Button
variant = "ghost"
size = "sm"
onClick = { ( ) = > setShowManualInput ( ! showManualInput ) }
className = "h-7 px-2 text-xs text-white/60 hover:bg-white/10"
>
{ showManualInput ? 'Список' : 'Ручной' }
< / Button >
< Button
onClick = { handleApplySelection }
disabled = { statsLoading || ( showManualInput ? ! manualIds . trim ( ) : selectedIds . size === 0 ) }
size = "sm"
className = "h-7 px-3 bg-blue-600 hover:bg-blue-700 text-white text-xs"
>
{ statsLoading ? (
< div className = "animate-spin rounded-full h-3 w-3 border-b border-white" / >
) : (
< >
< Search className = "h-3 w-3 mr-1" / >
З а г р у з и т ь
< / >
) }
< / Button >
< / div >
< / div >
{ /* Развернутый контент */ }
{ isExpanded && (
< div className = "space-y-3" >
{ showManualInput ? (
< Input
placeholder = "ID через запятую: 12345, 67890"
value = { manualIds }
onChange = { ( e ) = > setManualIds ( e . target . value ) }
className = "h-8 bg-white/5 border-white/20 text-white placeholder:text-white/40 text-xs"
/ >
) : (
< div className = "space-y-3" >
{ /* Компактные фильтры */ }
< div className = "flex items-center gap-2 text-xs" >
< select
value = { filterType }
onChange = { ( e ) = > setFilterType ( e . target . value === 'all' ? 'all' : parseInt ( e . target . value ) ) }
className = "h-7 bg-white/5 border border-white/20 rounded px-2 text-white text-xs"
>
< option value = "all" > В с е т и п ы < / option >
{ uniqueTypes . map ( ( type : number ) = > (
< option key = { type } value = { type } > { getCampaignTypeName ( type ) } < / option >
) ) }
< / select >
< select
value = { filterStatus }
onChange = { ( e ) = > setFilterStatus ( e . target . value === 'all' ? 'all' : parseInt ( e . target . value ) ) }
className = "h-7 bg-white/5 border border-white/20 rounded px-2 text-white text-xs"
>
< option value = "all" > В с е с т а т у с ы < / option >
{ uniqueStatuses . map ( ( status : number ) = > (
< option key = { status } value = { status } > { getCampaignStatusName ( status ) } < / option >
) ) }
< / select >
< / div >
{ /* Компактный список кампаний */ }
{ loading ? (
< Skeleton className = "h-20 bg-white/10" / >
) : error ? (
< Alert className = "bg-red-500/10 border-red-500/30 text-red-400 py-2" >
< AlertCircle className = "h-3 w-3" / >
< AlertDescription className = "text-xs" >
О ш и б к а : { error . message }
< / AlertDescription >
< / Alert >
) : (
< div className = "max-h-32 overflow-y-auto space-y-2" >
{ filteredCampaigns . map ( ( group : CampaignGroup ) = > (
< div key = { ` ${ group . type } - ${ group . status } ` } className = "bg-white/5 rounded p-2" >
< div className = "flex items-center justify-between mb-1" >
< div className = "flex items-center gap-2" >
< Checkbox
checked = { group . advert_list . every ( item = > selectedIds . has ( item . advertId ) ) }
onCheckedChange = { ( ) = > handleSelectAll ( group ) }
className = "h-3 w-3"
/ >
< span className = "text-xs font-medium text-white" >
{ getCampaignTypeName ( group . type ) }
< / span >
< span className = { ` text-xs ${ getStatusColor ( group . status ) } ` } >
{ getCampaignStatusName ( group . status ) }
< / span >
< Badge variant = "outline" className = "border-white/20 text-white text-xs px-1 py-0" >
{ group . count }
< / Badge >
< / div >
< / div >
< div className = "grid grid-cols-4 gap-1 ml-4" >
{ group . advert_list . map ( ( campaign ) = > (
< div
key = { campaign . advertId }
className = "flex items-center gap-1 p-1 bg-white/5 rounded cursor-pointer hover:bg-white/10 text-xs"
onClick = { ( ) = > handleCampaignToggle ( campaign . advertId ) }
>
< Checkbox
checked = { selectedIds . has ( campaign . advertId ) }
onCheckedChange = { ( ) = > handleCampaignToggle ( campaign . advertId ) }
className = "h-3 w-3"
/ >
< span className = "text-white truncate" > # { campaign . advertId } < / span >
< / div >
) ) }
< / div >
< / div >
) ) }
< / div >
) }
< / div >
) }
< / div >
) }
< / div >
< / Card >
)
}
// Компонент сверх-компактной таблицы кампаний
const UltraCompactCampaignTable = ( {
campaigns ,
expandedCampaigns ,
onToggleExpand
} : {
campaigns : CampaignStats [ ] ,
expandedCampaigns : Set < number > ,
onToggleExpand : ( id : number ) = > void
} ) = > {
const [ sortField , setSortField ] = useState < keyof CampaignStats > ( 'advertId' )
const [ sortDirection , setSortDirection ] = useState < 'asc' | 'desc' > ( 'desc' )
const formatCurrency = ( value : number ) = > {
if ( value === 0 ) return '—'
if ( value < 1000 ) return ` ${ value } ₽ `
if ( value < 1000000 ) return ` ${ ( value / 1000 ) . toFixed ( 1 ) } K₽ `
return ` ${ ( value / 1000000 ) . toFixed ( 1 ) } M₽ `
}
const formatNumber = ( value : number ) = > {
if ( value === 0 ) return '—'
if ( value < 1000 ) return value . toString ( )
if ( value < 1000000 ) return ` ${ ( value / 1000 ) . toFixed ( 1 ) } K `
return ` ${ ( value / 1000000 ) . toFixed ( 1 ) } M `
}
const formatPercent = ( value : number ) = > {
return value === 0 ? '—' : ` ${ value . toFixed ( 1 ) } % `
}
const getAppTypeIcon = ( appType : number ) = > {
const iconClass = "h-3 w-3"
switch ( appType ) {
case 1 : return < Smartphone className = { ` ${ iconClass } text-blue-400 ` } / >
case 2 : return < Monitor className = { ` ${ iconClass } text-green-400 ` } / >
case 3 : return < Globe className = { ` ${ iconClass } text-purple-400 ` } / >
case 32 : return < Monitor className = { ` ${ iconClass } text-emerald-400 ` } / >
case 64 : return < Globe className = { ` ${ iconClass } text-amber-400 ` } / >
default : return < Target className = { ` ${ iconClass } text-gray-400 ` } / >
}
}
const sortedCampaigns = [ . . . campaigns ] . sort ( ( a , b ) = > {
const aVal = a [ sortField ] as number
const bVal = b [ sortField ] as number
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal
} )
const handleSort = ( field : keyof CampaignStats ) = > {
if ( sortField === field ) {
setSortDirection ( sortDirection === 'asc' ? 'desc' : 'asc' )
} else {
setSortField ( field )
setSortDirection ( 'desc' )
}
}
return (
< div className = "space-y-2" >
{ /* Сверх-компактный заголовок таблицы */ }
< div className = "grid grid-cols-12 gap-1 p-2 bg-white/10 rounded text-xs font-medium text-white/80" >
< div
className = "col-span-2 flex items-center gap-1 cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'advertId' ) }
>
К а м п а н и я
< ArrowUpDown className = "h-3 w-3" / >
< / div >
< div
className = "text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'views' ) }
>
👁
< / div >
< div
className = "text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'clicks' ) }
>
🖱
< / div >
< div
className = "text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'ctr' ) }
>
CTR
< / div >
< div
className = "text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'cpc' ) }
>
CPC
< / div >
< div
className = "text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'sum' ) }
>
💰
< / div >
< div
className = "text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'orders' ) }
>
📦
< / div >
< div
className = "text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'cr' ) }
>
CR
< / div >
< div
className = "text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'shks' ) }
>
ш т
< / div >
< div
className = "col-span-2 text-center cursor-pointer hover:text-white"
onClick = { ( ) = > handleSort ( 'sum_price' ) }
>
В ы р у ч к а
< / div >
< div className = "text-center" > ROI < / div >
< / div >
{ /* Строки кампаний */ }
{ sortedCampaigns . map ( ( campaign ) = > {
const isExpanded = expandedCampaigns . has ( campaign . advertId )
const roi = campaign . sum > 0 ? ( ( campaign . sum_price - campaign . sum ) / campaign . sum * 100 ) : 0
return (
< div key = { campaign . advertId } className = "space-y-1" >
{ /* Основная строка кампании */ }
< div
className = "grid grid-cols-12 gap-1 p-2 bg-white/5 rounded border border-white/10 hover:bg-white/10 transition-colors cursor-pointer text-xs"
onClick = { ( ) = > onToggleExpand ( campaign . advertId ) }
>
< div className = "col-span-2 flex items-center gap-1" >
{ isExpanded ? < ChevronDown className = "h-3 w-3" / > : < ChevronRight className = "h-3 w-3" / > }
< span className = "font-medium text-white" > # { campaign . advertId } < / span >
< Badge variant = "outline" className = "border-white/20 text-white text-xs px-1 py-0" >
{ campaign . days . length } д
< / Badge >
< / div >
< div className = "text-center text-white" > { formatNumber ( campaign . views ) } < / div >
< div className = "text-center text-white" > { formatNumber ( campaign . clicks ) } < / div >
< div className = "text-center text-white" > { formatPercent ( campaign . ctr ) } < / div >
< div className = "text-center text-white" > { formatCurrency ( campaign . cpc ) } < / div >
< div className = "text-center text-white" > { formatCurrency ( campaign . sum ) } < / div >
< div className = "text-center text-white" > { formatNumber ( campaign . orders ) } < / div >
< div className = "text-center text-white" > { formatPercent ( campaign . cr ) } < / div >
< div className = "text-center text-white" > { formatNumber ( campaign . shks ) } < / div >
< div className = "col-span-2 text-center text-white" > { formatCurrency ( campaign . sum_price ) } < / div >
< div className = { ` text-center font-medium ${ roi > 0 ? 'text-green-400' : roi < 0 ? 'text-red-400' : 'text-gray-400' } ` } >
{ roi === 0 ? '—' : ` ${ roi > 0 ? '+' : '' } ${ roi . toFixed ( 0 ) } % ` }
< / div >
< / div >
{ /* Развернутое содержимое */ }
{ isExpanded && (
< div className = "ml-4 space-y-2" >
{ /* Компактная статистика по дням */ }
{ campaign . days . length > 0 && (
< div className = "bg-white/2 rounded p-2 border border-white/5" >
< h4 className = "text-xs font-medium text-white/70 mb-2 flex items-center gap-1" >
< Calendar className = "h-3 w-3" / >
П о д н я м ( { campaign . days . length } )
< / h4 >
< div className = "space-y-1 max-h-32 overflow-y-auto" >
{ campaign . days . map ( ( day , dayIndex ) = > (
< div key = { dayIndex } className = "grid grid-cols-11 gap-1 p-1 bg-white/5 rounded text-xs" >
< div className = "col-span-2 text-white/80" >
{ new Date ( day . date ) . toLocaleDateString ( 'ru-RU' , {
day : '2-digit' ,
month : '2-digit'
} ) }
< / div >
< div className = "text-center text-white" > { formatNumber ( day . views ) } < / div >
< div className = "text-center text-white" > { formatNumber ( day . clicks ) } < / div >
< div className = "text-center text-white" > { formatPercent ( day . ctr ) } < / div >
< div className = "text-center text-white" > { formatCurrency ( day . cpc ) } < / div >
< div className = "text-center text-white" > { formatCurrency ( day . sum ) } < / div >
< div className = "text-center text-white" > { formatNumber ( day . orders ) } < / div >
< div className = "text-center text-white" > { formatPercent ( day . cr ) } < / div >
< div className = "text-center text-white" > { formatNumber ( day . shks ) } < / div >
< div className = "text-center text-white" > { formatCurrency ( day . sum_price ) } < / div >
< / div >
) ) }
< / div >
< / div >
) }
{ /* Компактная статистика по платформам */ }
{ campaign . days . some ( day = > day . apps && day . apps . length > 0 ) && (
< div className = "bg-white/2 rounded p-2 border border-white/5" >
< h4 className = "text-xs font-medium text-white/70 mb-2 flex items-center gap-1" >
< BarChart3 className = "h-3 w-3" / >
П л а т ф о р м ы
< / h4 >
< div className = "space-y-1" >
{ campaign . days . flatMap ( day = > day . apps || [ ] )
. reduce ( ( acc : CampaignApp [ ] , app ) = > {
const existing = acc . find ( a = > a . appType === app . appType )
if ( existing ) {
existing . views += app . views
existing . clicks += app . clicks
existing . sum += app . sum
existing . orders += app . orders
existing . sum_price += app . sum_price
} else {
acc . push ( { . . . app } )
}
return acc
} , [ ] )
. map ( ( app , appIndex ) = > (
< div key = { appIndex } className = "grid grid-cols-10 gap-1 p-1 bg-white/5 rounded text-xs" >
< div className = "col-span-2 flex items-center gap-1" >
{ getAppTypeIcon ( app . appType ) }
< span className = "text-white/80 truncate" >
{ app . appType === 1 ? 'Мобайл' :
app . appType === 32 ? 'Десктоп' :
app . appType === 64 ? 'М о б .WB' : ` Тип ${ app . appType } ` }
< / span >
< / div >
< div className = "text-center text-white" > { formatNumber ( app . views ) } < / div >
< div className = "text-center text-white" > { formatNumber ( app . clicks ) } < / div >
< div className = "text-center text-white" > { formatPercent ( app . ctr ) } < / div >
< div className = "text-center text-white" > { formatCurrency ( app . sum ) } < / div >
< div className = "text-center text-white" > { formatNumber ( app . orders ) } < / div >
< div className = "text-center text-white" > { formatPercent ( app . cr ) } < / div >
< div className = "text-center text-white" > { formatNumber ( app . shks ) } < / div >
< div className = "text-center text-white" > { formatCurrency ( app . sum_price ) } < / div >
< / div >
) ) }
< / div >
< / div >
) }
{ /* Позиции товаров */ }
{ campaign . boosterStats . length > 0 && (
< div className = "bg-white/2 rounded p-2 border border-white/5" >
< h4 className = "text-xs font-medium text-white/70 mb-2 flex items-center gap-1" >
< TrendingUp className = "h-3 w-3" / >
П о з и ц и и
< / h4 >
< div className = "grid grid-cols-3 gap-1" >
{ campaign . boosterStats . slice ( 0 , 6 ) . map ( ( booster , index ) = > (
< div key = { index } className = "bg-white/5 rounded p-1 text-xs" >
< div className = "text-white/60" >
{ new Date ( booster . date ) . toLocaleDateString ( 'ru-RU' , { day : '2-digit' , month : '2-digit' } ) }
< / div >
< div className = "text-white font-medium" >
# { booster . avg_position }
< / div >
< / div >
) ) }
< / div >
< / div >
) }
< / div >
) }
< / div >
)
} ) }
< / div >
)
}
export function AdvertisingTab ( { selectedPeriod , useCustomDates , startDate , endDate } : CampaignStatsProps ) {
const [ c ampaignIds, setCampaignIds ] = useState ( '' )
const [ selectedC ampaignIds, setSelected CampaignIds ] = useState < number [ ] > ( [ ] )
const [ campaignStats , setCampaignStats ] = useState < CampaignStats [ ] > ( [ ] )
const [ expandedCampaigns , setExpandedCampaigns ] = useState < Set < number > > ( new Set ( ) )
const [ showChart , setShowChart ] = useState ( false )
const [ getCampaignStats , { loading , error } ] = useLazyQuery ( GET_WILDBERRIES_CAMPAIGN_STATS , {
onCompleted : ( data ) = > {
@ -83,15 +675,13 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
}
} )
const handleGetStats = ( ) = > {
if ( ! campaignIds . trim ( ) ) return
const ids = campaignIds . split ( ',' ) . map ( id = > parseInt ( id . trim ( ) ) ) . filter ( id = > ! isNaN ( id ) )
const handleCampaignsSelected = ( ids : number [ ] ) = > {
if ( ids . length === 0 ) return
setSelectedCampaignIds ( ids )
let campaigns
if ( useCustomDates && startDate && endDate ) {
// Используем интервал для пользовательских дат
campaigns = ids . map ( id = > ( {
id ,
interval : {
@ -100,7 +690,6 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
}
} ) )
} else {
// Для предустановленных периодов вычисляем даты
const endDateCalc = new Date ( )
const startDateCalc = new Date ( )
@ -132,6 +721,16 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
} )
}
const toggleCampaignExpanded = ( campaignId : number ) = > {
const newExpanded = new Set ( expandedCampaigns )
if ( newExpanded . has ( campaignId ) ) {
newExpanded . delete ( campaignId )
} else {
newExpanded . add ( campaignId )
}
setExpandedCampaigns ( newExpanded )
}
const formatCurrency = ( value : number ) = > {
return new Intl . NumberFormat ( 'ru-RU' , {
style : 'currency' ,
@ -178,46 +777,19 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
}
return (
< div className = "h-full flex flex-col space-y-4 overflow-hidden" >
{ /* Форма поиска кампаний */ }
< Card className = "glass-card flex-shrink-0 p-4" >
< div className = "flex items-center gap-4" >
< div className = "flex-1" >
< label className = "block text-sm font-medium text-white/80 mb-2" >
ID к а м п а н и й ( ч е р е з з а п я т у ю )
< / label >
< Input
placeholder = "Например: 12345, 67890, 11111"
value = { campaignIds }
onChange = { ( e ) = > setCampaignIds ( e . target . value ) }
className = "bg-white/5 border-white/20 text-white placeholder:text-white/40"
< div className = "h-full flex flex-col space-y-3 overflow-hidden" >
{ /* Компактный селектор кампаний */ }
< CompactCampaignSelector
onCampaignsSelected = { handleCampaignsSelected }
selectedCampaigns = { selectedCampaignIds }
loading = { loading }
/ >
< / div >
< Button
onClick = { handleGetStats }
disabled = { loading || ! campaignIds . trim ( ) }
className = "bg-white/10 hover:bg-white/20 border border-white/20 text-white flex items-center gap-2 mt-6"
>
{ loading ? (
< >
< div className = "animate-spin rounded-full h-4 w-4 border-b-2 border-white" / >
З а г р у з к а . . .
< / >
) : (
< >
< Search className = "h-4 w-4" / >
П о л у ч и т ь с т а т и с т и к у
< / >
) }
< / Button >
< / div >
< / Card >
{ /* Ошибки */ }
{ error && (
< Alert className = "bg-red-500/10 border-red-500/30 text-red-400" >
< AlertCircle className = "h-4 w-4 " / >
< AlertDescription >
< Alert className = "bg-red-500/10 border-red-500/30 text-red-400 py-2 " >
< AlertCircle className = "h-3 w-3 " / >
< AlertDescription className = "text-xs" >
{ error . message }
< / AlertDescription >
< / Alert >
@ -226,57 +798,61 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
{ /* Результаты */ }
< div className = "flex-1 overflow-auto" >
{ loading ? (
< div className = "space-y-4 " >
< div className = "space-y-2 " >
{ [ 1 , 2 , 3 ] . map ( ( i ) = > (
< Card key = { i } className = "glass-card p-6" >
< Skeleton className = "h-8 w-48 mb-4 bg-white/10" / >
< div className = "grid grid-cols-4 gap-4" >
{ [ 1 , 2 , 3 , 4 ] . map ( ( j ) = > (
< Skeleton key = { j } className = "h-16 bg-white/5" / >
) ) }
< / div >
< / Card >
< Skeleton key = { i } className = "h-12 bg-white/10" / >
) ) }
< / div >
) : campaignStats . length > 0 ? (
< div className = "space-y-6 " >
{ /* О бщая статистика по всем кампаниям */ }
< Card className = "glass-card p-6 " >
< h3 className = "text-lg font-semibold text-white mb-4 flex items-center gap-2 " >
< TrendingUp className = "h-5 w-5" / >
О б щ а я с т а т и с т и к а ( { campaignStats . length } к а м п а н и й )
< div className = "space-y-3 " >
{ /* Компактная о бщая статистика */ }
< Card className = "glass-card p-3 " >
< div className = "flex items-center justify-between mb-3 " >
< h3 className = "text-sm font-semibold text-white flex items-center gap-2" >
< TrendingUp className = "h-4 w-4" / >
С в о д к а ( { campaignStats . length } к а м п а н и й )
< / h3 >
< div className = "grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-4 mb-6" >
{ /* Показы */ }
< div className = "bg-white/5 rounded-lg p-4" >
< div className = "flex items-center gap-2 mb-1" >
< Eye className = "h-4 w-4 text-purple-400" / >
< span className = "text-sm text-white/60" > П о к а з ы < / span >
< Button
variant = "ghost"
size = "sm"
onClick = { ( ) = > setShowChart ( ! showChart ) }
className = "h-6 px-2 text-xs text-white/60 hover:bg-white/10"
>
{ showChart ? < Minimize2 className = "h-3 w-3" / > : < BarChart3 className = "h-3 w-3" / > }
{ showChart ? 'Скрыть' : 'График' }
< / Button >
< / div >
< div className = "text-xl font-bold text-white" >
< div className = "grid grid-cols-4 md:grid-cols-8 gap-2 mb-3" >
{ /* Показы */ }
< div className = "bg-white/5 rounded p-2" >
< div className = "flex items-center gap-1 mb-1" >
< Eye className = "h-3 w-3 text-purple-400" / >
< span className = "text-xs text-white/60" > П о к а з ы < / span >
< / div >
< div className = "text-sm font-bold text-white" >
{ formatNumber ( campaignStats . reduce ( ( sum , stat ) = > sum + stat . views , 0 ) ) }
< / div >
< / div >
{ /* Клики */ }
< div className = "bg-white/5 rounded-lg p-4 " >
< div className = "flex items-center gap-2 mb-1" >
< MousePointer className = "h-4 w-4 text-cyan-400" / >
< span className = "text-sm text-white/60" > К л и к и < / span >
< div className = "bg-white/5 rounded p-2 " >
< div className = "flex items-center gap-1 mb-1" >
< MousePointer className = "h-3 w-3 text-cyan-400" / >
< span className = "text-x s text-white/60" > К л и к и < / span >
< / div >
< div className = "text-xl font-bold text-white" >
< div className = "text-sm font-bold text-white" >
{ formatNumber ( campaignStats . reduce ( ( sum , stat ) = > sum + stat . clicks , 0 ) ) }
< / div >
< / div >
{ /* CTR */ }
< div className = "bg-white/5 rounded-lg p-4 " >
< div className = "flex items-center gap-2 mb-1" >
< Percent className = "h-4 w-4 text-green-400" / >
< span className = "text-sm text-white/60" > CTR < / span >
< div className = "bg-white/5 rounded p-2 " >
< div className = "flex items-center gap-1 mb-1" >
< Percent className = "h-3 w-3 text-green-400" / >
< span className = "text-x s text-white/60" > CTR < / span >
< / div >
< div className = "text-xl font-bold text-white" >
< div className = "text-sm font-bold text-white" >
{ formatPercent (
campaignStats . reduce ( ( sum , stat , _ , arr ) = > {
const totalViews = arr . reduce ( ( s , st ) = > s + st . views , 0 )
@ -288,12 +864,12 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
< / div >
{ /* CPC */ }
< div className = "bg-white/5 rounded-lg p-4 " >
< div className = "flex items-center gap-2 mb-1" >
< DollarSign className = "h-4 w-4 text-amber-400" / >
< span className = "text-sm text-white/60" > CPC < / span >
< div className = "bg-white/5 rounded p-2 " >
< div className = "flex items-center gap-1 mb-1" >
< DollarSign className = "h-3 w-3 text-amber-400" / >
< span className = "text-x s text-white/60" > CPC < / span >
< / div >
< div className = "text-xl font-bold text-white" >
< div className = "text-sm font-bold text-white" >
{ formatCurrency (
campaignStats . reduce ( ( sum , stat , _ , arr ) = > {
const totalClicks = arr . reduce ( ( s , st ) = > s + st . clicks , 0 )
@ -305,34 +881,34 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
< / div >
{ /* Затраты */ }
< div className = "bg-white/5 rounded-lg p-4 " >
< div className = "flex items-center gap-2 mb-1" >
< DollarSign className = "h-4 w-4 text-red-400" / >
< span className = "text-sm text-white/60" > З а т р а т ы < / span >
< div className = "bg-white/5 rounded p-2 " >
< div className = "flex items-center gap-1 mb-1" >
< DollarSign className = "h-3 w-3 text-red-400" / >
< span className = "text-x s text-white/60" > З а т р а т ы < / span >
< / div >
< div className = "text-xl font-bold text-white" >
< div className = "text-sm font-bold text-white" >
{ formatCurrency ( campaignStats . reduce ( ( sum , stat ) = > sum + stat . sum , 0 ) ) }
< / div >
< / div >
{ /* Заказы */ }
< div className = "bg-white/5 rounded-lg p-4 " >
< div className = "flex items-center gap-2 mb-1" >
< ShoppingCart className = "h-4 w-4 text-green-400" / >
< span className = "text-sm text-white/60" > З а к а з ы < / span >
< div className = "bg-white/5 rounded p-2 " >
< div className = "flex items-center gap-1 mb-1" >
< ShoppingCart className = "h-3 w-3 text-green-400" / >
< span className = "text-x s text-white/60" > З а к а з ы < / span >
< / div >
< div className = "text-xl font-bold text-white" >
< div className = "text-sm font-bold text-white" >
{ formatNumber ( campaignStats . reduce ( ( sum , stat ) = > sum + stat . orders , 0 ) ) }
< / div >
< / div >
{ /* CR */ }
< div className = "bg-white/5 rounded-lg p-4 " >
< div className = "flex items-center gap-2 mb-1" >
< Percent className = "h-4 w-4 text-blue-400" / >
< span className = "text-sm text-white/60" > CR < / span >
< div className = "bg-white/5 rounded p-2 " >
< div className = "flex items-center gap-1 mb-1" >
< Percent className = "h-3 w-3 text-blue-400" / >
< span className = "text-x s text-white/60" > CR < / span >
< / div >
< div className = "text-xl font-bold text-white" >
< div className = "text-sm font-bold text-white" >
{ formatPercent (
campaignStats . reduce ( ( sum , stat , _ , arr ) = > {
const totalClicks = arr . reduce ( ( s , st ) = > s + st . clicks , 0 )
@ -344,63 +920,54 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
< / div >
{ /* Выручка */ }
< div className = "bg-white/5 rounded-lg p-4 " >
< div className = "flex items-center gap-2 mb-1" >
< DollarSign className = "h-4 w-4 text-emerald-400" / >
< span className = "text-sm text-white/60" > В ы р у ч к а < / span >
< div className = "bg-white/5 rounded p-2 " >
< div className = "flex items-center gap-1 mb-1" >
< DollarSign className = "h-3 w-3 text-emerald-400" / >
< span className = "text-x s text-white/60" > В ы р у ч к а < / span >
< / div >
< div className = "text-xl font-bold text-white" >
< div className = "text-sm font-bold text-white" >
{ formatCurrency ( campaignStats . reduce ( ( sum , stat ) = > sum + stat . sum_price , 0 ) ) }
< / div >
< / div >
< / div >
{ /* Г рафик */ }
{ chartData . length > 0 && (
< div className = "mt-6 " >
< h4 className = "text-md font-medium text-white mb-4" > Д и н а м и к а п о д н я м < / h4 >
< div className = "h-80" >
{ /* Компактный г рафик */ }
{ showChart && chartData . length > 0 && (
< div className = "mt-3 " >
< div className = "h-32" >
< ChartContainer config = { chartConfig } >
< LineChart data = { chartData } >
< CartesianGrid strokeDasharray = "3 3" stroke = "rgba(255,255,255,0.1)" / >
< XAxis
dataKey = "date"
tick = { { fill : 'rgba(255,255,255,0.6)' , fontSize : 12 } }
tick = { { fill : 'rgba(255,255,255,0.6)' , fontSize : 10 } }
axisLine = { false }
/ >
< YAxis
tick = { { fill : 'rgba(255,255,255,0.6)' , fontSize : 12 } }
tick = { { fill : 'rgba(255,255,255,0.6)' , fontSize : 10 } }
axisLine = { false }
/ >
< ChartTooltip content = { < ChartTooltipContent / > } / >
< ChartLegend content = { < ChartLegendContent / > } / >
< Line
type = "monotone"
dataKey = "views"
stroke = "#8b5cf6"
strokeWidth = { 2 }
dot = { { fill : '#8b5cf6' , strokeWidth : 2 , r : 4 } }
strokeWidth = { 1 }
dot = { { fill : '#8b5cf6' , strokeWidth : 1 , r : 2 } }
/ >
< Line
type = "monotone"
dataKey = "clicks"
stroke = "#06b6d4"
strokeWidth = { 2 }
dot = { { fill : '#06b6d4' , strokeWidth : 2 , r : 4 } }
/ >
< Line
type = "monotone"
dataKey = "sum"
stroke = "#f59e0b"
strokeWidth = { 2 }
dot = { { fill : '#f59e0b' , strokeWidth : 2 , r : 4 } }
strokeWidth = { 1 }
dot = { { fill : '#06b6d4' , strokeWidth : 1 , r : 2 } }
/ >
< Line
type = "monotone"
dataKey = "orders"
stroke = "#10b981"
strokeWidth = { 2 }
dot = { { fill : '#10b981' , strokeWidth : 2 , r : 4 } }
strokeWidth = { 1 }
dot = { { fill : '#10b981' , strokeWidth : 1 , r : 2 } }
/ >
< / LineChart >
< / ChartContainer >
@ -409,61 +976,24 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
) }
< / Card >
{ /* Деталь ная с татистика по каждой кампании */ }
{ campaignStats . map ( ( campaign ) = > (
< Card key = { campaign . advertId } className = "glass-card p-6 ">
< div className = "flex items-center justify-between mb-4 " >
< h 3 className = "text-lg font-semibold text-white" >
К а м п а н и я # { campaign . advertId }
{ /* Сверх-компакт ная таблица кампаний */ }
< Card className = "glass-card p-3" >
< div className = "flex items-center justify-between mb-3 ">
< h3 className = "text-sm font-semibold text-white flex items-center gap-2 " >
< BarChart 3 className = "h-4 w-4" / >
Д е т а л ь н а я с т а т и с т и к а
< / h3 >
< Badge variant = "outline" className = "border-white/20 text-white" >
{ campaign . days . length } д н е й
< / Badge >
< div className = "text-xs text-white/60 " >
{ campaignStats . reduce ( ( sum , stat ) = > sum + stat . days . length , 0 ) } д н е й д а н н ы х
< / div >
< / div >
< div className = "grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-4" >
< div className = "bg-white/5 rounded-lg p-3" >
< div className = "text-sm text-white/60 mb-1" > П о к а з ы < / div >
< div className = "font-bold text-white" > { formatNumber ( campaign . views ) } < / div >
< / div >
< div className = "bg-white/5 rounded-lg p-3" >
< div className = "text-sm text-white/60 mb-1" > К л и к и < / div >
< div className = "font-bold text-white" > { formatNumber ( campaign . clicks ) } < / div >
< / div >
< div className = "bg-white/5 rounded-lg p-3" >
< div className = "text-sm text-white/60 mb-1" > CTR < / div >
< div className = "font-bold text-white" > { formatPercent ( campaign . ctr ) } < / div >
< / div >
< div className = "bg-white/5 rounded-lg p-3" >
< div className = "text-sm text-white/60 mb-1" > CPC < / div >
< div className = "font-bold text-white" > { formatCurrency ( campaign . cpc ) } < / div >
< / div >
< div className = "bg-white/5 rounded-lg p-3" >
< div className = "text-sm text-white/60 mb-1" > З а т р а т ы < / div >
< div className = "font-bold text-white" > { formatCurrency ( campaign . sum ) } < / div >
< / div >
< div className = "bg-white/5 rounded-lg p-3" >
< div className = "text-sm text-white/60 mb-1" > З а к а з ы < / div >
< div className = "font-bold text-white" > { formatNumber ( campaign . orders ) } < / div >
< / div >
< div className = "bg-white/5 rounded-lg p-3" >
< div className = "text-sm text-white/60 mb-1" > CR < / div >
< div className = "font-bold text-white" > { formatPercent ( campaign . cr ) } < / div >
< / div >
< div className = "bg-white/5 rounded-lg p-3" >
< div className = "text-sm text-white/60 mb-1" > В ы р у ч к а < / div >
< div className = "font-bold text-white" > { formatCurrency ( campaign . sum_price ) } < / div >
< / div >
< / div >
< UltraCompactCampaignTable
campaigns = { campaignStats }
expandedCampaigns = { expandedCampaigns }
onToggleExpand = { toggleCampaignExpanded }
/ >
< / Card >
) ) }
< / div >
) : (
< Card className = "glass-card h-full overflow-hidden p-6" >
@ -471,7 +1001,7 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
< div className = "text-center" >
< TrendingUp className = "h-12 w-12 text-white/40 mx-auto mb-4" / >
< h3 className = "text-lg font-semibold text-white mb-2" > С т а т и с т и к а р е к л а м н ы х к а м п а н и й < / h3 >
< p className = "text-white/60 mb-4" > В в е д и т е ID к а м п а н и й д л я п о л у ч е н и я д е т а л ь н о й с т а т и с т и к и < / p >
< p className = "text-white/60 mb-4" > В ы б е р и т е к а м п а н и и д л я п о л у ч е н и я д е т а л ь н о й с т а т и с т и к и < / p >
< p className = "text-white/40 text-sm" >
П о д д е р ж и в а е т с я API Wildberries / adv / v2 / fullstats
< / p >