From 6fbb5702821a0fb11c812b3287054bd8add0ae61 Mon Sep 17 00:00:00 2001 From: Bivekich Date: Mon, 28 Jul 2025 13:19:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=B0=D0=BA=D1=82=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC=D0=BF=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=B2=20AdvertisingTab,?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=82=D0=B8=D0=BF=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20API.=20?= =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20?= =?UTF-8?q?GraphQL=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=BF=D0=B0=D0=BD=D0=B8=D0=B9=20Wildberries.=20=D0=9E=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=D0=BF=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85.=20=D0=9E=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D0=BE=D0=BB=D0=B2=D0=B5=D1=80=D1=8B=20=D0=B8=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B8=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seller-statistics/advertising-tab.tsx | 1018 +++++++++++++---- src/graphql/queries.ts | 65 +- src/graphql/resolvers.ts | 67 ++ src/graphql/typedefs.ts | 73 +- 4 files changed, 966 insertions(+), 257 deletions(-) diff --git a/src/components/seller-statistics/advertising-tab.tsx b/src/components/seller-statistics/advertising-tab.tsx index 76fff3a..37425cf 100644 --- a/src/components/seller-statistics/advertising-tab.tsx +++ b/src/components/seller-statistics/advertising-tab.tsx @@ -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: Array<{ - 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<{ - date: 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: CampaignDay[] + boosterStats: BoosterStat[] +} + +interface CampaignStatsProps { + selectedPeriod: string + useCustomDates: boolean + startDate: string + endDate: string +} + +// Интерфейсы для списка кампаний +interface CampaignListItem { + advertId: number + changeTime: 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>(new Set(selectedCampaigns)) + const [filterType, setFilterType] = useState('all') + const [filterStatus, setFilterStatus] = useState('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 = { + 4: 'Авто', + 5: 'Фразы', + 6: 'Предмет', + 7: 'Бренд', + 8: 'Медиа', + 9: 'Карусель' + } + return types[type] || `Тип ${type}` + } + + const getCampaignStatusName = (status: number) => { + const statuses: Record = { + 7: 'Завершена', + 8: 'Отклонена', + 9: 'Активна', + 11: 'На паузе' + } + return statuses[status] || `Статус ${status}` + } + + const getStatusColor = (status: number) => { + const colors: Record = { + 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 ( + +
+ {/* Компактный заголовок */} +
+
+ + + {selectedIds.size} + +
+ +
+ + +
+
+ + {/* Развернутый контент */} + {isExpanded && ( +
+ {showManualInput ? ( + setManualIds(e.target.value)} + className="h-8 bg-white/5 border-white/20 text-white placeholder:text-white/40 text-xs" + /> + ) : ( +
+ {/* Компактные фильтры */} +
+ + + +
+ + {/* Компактный список кампаний */} + {loading ? ( + + ) : error ? ( + + + + Ошибка: {error.message} + + + ) : ( +
+ {filteredCampaigns.map((group: CampaignGroup) => ( +
+
+
+ selectedIds.has(item.advertId))} + onCheckedChange={() => handleSelectAll(group)} + className="h-3 w-3" + /> + + {getCampaignTypeName(group.type)} + + + {getCampaignStatusName(group.status)} + + + {group.count} + +
+
+ +
+ {group.advert_list.map((campaign) => ( +
handleCampaignToggle(campaign.advertId)} + > + handleCampaignToggle(campaign.advertId)} + className="h-3 w-3" + /> + #{campaign.advertId} +
+ ))} +
+
+ ))} +
+ )} +
+ )} +
+ )} +
+
+ ) +} + +// Компонент сверх-компактной таблицы кампаний +const UltraCompactCampaignTable = ({ + campaigns, + expandedCampaigns, + onToggleExpand +}: { + campaigns: CampaignStats[], + expandedCampaigns: Set, + onToggleExpand: (id: number) => void +}) => { + const [sortField, setSortField] = useState('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 + case 2: return + case 3: return + case 32: return + case 64: return + default: return + } + } + + 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 ( +
+ {/* Сверх-компактный заголовок таблицы */} +
+
handleSort('advertId')} + > + Кампания + +
+
handleSort('views')} + > + 👁 +
+
handleSort('clicks')} + > + 🖱 +
+
handleSort('ctr')} + > + CTR +
+
handleSort('cpc')} + > + CPC +
+
handleSort('sum')} + > + 💰 +
+
handleSort('orders')} + > + 📦 +
+
handleSort('cr')} + > + CR +
+
handleSort('shks')} + > + шт +
+
handleSort('sum_price')} + > + Выручка +
+
ROI
+
+ + {/* Строки кампаний */} + {sortedCampaigns.map((campaign) => { + const isExpanded = expandedCampaigns.has(campaign.advertId) + const roi = campaign.sum > 0 ? ((campaign.sum_price - campaign.sum) / campaign.sum * 100) : 0 + + return ( +
+ {/* Основная строка кампании */} +
onToggleExpand(campaign.advertId)} + > +
+ {isExpanded ? : } + #{campaign.advertId} + + {campaign.days.length}д + +
+ +
{formatNumber(campaign.views)}
+
{formatNumber(campaign.clicks)}
+
{formatPercent(campaign.ctr)}
+
{formatCurrency(campaign.cpc)}
+
{formatCurrency(campaign.sum)}
+
{formatNumber(campaign.orders)}
+
{formatPercent(campaign.cr)}
+
{formatNumber(campaign.shks)}
+
{formatCurrency(campaign.sum_price)}
+
0 ? 'text-green-400' : roi < 0 ? 'text-red-400' : 'text-gray-400'}`}> + {roi === 0 ? '—' : `${roi > 0 ? '+' : ''}${roi.toFixed(0)}%`} +
+
+ + {/* Развернутое содержимое */} + {isExpanded && ( +
+ {/* Компактная статистика по дням */} + {campaign.days.length > 0 && ( +
+

+ + По дням ({campaign.days.length}) +

+ +
+ {campaign.days.map((day, dayIndex) => ( +
+
+ {new Date(day.date).toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit' + })} +
+
{formatNumber(day.views)}
+
{formatNumber(day.clicks)}
+
{formatPercent(day.ctr)}
+
{formatCurrency(day.cpc)}
+
{formatCurrency(day.sum)}
+
{formatNumber(day.orders)}
+
{formatPercent(day.cr)}
+
{formatNumber(day.shks)}
+
{formatCurrency(day.sum_price)}
+
+ ))} +
+
+ )} + + {/* Компактная статистика по платформам */} + {campaign.days.some(day => day.apps && day.apps.length > 0) && ( +
+

+ + Платформы +

+ +
+ {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) => ( +
+
+ {getAppTypeIcon(app.appType)} + + {app.appType === 1 ? 'Мобайл' : + app.appType === 32 ? 'Десктоп' : + app.appType === 64 ? 'Моб.WB' : `Тип${app.appType}`} + +
+
{formatNumber(app.views)}
+
{formatNumber(app.clicks)}
+
{formatPercent(app.ctr)}
+
{formatCurrency(app.sum)}
+
{formatNumber(app.orders)}
+
{formatPercent(app.cr)}
+
{formatNumber(app.shks)}
+
{formatCurrency(app.sum_price)}
+
+ ))} +
+
+ )} + + {/* Позиции товаров */} + {campaign.boosterStats.length > 0 && ( +
+

+ + Позиции +

+
+ {campaign.boosterStats.slice(0, 6).map((booster, index) => ( +
+
+ {new Date(booster.date).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit' })} +
+
+ #{booster.avg_position} +
+
+ ))} +
+
+ )} +
+ )} +
+ ) + })} +
+ ) } export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endDate }: CampaignStatsProps) { - const [campaignIds, setCampaignIds] = useState('') + const [selectedCampaignIds, setSelectedCampaignIds] = useState([]) const [campaignStats, setCampaignStats] = useState([]) + const [expandedCampaigns, setExpandedCampaigns] = useState>(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', @@ -164,7 +763,7 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD color: "#8b5cf6", }, clicks: { - label: "Клики", + label: "Клики", color: "#06b6d4", }, sum: { @@ -178,46 +777,19 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD } return ( -
- {/* Форма поиска кампаний */} - -
-
- - setCampaignIds(e.target.value)} - className="bg-white/5 border-white/20 text-white placeholder:text-white/40" - /> -
- -
-
+
+ {/* Компактный селектор кампаний */} + {/* Ошибки */} {error && ( - - - + + + {error.message} @@ -226,57 +798,61 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD {/* Результаты */}
{loading ? ( -
+
{[1, 2, 3].map((i) => ( - - -
- {[1, 2, 3, 4].map((j) => ( - - ))} -
-
+ ))}
) : campaignStats.length > 0 ? ( -
- {/* Общая статистика по всем кампаниям */} - -

- - Общая статистика ({campaignStats.length} кампаний) -

+
+ {/* Компактная общая статистика */} + +
+

+ + Сводка ({campaignStats.length} кампаний) +

+ +
-
+
{/* Показы */} -
-
- - Показы +
+
+ + Показы
-
+
{formatNumber(campaignStats.reduce((sum, stat) => sum + stat.views, 0))}
{/* Клики */} -
-
- - Клики +
+
+ + Клики
-
+
{formatNumber(campaignStats.reduce((sum, stat) => sum + stat.clicks, 0))}
{/* CTR */} -
-
- - CTR +
+
+ + CTR
-
+
{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
{/* CPC */} -
-
- - CPC +
+
+ + CPC
-
+
{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
{/* Затраты */} -
-
- - Затраты +
+
+ + Затраты
-
+
{formatCurrency(campaignStats.reduce((sum, stat) => sum + stat.sum, 0))}
{/* Заказы */} -
-
- - Заказы +
+
+ + Заказы
-
+
{formatNumber(campaignStats.reduce((sum, stat) => sum + stat.orders, 0))}
{/* CR */} -
-
- - CR +
+
+ + CR
-
+
{formatPercent( campaignStats.reduce((sum, stat, _, arr) => { const totalClicks = arr.reduce((s, st) => s + st.clicks, 0) @@ -344,126 +920,80 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD
{/* Выручка */} -
-
- - Выручка +
+
+ + Выручка
-
+
{formatCurrency(campaignStats.reduce((sum, stat) => sum + stat.sum_price, 0))}
- {/* График */} - {chartData.length > 0 && ( -
-

Динамика по дням

-
- - - - - - } /> - } /> - - - - - - -
-
- )} + {/* Компактный график */} + {showChart && chartData.length > 0 && ( +
+
+ + + + + + } /> + + + + + +
+
+ )} - {/* Детальная статистика по каждой кампании */} - {campaignStats.map((campaign) => ( - -
-

- Кампания #{campaign.advertId} -

- - {campaign.days.length} дней - + {/* Сверх-компактная таблица кампаний */} + +
+

+ + Детальная статистика +

+
+ {campaignStats.reduce((sum, stat) => sum + stat.days.length, 0)} дней данных
- -
-
-
Показы
-
{formatNumber(campaign.views)}
-
- -
-
Клики
-
{formatNumber(campaign.clicks)}
-
- -
-
CTR
-
{formatPercent(campaign.ctr)}
-
- -
-
CPC
-
{formatCurrency(campaign.cpc)}
-
- -
-
Затраты
-
{formatCurrency(campaign.sum)}
-
- -
-
Заказы
-
{formatNumber(campaign.orders)}
-
- -
-
CR
-
{formatPercent(campaign.cr)}
-
- -
-
Выручка
-
{formatCurrency(campaign.sum_price)}
-
-
- - ))} +
+ + +
) : ( @@ -471,7 +1001,7 @@ export function AdvertisingTab({ selectedPeriod, useCustomDates, startDate, endD

Статистика рекламных кампаний

-

Введите ID кампаний для получения детальной статистики

+

Выберите кампании для получения детальной статистики

Поддерживается API Wildberries /adv/v2/fullstats

diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index e4ef1bc..7ccbb26 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -712,7 +712,10 @@ export const GET_WILDBERRIES_CAMPAIGN_STATS = gql` cr shks sum_price - dates + interval { + begin + end + } days { date views @@ -725,25 +728,65 @@ export const GET_WILDBERRIES_CAMPAIGN_STATS = gql` cr shks sum_price + apps { + views + clicks + ctr + cpc + sum + atbs + orders + cr + shks + sum_price + appType + nm { + views + clicks + ctr + cpc + sum + atbs + orders + cr + shks + sum_price + name + nmId + } + } } boosterStats { date - views - clicks - ctr - cpc - sum - atbs - orders - cr - shks - sum_price + nm + avg_position } } } } ` +export const GET_WILDBERRIES_CAMPAIGNS_LIST = gql` + query GetWildberriesCampaignsList { + getWildberriesCampaignsList { + success + message + data { + adverts { + type + status + count + advert_list { + advertId + changeTime + } + } + all + } + } + } +` + // Админ запросы export const ADMIN_ME = gql` query AdminMe { diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index e25bb4c..983988e 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -5836,6 +5836,73 @@ const wildberriesQueries = { }; } }, + + getWildberriesCampaignsList: async ( + _: unknown, + __: unknown, + context: Context + ) => { + if (!context.user) { + throw new GraphQLError("Требуется авторизация", { + extensions: { code: "UNAUTHENTICATED" }, + }); + } + + try { + // Получаем организацию пользователя и её WB API ключ + const user = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { + organization: { + include: { + apiKeys: true, + }, + }, + }, + }); + + if (!user?.organization) { + throw new GraphQLError("Организация не найдена"); + } + + if (user.organization.type !== "SELLER") { + throw new GraphQLError("Доступно только для продавцов"); + } + + const wbApiKeyRecord = user.organization.apiKeys?.find( + (key) => key.marketplace === "WILDBERRIES" && key.isActive + ); + + if (!wbApiKeyRecord) { + throw new GraphQLError("WB API ключ не настроен"); + } + + // Создаем экземпляр сервиса + const wbService = new WildberriesService(wbApiKeyRecord.apiKey); + + // Получаем список кампаний + const campaignsList = await wbService.getCampaignsList(); + + return { + success: true, + data: campaignsList, + message: null, + }; + } catch (error) { + console.error("Error fetching WB campaigns list:", error); + return { + success: false, + message: + error instanceof Error + ? error.message + : "Ошибка получения списка кампаний", + data: { + adverts: [], + all: 0, + }, + }; + } + }, }; // Добавляем админ запросы и мутации к основным резолверам diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index 0411bc1..6ab694e 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -102,6 +102,9 @@ export const typeDefs = gql` getWildberriesCampaignStats( input: WildberriesCampaignStatsInput! ): WildberriesCampaignStatsResponse! + + # Список кампаний Wildberries + getWildberriesCampaignsList: WildberriesCampaignsListResponse! } type Mutation { @@ -1057,9 +1060,14 @@ export const typeDefs = gql` cr: Float! shks: Int! sum_price: Float! - dates: [String!]! + interval: WildberriesCampaignInterval days: [WildberriesCampaignDayStats!]! - boosterStats: [WildberriesCampaignDayStats!]! + boosterStats: [WildberriesBoosterStats!]! + } + + type WildberriesCampaignInterval { + begin: String! + end: String! } type WildberriesCampaignDayStats { @@ -1074,5 +1082,66 @@ export const typeDefs = gql` cr: Float! shks: Int! sum_price: Float! + apps: [WildberriesAppStats!] + } + + type WildberriesAppStats { + views: Int! + clicks: Int! + ctr: Float! + cpc: Float! + sum: Float! + atbs: Int! + orders: Int! + cr: Float! + shks: Int! + sum_price: Float! + appType: Int! + nm: [WildberriesProductStats!] + } + + type WildberriesProductStats { + views: Int! + clicks: Int! + ctr: Float! + cpc: Float! + sum: Float! + atbs: Int! + orders: Int! + cr: Float! + shks: Int! + sum_price: Float! + name: String! + nmId: Int! + } + + type WildberriesBoosterStats { + date: String! + nm: Int! + avg_position: Float! + } + + # Типы для списка кампаний + type WildberriesCampaignsListResponse { + success: Boolean! + message: String + data: WildberriesCampaignsData! + } + + type WildberriesCampaignsData { + adverts: [WildberriesCampaignGroup!]! + all: Int! + } + + type WildberriesCampaignGroup { + type: Int! + status: Int! + count: Int! + advert_list: [WildberriesCampaignItem!]! + } + + type WildberriesCampaignItem { + advertId: Int! + changeTime: String! } `;