"use client"; import React, { useState, useEffect } from "react"; import { DashboardHeader } from "@/components/layout/dashboard-header"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { Calendar } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import { format } from "date-fns"; import { ru } from "date-fns/locale"; import { channelsApi, analyticsApi } from "@/lib/api"; import { formatCurrency, formatNumber } from "@/lib/utils/format"; import { Loader2, AlertCircle, CalendarIcon, ArrowUpDown, ArrowUp, ArrowDown, Download, } from "lucide-react"; import type { TargetChannel, CreativeAnalytics } from "@/lib/types/api"; import * as XLSX from "xlsx"; type SortField = | "name" | "placements_count" | "total_cost" | "total_subscriptions" | "total_views" | "avg_cpf" | "avg_cpm" | null; type SortDirection = "asc" | "desc" | null; export default function CreativesAnalyticsPage() { const [channels, setChannels] = useState([]); const [selectedChannelId, setSelectedChannelId] = useState(""); const [dateFrom, setDateFrom] = useState(); const [dateTo, setDateTo] = useState(); const [creatives, setCreatives] = useState([]); const [filteredCreatives, setFilteredCreatives] = useState< CreativeAnalytics[] >([]); const [loading, setLoading] = useState(true); const [loadingData, setLoadingData] = useState(false); const [error, setError] = useState(null); const [sortField, setSortField] = useState(null); const [sortDirection, setSortDirection] = useState(null); useEffect(() => { loadChannels(); }, []); const loadChannels = async () => { try { setLoading(true); const response = await channelsApi.list(); const activeChannels = response.target_channels.filter((c) => c.is_active); setChannels(activeChannels); if (activeChannels.length > 0) { setSelectedChannelId(activeChannels[0].id); } } catch (err: any) { setError(err?.error?.message || "Ошибка загрузки каналов"); } finally { setLoading(false); } }; const loadCreativesData = async () => { if (!selectedChannelId) return; try { setLoadingData(true); const response = await analyticsApi.creatives({ target_channel_id: selectedChannelId, }); setCreatives(response.creatives); setFilteredCreatives(response.creatives); } catch (err: any) { alert(err?.error?.message || "Ошибка загрузки данных"); } finally { setLoadingData(false); } }; // Сортировка const handleSort = (field: SortField) => { if (sortField === field) { if (sortDirection === "asc") { setSortDirection("desc"); } else if (sortDirection === "desc") { setSortField(null); setSortDirection(null); } } else { setSortField(field); setSortDirection("asc"); } }; const getSortIcon = (field: SortField) => { if (sortField !== field) { return ; } if (sortDirection === "asc") { return ; } return ; }; useEffect(() => { if (!sortField || !sortDirection) { setFilteredCreatives([...creatives]); return; } const sorted = [...creatives].sort((a, b) => { let aValue: string | number | null = 0; let bValue: string | number | null = 0; switch (sortField) { case "name": aValue = a.name; bValue = b.name; break; case "placements_count": aValue = a.placements_count; bValue = b.placements_count; break; case "total_cost": aValue = a.total_cost; bValue = b.total_cost; break; case "total_subscriptions": aValue = a.total_subscriptions; bValue = b.total_subscriptions; break; case "total_views": aValue = a.total_views; bValue = b.total_views; break; case "avg_cpf": aValue = a.avg_cpf || 0; bValue = b.avg_cpf || 0; break; case "avg_cpm": aValue = a.avg_cpm || 0; bValue = b.avg_cpm || 0; break; } if (typeof aValue === "string" && typeof bValue === "string") { return sortDirection === "asc" ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); } if (typeof aValue === "number" && typeof bValue === "number") { return sortDirection === "asc" ? aValue - bValue : bValue - aValue; } return 0; }); setFilteredCreatives(sorted); }, [sortField, sortDirection, creatives]); // Экспорт const exportToExcel = () => { const data = filteredCreatives.map((creative) => ({ Название: creative.name, Размещений: creative.placements_count, "Общая стоимость": creative.total_cost, Подписки: creative.total_subscriptions, Просмотры: creative.total_views, "Средний CPF": creative.avg_cpf || 0, "Средний CPM": creative.avg_cpm || 0, })); const ws = XLSX.utils.json_to_sheet(data); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Креативы"); const colWidths = [ { wch: 30 }, { wch: 12 }, { wch: 15 }, { wch: 12 }, { wch: 12 }, { wch: 12 }, { wch: 12 }, ]; ws["!cols"] = colWidths; XLSX.writeFile( wb, `Креативы_Аналитика_${new Date().toISOString().split("T")[0]}.xlsx` ); }; const exportToCSV = () => { const data = filteredCreatives.map((creative) => ({ Название: creative.name, Размещений: creative.placements_count, "Общая стоимость": creative.total_cost, Подписки: creative.total_subscriptions, Просмотры: creative.total_views, "Средний CPF": creative.avg_cpf || 0, "Средний CPM": creative.avg_cpm || 0, })); const headers = Object.keys(data[0]); const csvContent = [ headers.join(","), ...data.map((row) => headers .map((header) => { const value = row[header as keyof typeof row]; return value !== null && value !== undefined ? value : ""; }) .join(",") ), ].join("\n"); const blob = new Blob(["\ufeff" + csvContent], { type: "text/csv;charset=utf-8;", }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `Креативы_Аналитика_${new Date().toISOString().split("T")[0]}.csv`; link.click(); URL.revokeObjectURL(link.href); }; const exportToTXT = () => { const data = filteredCreatives.map((creative) => ({ Название: creative.name, Размещений: creative.placements_count, "Общая стоимость": creative.total_cost, Подписки: creative.total_subscriptions, Просмотры: creative.total_views, "Средний CPF": creative.avg_cpf || 0, "Средний CPM": creative.avg_cpm || 0, })); const headers = Object.keys(data[0]); const columnWidths = headers.map((header) => { const maxLength = Math.max( header.length, ...data.map((row) => String(row[header as keyof typeof row] || "").length ) ); return Math.min(maxLength + 2, 30); }); const txtContent = [ headers.map((header, i) => header.padEnd(columnWidths[i])).join(" | "), columnWidths.map((width) => "-".repeat(width)).join("-+-"), ...data.map((row) => headers .map((header, i) => String(row[header as keyof typeof row] || "").padEnd(columnWidths[i]) ) .join(" | ") ), ].join("\n"); const blob = new Blob([txtContent], { type: "text/plain;charset=utf-8" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `Креативы_Аналитика_${new Date().toISOString().split("T")[0]}.txt`; link.click(); URL.revokeObjectURL(link.href); }; if (loading) { return ( <>
); } return ( <>

Аналитика креативов

Производительность рекламных креативов

{error && ( {error} )} {channels.length === 0 ? ( Нет активных целевых каналов. Добавьте канал для начала работы с аналитикой. ) : ( <> {/* Фильтры */} Фильтры Настройте параметры для отображения данных
{/* Таблица */} {loadingData ? ( ) : filteredCreatives.length > 0 ? (
Креативы Показатели производительности креативов
Экспорт в Excel (.xlsx) Экспорт в CSV (.csv) Экспорт в TXT (.txt)
handleSort("name")} > Название {getSortIcon("name")} handleSort("placements_count")} > Размещений {getSortIcon("placements_count")} handleSort("total_cost")} > Общая стоимость {getSortIcon("total_cost")} handleSort("total_subscriptions")} > Подписки {getSortIcon("total_subscriptions")} handleSort("total_views")} > Просмотры {getSortIcon("total_views")} handleSort("avg_cpf")} > Средний CPF {getSortIcon("avg_cpf")} handleSort("avg_cpm")} > Средний CPM {getSortIcon("avg_cpm")} {filteredCreatives.map((creative) => ( {creative.name} {formatNumber(creative.placements_count)} {formatCurrency(creative.total_cost)} {formatNumber(creative.total_subscriptions)} {formatNumber(creative.total_views)} {creative.avg_cpf !== null ? formatCurrency(creative.avg_cpf) : "—"} {creative.avg_cpm !== null ? formatCurrency(creative.avg_cpm) : "—"} ))}
) : ( creatives.length === 0 && !loadingData && ( Нет данных по креативам. Примените фильтры для отображения данных. ) )} )}
); }