"use client"; // ============================================================================ // Placements Overview Page - Projects with Placement Statistics // ============================================================================ import { useEffect, useState, useMemo } from "react"; import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import { Loader2, FolderKanban, ChevronRight, Users, Eye, DollarSign, Target, TrendingUp, CircleDollarSign, } from "lucide-react"; import { DashboardHeader } from "@/components/layout/dashboard-header"; import { useWorkspace } from "@/components/providers/workspace-provider"; import { demoAwarePlacementsApi } from "@/lib/demo"; import { placementsApi } from "@/lib/api"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import type { Project, ProjectPlacementsSummary, Placement } from "@/lib/types/api"; // ============================================================================ // 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 formatCompact = (value: number | null | undefined): string => { if (value === null || value === undefined) return "—"; if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`; if (value >= 1000) return `${(value / 1000).toFixed(1)}K`; return value.toString(); }; // ============================================================================ // Component // ============================================================================ export default function PurchasePlansPage() { const params = useParams(); const router = useRouter(); const workspaceId = params?.workspaceId as string; const { projects, isLoadingProjects, currentWorkspace, canViewAnalytics, canViewSubscriptions } = useWorkspace(); const [placementsByProject, setPlacementsByProject] = useState>({}); const [loadingStats, setLoadingStats] = useState(true); const { isDemoMode } = useWorkspace(); useEffect(() => { if (projects.length > 0) { loadAllPlacements(); } else { setLoadingStats(false); } }, [projects, workspaceId]); const loadAllPlacements = async () => { setLoadingStats(true); const placementsMap: Record = {}; for (const project of projects) { try { const api = isDemoMode ? demoAwarePlacementsApi : placementsApi; const placements = await api.getByProject(workspaceId, project.id); placementsMap[project.id] = placements; } catch (err) { console.error(`Failed to load placements for project ${project.id}:`, err); placementsMap[project.id] = []; } } setPlacementsByProject(placementsMap); setLoadingStats(false); }; const calculateProjectSummary = (placements: Placement[]): ProjectPlacementsSummary => { const total_cost = placements.reduce((sum, p) => sum + (p.details?.cost?.value || 0), 0); const total_subscriptions = placements.reduce((sum, p) => sum + (p.placement_post?.subscriptions_count || 0), 0); const total_views = placements.reduce((sum, p) => sum + (p.placement_post?.views_count || 0), 0); const channelsSet = new Set(placements.map(p => p.channel.id)); return { total_placements: placements.length, total_channels: channelsSet.size, total_cost, total_subscriptions, total_views, avg_cpf: total_subscriptions > 0 ? total_cost / total_subscriptions : null, avg_cpm: total_views > 0 ? (total_cost / total_views) * 1000 : null, }; }; const isLoading = isLoadingProjects || loadingStats; return ( <>

Размещения

Статистика по всем размещениям ваших проектов

{isLoading ? (
) : projects.length === 0 ? (

Нет проектов

Добавьте проект через Telegram бота для отслеживания размещений

) : (
{projects.map((project) => { const placements = placementsByProject[project.id] || []; const summary = calculateProjectSummary(placements); const avatarUrl = project.username ? `https://t.me/i/userpic/320/${project.username}.jpg` : null; return ( router.push(`/dashboard/${workspaceId}/purchase-plans/${project.id}`) } >
{project.title} {project.username && ( @{project.username} )}
{summary.total_placements > 0 ? (
Размещений
{formatNumber(summary.total_placements)}
Каналов
{formatNumber(summary.total_channels)}
Охват: {canViewAnalytics ? formatCompact(summary.total_views) : "—"}
Подписок: {canViewSubscriptions ? formatCompact(summary.total_subscriptions) : "—"}
Бюджет
{formatCurrency(summary.total_cost)}
CPF
{canViewSubscriptions ? formatCurrency(summary.avg_cpf) : "—"}
) : (
Нет размещений
)}
); })}
)}
); }