"use client"; // ============================================================================ // Creatives List Page - Enhanced with Statistics // ============================================================================ import { useEffect, useState, useMemo } from "react"; import { useParams } from "next/navigation"; import Link from "next/link"; import { Loader2, Search, Plus, Folder, MoreHorizontal, Archive, Pencil, Trash2, Eye, Copy, TrendingUp, Users, ShoppingCart, BarChart3, Info, Grid, List, } from "lucide-react"; import { DashboardHeader } from "@/components/layout/dashboard-header"; import { useWorkspace } from "@/components/providers/workspace-provider"; import { creativesApi } from "@/lib/api"; import { demoAwareCreativesApi, demoAwareAnalyticsApi } from "@/lib/demo"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { BOT_USERNAME } from "@/lib/utils/constants"; import type { Creative, CreativeAnalyticsItem } from "@/lib/types/api"; // ============================================================================ // Types // ============================================================================ interface CreativeWithStats extends Creative { analytics?: CreativeAnalyticsItem; } // ============================================================================ // Helpers // ============================================================================ const formatNumber = (value: number | null | undefined): string => { if (value === null || value === undefined) return "—"; return new Intl.NumberFormat("ru-RU").format(value); }; const formatCurrency = (value: number | null | undefined): string => { if (value === null || value === undefined) return "—"; return new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB", minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(value); }; const getCreativeWord = (count: number): string => { const mod10 = count % 10; const mod100 = count % 100; if (mod100 >= 11 && mod100 <= 14) { return "креативов"; } if (mod10 === 1) { return "креатив"; } if (mod10 >= 2 && mod10 <= 4) { return "креатива"; } return "креативов"; }; // ============================================================================ // Component // ============================================================================ export default function CreativesPage() { const params = useParams(); const workspaceId = params?.workspaceId as string; const { selectedProjects, getProjectFilterId, allProjectsSelected, hasPermission } = useWorkspace(); const projectFilterId = getProjectFilterId(); const [creatives, setCreatives] = useState([]); const [analyticsData, setAnalyticsData] = useState([]); const [loading, setLoading] = useState(true); const [loadingAnalytics, setLoadingAnalytics] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false); const [search, setSearch] = useState(""); const [includeArchived, setIncludeArchived] = useState(false); const [viewMode, setViewMode] = useState<"table" | "grid">("table"); const canWrite = hasPermission("placements_write"); useEffect(() => { loadCreatives(); }, [workspaceId, projectFilterId, includeArchived]); useEffect(() => { if (creatives.length > 0) { loadAnalytics(); } }, [creatives, workspaceId, projectFilterId]); const loadCreatives = async () => { try { setLoading(true); const response = await demoAwareCreativesApi.list(workspaceId, { project_id: projectFilterId, include_archived: includeArchived, size: 100, }); setCreatives(response.items); } catch (err) { console.error("Failed to load creatives:", err); } finally { setLoading(false); } }; const loadAnalytics = async () => { try { setLoadingAnalytics(true); const response = await demoAwareAnalyticsApi.creatives(workspaceId, { project_id: projectFilterId, size: 100, }); setAnalyticsData(response.items); } catch (err) { console.error("Failed to load analytics:", err); } finally { setLoadingAnalytics(false); } }; // Merge creatives with analytics const creativesWithStats: CreativeWithStats[] = useMemo(() => { return creatives.map((creative) => ({ ...creative, analytics: analyticsData.find((a) => a.id === creative.id), })); }, [creatives, analyticsData]); const handleArchive = async (creative: Creative) => { try { await creativesApi.update(workspaceId, creative.id, {}); loadCreatives(); } catch (err) { console.error("Failed to archive creative:", err); } }; const handleDelete = async (creativeId: string) => { if (!confirm("Удалить креатив? Это действие необратимо.")) return; try { await creativesApi.delete(workspaceId, creativeId); loadCreatives(); } catch (err) { console.error("Failed to delete creative:", err); } }; const handleCopyText = (text: string) => { navigator.clipboard.writeText(text); }; // Filter by search const filteredCreatives = creativesWithStats.filter( (c) => c.name.toLowerCase().includes(search.toLowerCase()) || c.text.toLowerCase().includes(search.toLowerCase()) ); // Totals const totals = useMemo(() => { const data = filteredCreatives.filter((c) => c.analytics); return { placements: data.reduce((sum, c) => sum + (c.analytics?.placements_count || 0), 0), cost: data.reduce((sum, c) => sum + (c.analytics?.total_cost || 0), 0), subscriptions: data.reduce((sum, c) => sum + (c.analytics?.total_subscriptions || 0), 0), views: data.reduce((sum, c) => sum + (c.analytics?.total_views || 0), 0), }; }, [filteredCreatives]); return (

Креативы

{filteredCreatives.length} {getCreativeWord(filteredCreatives.length)} • {formatCurrency(totals.cost)} •{" "} {formatNumber(totals.subscriptions)} подписчиков

{canWrite && ( )}
setSearch(e.target.value)} className="pl-10" />
setIncludeArchived(checked === true)} />
{loading ? (
) : filteredCreatives.length === 0 ? (

Нет креативов

{search ? "Ничего не найдено по вашему запросу" : "Создайте первый креатив для начала работы"}

{canWrite && !search && ( )}
) : viewMode === "table" ? ( Название Текст
Размещений Количество размещений с этим креативом
Потрачено Сумма всех размещений с этим креативом
Подписчики Привлеченные подписчики
Сред. CPS Cost Per Subscription — средняя стоимость подписчика
Статус
{filteredCreatives.map((creative) => ( {creative.name}
{creative.text.replace("{invite_link}", "[ссылка]")}
{creative.analytics?.placements_count ?? creative.placements_count} {loadingAnalytics ? ( ) : ( formatCurrency(creative.analytics?.total_cost) )} {loadingAnalytics ? ( ) : ( formatNumber(creative.analytics?.total_subscriptions) )} {loadingAnalytics ? ( ) : ( formatCurrency(creative.analytics?.avg_cpf) )} {creative.status === "active" ? "Активен" : "Архив"} Просмотр Аналитика handleCopyText(creative.text)}> Копировать текст {canWrite && ( <> Редактировать handleArchive(creative)}> {creative.status === "active" ? "В архив" : "Восстановить"} {creative.placements_count === 0 && ( handleDelete(creative.id)} className="text-destructive focus:text-destructive" > Удалить )} )}
))}
) : ( // Grid View
{filteredCreatives.map((creative) => (
{creative.name} {creative.text.replace("{invite_link}", "[ссылка]")}
{creative.status === "active" ? "Активен" : "Архив"}
{/* Stats */} {creative.analytics && (
{formatCurrency(creative.analytics.total_cost)}
Потрачено
{formatNumber(creative.analytics.total_subscriptions)}
Подписчиков
{formatNumber(creative.analytics.total_views)}
Просмотров
{formatCurrency(creative.analytics.avg_cpf)}
Сред. CPS
)} {/* Actions */}
))}
)}
{/* Dialog для создания креатива */} Создание креатива Создание креатива работает через Telegram бота

Чтобы создать креатив:

  1. Перейдите в Telegram бота @{BOT_USERNAME}
  2. Откройте раздел Креативы
  3. Нажмите Создание
  4. Перешлите созданный креатив боту
  5. Креатив автоматически отобразится на платформе
); }